[CH4-03] IA, IMC & Character

김여울·2025년 9월 10일

내일배움캠프

목록 보기
73/139

캐릭터 동작 연결/추가하기

필요한 동작들 InputAction 만들기

InputMappingContext에 연결하기

캐릭터 블루프린트 디테일에서 Input 설정하기

코드로 연결하기

Character.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "DCCharacter.generated.h"

class UInputMappingContext;
class UInputAction;
class UAnimMontage;

UCLASS()
class DC_API ADCCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ADCCharacter();

	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	/** ----- 입력 리소스 ----- */
	UPROPERTY(EditDefaultsOnly, Category="Input")
	TObjectPtr<UInputMappingContext> DefaultMappingContext;

	UPROPERTY(EditDefaultsOnly, Category="Input")
	TObjectPtr<UInputAction> MoveAction;

	UPROPERTY(EditDefaultsOnly, Category="Input")
	TObjectPtr<UInputAction> LookAction;

	UPROPERTY(EditDefaultsOnly, Category="Input")
	TObjectPtr<UInputAction> JumpAction;

	UPROPERTY(EditDefaultsOnly, Category="Input")
	TObjectPtr<UInputAction> DanceAction;

	UPROPERTY(EditDefaultsOnly, Category="Input")
	TObjectPtr<UInputAction> AttackAction;

	UPROPERTY(EditDefaultsOnly, Category="Input")
	TObjectPtr<UInputAction> SlideAction;

	/** ----- 공격 몽타주(슬롯 재생) ----- */
	UPROPERTY(EditDefaultsOnly, Category="Anim")
	TObjectPtr<UAnimMontage> AttackMontage;

	/** ----- 상태 Getter (AnimInstance가 사용) ----- */
	UFUNCTION(BlueprintPure, Category="State")
	bool GetIsDancing() const
	{
		return bIsDancing;
	}

	UFUNCTION(BlueprintPure, Category="State")
	bool GetCanAttack() const
	{
		return bCanAttack;
	}

	UFUNCTION(BlueprintPure, Category="State")
	bool GetIsSliding() const
	{
		return bIsSliding;
	}

protected:
	/** 네트워크 복제 예시 변수들 */
	UPROPERTY(Replicated)
	float CurrentHealth = 100.f;

	UPROPERTY(Replicated)
	bool bIsCriminal = false;

	/** 춤 상태: ReplicatedUsing으로 변경 시 클라도 즉시 반영 */
	UPROPERTY(ReplicatedUsing=OnRep_IsDancing)
	bool bIsDancing = false;

	/** 공격 가능 플래그: 서버가 관리, 필요 시 Replicate */
	UPROPERTY(Replicated)
	bool bCanAttack = false;

	/** 슬라이드: RepNotify로 이동속도 등 즉시 반영 */
	UPROPERTY(ReplicatedUsing=OnRep_IsSliding)
	bool bIsSliding = false;

	/** 타이머 핸들: 춤 끝나고 공격 가능 켜기 */
	FTimerHandle AttackTimerHandle;

protected:
	/** 입력 핸들러 */
	void Move(const struct FInputActionValue& Value);
	void Look(const struct FInputActionValue& Value);
	void Dance(const struct FInputActionValue& Value);
	void Attack(const struct FInputActionValue& Value);
	void StartSlide(const struct FInputActionValue& Value);
	void EndSlide(const struct FInputActionValue& Value);

	/** 서버에서 상태 토글 */
	UFUNCTION(Server, Reliable)
	void Server_SetIsDancing(bool NewValue);

	UFUNCTION(Server, Reliable)
	void Server_SetIsSliding(bool NewValue);

	/** RepNotify */
	UFUNCTION()
	void OnRep_IsSliding();

	UFUNCTION()
	void OnRep_IsDancing();

	/** 유틸 */
	void EnableAttack();

public:
	// 네트 리플리케이션 등록
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

protected:
    /** ----- 캐릭터 상태 (체력, 이동속도) ----- (성준모) */
    float CurrentHP = 100.f;
    float MaxHP = 100.f;

    //float CurrentSpeed = 600.f;
    float MaxSpeed = 700.f;

public:
    /** ----- 캐릭터 상태 (체력, 이동속도) ----- (성준모) */
    float GetCurrentHP() const { return CurrentHP; }
    float GetMaxHP() const { return MaxHP; }
    void SetCurrentHP(float NewHP);

    //float GetCurrentSpeed() const { return CurrentSpeed; }
    float GetMaxSpeed() const { return MaxSpeed; }
};

Character.cpp

#include "Character/DCCharacter.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Net/UnrealNetwork.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Components/CapsuleComponent.h"
#include "Components/PrimitiveComponent.h"
#include "GameFramework/Character.h"
#include "TimerManager.h"

ADCCharacter::ADCCharacter()
{
	PrimaryActorTick.bCanEverTick = true;

	GetCapsuleComponent()->InitCapsuleSize(42.f, 96.f);

	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;

	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->RotationRate = FRotator(0.f, 500.f, 0.f);
}

void ADCCharacter::BeginPlay()
{
	Super::BeginPlay();

	if (APlayerController* PC = Cast<APlayerController>(Controller))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(DefaultMappingContext, 0);
		}
	}
}

void ADCCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

void ADCCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
		EIC->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
		EIC->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);

		EIC->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ADCCharacter::Move);
		EIC->BindAction(LookAction, ETriggerEvent::Triggered, this, &ADCCharacter::Look);

		EIC->BindAction(DanceAction, ETriggerEvent::Started, this, &ADCCharacter::Dance);
		EIC->BindAction(AttackAction, ETriggerEvent::Started, this, &ADCCharacter::Attack);

		EIC->BindAction(SlideAction, ETriggerEvent::Started, this, &ADCCharacter::StartSlide);
		EIC->BindAction(SlideAction, ETriggerEvent::Completed, this, &ADCCharacter::EndSlide);
	}
}

void ADCCharacter::Move(const FInputActionValue& Value)
{
	// 춤추는 중엔 이동 금지
	if (bIsDancing)
	{
		return;
	}

	const FVector2D Axis = Value.Get<FVector2D>();
	if (Controller)
	{
		const FRotator ControlRot = Controller->GetControlRotation();
		const FVector Forward = FRotationMatrix(ControlRot).GetUnitAxis(EAxis::X);
		const FVector Right = FRotationMatrix(ControlRot).GetUnitAxis(EAxis::Y);

		AddMovementInput(Forward, Axis.Y);
		AddMovementInput(Right, Axis.X);
	}
}

void ADCCharacter::Look(const FInputActionValue& Value)
{
	const FVector2D Axis = Value.Get<FVector2D>();
	if (Controller)
	{
		AddControllerYawInput(Axis.X);
		AddControllerPitchInput(Axis.Y);
	}
}

void ADCCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(ADCCharacter, CurrentHealth);
	DOREPLIFETIME(ADCCharacter, bIsCriminal);
	DOREPLIFETIME(ADCCharacter, bIsSliding);
	DOREPLIFETIME(ADCCharacter, bIsDancing);
	DOREPLIFETIME(ADCCharacter, bCanAttack);
}

// 성준모: 체력 설정 기능? 수정하셔도 되요!
void ADCCharacter::SetCurrentHP(float NewHP)
{
    CurrentHP = FMath::Clamp(NewHP, 0.f, MaxHP);

    if (CurrentHP <= 0.f)
    {
        // 사망 기능 구현
    }
}

void ADCCharacter::Dance(const FInputActionValue& /*Value*/)
{
	// 중복 방지
	if (bIsDancing)
	{
		return;
	}

	// 서버로 상태 변경 요청
	if (HasAuthority())
	{
		Server_SetIsDancing(true);
	}
	else
	{
		Server_SetIsDancing(true);
	}

	UE_LOG(LogTemp, Warning, TEXT("춤 시작"));

	// 5초 뒤 춤 종료 + 공격 가능
	GetWorldTimerManager().SetTimer(AttackTimerHandle, this, &ADCCharacter::EnableAttack, 5.0f, false);
}

void ADCCharacter::Attack(const FInputActionValue& /*Value*/)
{
	if (!bCanAttack)
	{
		UE_LOG(LogTemp, Warning, TEXT("공격 불가"));
		return;
	}

	UE_LOG(LogTemp, Warning, TEXT("공격!"));

	// 한 번 사용 시 비활성화(원한다면 쿨다운 타이머로 다시 true)
	bCanAttack = false;

	// 몽타주 재생(AnimBP에 DefaultSlot 연결해두어야 함)
	if (AttackMontage)
	{
		// 서버 권한 기준 재생(멀티플레이 반영)
		if (UAnimInstance* AnimInst = GetMesh() ? GetMesh()->GetAnimInstance() : nullptr)
		{
			AnimInst->Montage_Play(AttackMontage, 1.0f);
		}
	}
}

void ADCCharacter::EnableAttack()
{
	// 춤 종료 -> 서버 반영
	if (HasAuthority())
	{
		Server_SetIsDancing(false);
	}
	else
	{
		Server_SetIsDancing(false);
	}

	bCanAttack = true;

	UE_LOG(LogTemp, Warning, TEXT("춤 종료, 공격 가능"));
}

void ADCCharacter::Server_SetIsDancing_Implementation(bool NewValue)
{
	bIsDancing = NewValue;

	// 이동 제어
	if (bIsDancing)
	{
		GetCharacterMovement()->StopMovementImmediately();
		GetCharacterMovement()->DisableMovement();
	}
	else
	{
		GetCharacterMovement()->SetMovementMode(MOVE_Walking);
	}

	// 로컬에서도 즉시 반영되게 RepNotify 수동 호출(옵션)
	OnRep_IsDancing();
}

void ADCCharacter::Server_SetIsSliding_Implementation(bool NewValue)
{
	bIsSliding = NewValue;

	// 로컬에서도 즉시 반영되게 RepNotify 수동 호출(옵션)
	OnRep_IsSliding();
}

void ADCCharacter::OnRep_IsSliding()
{
	if (bIsSliding)
	{
		UE_LOG(LogTemp, Warning, TEXT("슬라이드 시작"));
		GetCharacterMovement()->MaxWalkSpeed = 300.f;
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("슬라이드 종료"));
		GetCharacterMovement()->MaxWalkSpeed = 600.f;
	}
}

void ADCCharacter::OnRep_IsDancing()
{
	// 필요 시 클라에서 효과/사운드 등 처리
}

void ADCCharacter::StartSlide(const FInputActionValue& /*Value*/)
{
	// 주변 밀치기(네가 준 로직 그대로)
	const FVector Center = GetActorLocation();
	const float Radius = 200.f;

	TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes;
	ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECollisionChannel::ECC_Pawn));

	TArray<AActor*> ActorsToIgnore;
	ActorsToIgnore.Add(this);

	TArray<AActor*> OutActors;

	UKismetSystemLibrary::SphereOverlapActors(
		GetWorld(),
		Center,
		Radius,
		ObjectTypes,
		AActor::StaticClass(),
		ActorsToIgnore,
		OutActors
	);

	for (AActor* OverlappedActor : OutActors)
	{
		if (ACharacter* OtherCharacter = Cast<ACharacter>(OverlappedActor))
		{
			FVector LaunchDir = OtherCharacter->GetActorLocation() - GetActorLocation();
			LaunchDir.Normalize();

			const float CurrentSpeed = GetVelocity().Size();
			const float PushStrength = FMath::Clamp(CurrentSpeed * 1.5f, 500.f, 1500.f);

			OtherCharacter->LaunchCharacter(LaunchDir * PushStrength, true, true);
		}
	}

	// 서버에 슬라이드 상태 반영
	if (HasAuthority())
	{
		Server_SetIsSliding(true);
	}
	else
	{
		Server_SetIsSliding(true);
	}
}

void ADCCharacter::EndSlide(const FInputActionValue& /*Value*/)
{
	if (HasAuthority())
	{
		Server_SetIsSliding(false);
	}
	else
	{
		Server_SetIsSliding(false);
	}
}

0개의 댓글