언리얼 팀프로젝트

최종 팀프로젝트 - 캐릭터 구현 (이동/앉기/달리기)

Turtle_Jun 2025. 11. 3. 21:06

 

 

캐릭터 이동/앉기/달리기를 구현해놓은 상태로 애님 블루프린트 구현 및 애니메이션 연결작업을 내일 할 예정이다.

 

// 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
				);
		}
	}
}