캐릭터 이동/앉기/달리기를 구현해놓은 상태로 애님 블루프린트 구현 및 애니메이션 연결작업을 내일 할 예정이다.
// Character.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MJCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
class UAnimMontage;
struct FInputActionValue;
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All); // Chracter Log 정의
UCLASS()
class MIJUNG_API AMJCharacter : public ACharacter
{
GENERATED_BODY()
protected:
// SpringArm
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
USpringArmComponent* CameraArm;
/** Follow camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
UCameraComponent* FollowCamera;
/** MappingContext */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputMappingContext* DefaultMappingContext;
/** Jump Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* JumpAction;
/** Move Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* MoveAction;
/** Look Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* LookAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* InteractAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* CrouchAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* SprintAction;
public:
AMJCharacter();
virtual void BeginPlay() override;
UPROPERTY(EditDefaultsOnly, Category = "Movement")
float SprintSpeed = 600.f;
UPROPERTY(EditDefaultsOnly, Category = "Movement")
float WalkSpeed = 300.f;
#pragma region InputActions
protected:
/** Called for movement input */
void Move(const FInputActionValue& Value);
/** Called for looking input */
void Look(const FInputActionValue& Value);
void OnInteract(const FInputActionValue& Value);
void HandleCrouchToggle(const FInputActionValue& Value);
void StartSprint(const FInputActionValue& Value);
void StopSprint(const FInputActionValue& Value);
#pragma endregion InputActions
protected:
virtual void NotifyControllerChanged() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
public:
UPROPERTY(EditDefaultsOnly, Category = "Interact")
TObjectPtr<UAnimMontage> CharacterInteractMontage;
UPROPERTY(Replicated)
bool bIsInteracting = false; // 상호작용 여부
private:
FTimerHandle SpeedInterpTimerHandle;
UPROPERTY(EditDefaultsOnly, Category = "Movement")
float SpeedInterpSpeed = 5.f;
// Sprint -> Walk 보간
void InterpSpeed();
#pragma region Replicated
public:
UPROPERTY(ReplicatedUsing = OnRep_IsSprinting ,VisibleDefaultsOnly ,BlueprintReadOnly ,Category = "Sprint")
uint8 bIsSprinting : 1; // 달리기 플래그
protected:
UFUNCTION(NetMulticast, Reliable)
void Multicast_PlayMontage(UAnimMontage* Montage, float PlayRate = 1.f);
UFUNCTION(Server, Reliable)
void ServerRPC_SetCrouching(bool bNewCrouching);
UFUNCTION(Server, Reliable)
void ServerRPC_SetSprinting(bool bNewSprinting);
UFUNCTION()
void OnRep_IsSprinting();
void UpdateMovementSpeed(); // 속도 변경
#pragma endregion
};
// Character.cpp
#include "Character/MJCharacter.h"
#include "Engine/LocalPlayer.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/Controller.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "Net/UnrealNetwork.h"
DEFINE_LOG_CATEGORY(LogTemplateCharacter);
// Sets default values
AMJCharacter::AMJCharacter()
{
bReplicates = true;
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
GetCharacterMovement()->SetIsReplicated(true);
// Configure character movement
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;
// 크라우치 가능하도록 설정
GetCharacterMovement()->NavAgentProps.bCanCrouch = true;
// 리슨 서버 및 일반 클라이언트 환경에서만 카메라를 생성
if (!IsRunningDedicatedServer())
{
CameraArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraArm"));
CameraArm->SetupAttachment(RootComponent);
CameraArm->TargetArmLength = 400.0f;
CameraArm->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraArm, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
}
// 헤더에서 비트필드로 설정되어있으므로 생성자에서 초기값 설정
bIsSprinting = false;
}
// Called when the game starts or when spawned
void AMJCharacter::BeginPlay()
{
Super::BeginPlay();
}
void AMJCharacter::Move(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
void AMJCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
}
void AMJCharacter::OnInteract(const FInputActionValue& Value)
{
if (bIsInteracting) return; // 이미 상호작용 중이면 무시
if (!GetCharacterMovement() ||
GetCharacterMovement()->IsFalling() ||
GetCharacterMovement()->IsCrouching()) return;
bIsInteracting = true;
UE_LOG(LogTemplateCharacter, Warning, TEXT("OnInteract!"));
}
void AMJCharacter::HandleCrouchToggle(const FInputActionValue& Value)
{
if (!GetCharacterMovement())
{
UE_LOG(LogTemp, Error, TEXT("[%s] CharacterMovement is null - Player: %s"),
HasAuthority() ? TEXT("Server") : TEXT("Client"), *GetName());
return;
}
if (!bIsCrouched)
{
Crouch();
if (!HasAuthority())
{
ServerRPC_SetCrouching(true);
}
}
else
{
UnCrouch();
if (!HasAuthority())
{
ServerRPC_SetCrouching(false);
}
}
}
void AMJCharacter::StartSprint(const FInputActionValue& Value)
{
if (bIsCrouched) return;
if (!HasAuthority())
{
ServerRPC_SetSprinting(true);
UpdateMovementSpeed();
}
else
{
bIsSprinting = true;
UpdateMovementSpeed();
}
}
void AMJCharacter::StopSprint(const FInputActionValue& Value)
{
if (!HasAuthority())
{
ServerRPC_SetSprinting(false);
UpdateMovementSpeed();
}
else
{
bIsSprinting = false;
UpdateMovementSpeed();
}
}
void AMJCharacter::NotifyControllerChanged()
{
Super::NotifyControllerChanged();
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
if (PlayerController->IsLocalController()) // 로컬 컨트롤러인지 확인
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
}
}
}
// Called to bind functionality to input
void AMJCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 로컬 플레이어의 입력을 설정할 때만 실행되도록 Controller 검사 추가
if (Controller && Controller->IsLocalController())
{
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
// Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
// Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ThisClass::Move);
// Looking
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ThisClass::Look);
// Crouching - Started 이벤트로 토글
EnhancedInputComponent->BindAction(CrouchAction, ETriggerEvent::Started, this, &ThisClass::HandleCrouchToggle);
// Sprint Start
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Triggered, this, &ThisClass::StartSprint);
// Sprint Stop
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &ThisClass::StopSprint);
// Interact
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &ThisClass::OnInteract);
}
else
{
UE_LOG(LogTemplateCharacter, Error, TEXT("'%s' Failed to find an Enhanced Input component!"), *GetNameSafe(this));
}
}
}
void AMJCharacter::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, bIsSprinting);
}
void AMJCharacter::InterpSpeed()
{
if (!GetCharacterMovement())
{
GetWorldTimerManager().ClearTimer(SpeedInterpTimerHandle);
return;
}
float CurrentSpeed = GetCharacterMovement()->MaxWalkSpeed;
float NewSpeed = FMath::FInterpTo(
CurrentSpeed,
WalkSpeed,
0.16f,
SpeedInterpSpeed
);
GetCharacterMovement()->MaxWalkSpeed = NewSpeed;
// 속도 보간 끝 타이머 해제 및 로그 출력
if (FMath::IsNearlyEqual(NewSpeed, WalkSpeed, 1.0f))
{
GetWorldTimerManager().ClearTimer(SpeedInterpTimerHandle);
GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
UE_LOG(LogTemplateCharacter, Log, TEXT("[%s] Speed Interp Complete: %f"),
HasAuthority() ? TEXT("Server") : TEXT("Client"),
WalkSpeed);
}
}
void AMJCharacter::ServerRPC_SetSprinting_Implementation(bool bNewSprinting)
{
if (!IsValid(this) || !GetCharacterMovement()) return;
bIsSprinting = bNewSprinting;
UpdateMovementSpeed();
}
void AMJCharacter::ServerRPC_SetCrouching_Implementation(bool bNewCrouching)
{
if (bNewCrouching)
{
Crouch();
}
else
{
UnCrouch();
}
}
void AMJCharacter::OnRep_IsSprinting()
{
// Sprint OnRep 필요시 구현
}
void AMJCharacter::Multicast_PlayMontage_Implementation(UAnimMontage* Montage, float PlayRate)
{
// 캐릭터 몽타주 재생
}
void AMJCharacter::UpdateMovementSpeed()
{
if (!GetCharacterMovement()) return;
if (bIsSprinting)
{
GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
}
else
{
// Sprint -> Walk 보간
if (!GetWorldTimerManager().IsTimerActive(SpeedInterpTimerHandle))
{
GetWorldTimerManager().SetTimer(
SpeedInterpTimerHandle,
this,
&ThisClass::InterpSpeed,
0.016f,
true
);
}
}
}
'언리얼 팀프로젝트' 카테고리의 다른 글
| 최종 팀 프로젝트 -캐릭터 모듈 컴포넌트 구현 (0) | 2025.11.05 |
|---|---|
| 최종 팀프로젝트 - 캐릭터 기본 애니메이션 구현 (0) | 2025.11.04 |
| 멀티 플레이(데디서버) 물속 들어갔을때 체력 줄어드는 기믹 구현 완료 (0) | 2025.10.02 |
| 멀티 플레이(데디 서버) Sliding 이펙트 및 Attack, Hit 사운드 재생 구현 (0) | 2025.10.01 |
| 멀티 플레이어(데디 서버) Hit 이펙트 구현 완료 (0) | 2025.09.30 |