언리얼엔진 학습 및 과제

액터 리플리케이션을 이용한 게임 제작 (8)

park94128 2026. 1. 5. 23:56

● 내가 직접 애니메이션을 만들자

미리 만들어진 애니메이션 블루프린트를 분석하여 문제를 해결할 수도 있겠지만, 사실 NPC는 그냥 돌아다니기만 하면 그만이다. 그래서 그냥 내가 직접 애니메이션을 만들기로 결정했다.

 

A. NPC의 이동 여부를 블루프린트에서 알 수 있어야 한다.

스테이트 트리의 태스크를 만들 때 NPC의 이동 여부에 따라 태스크 종료를 결정하도록 만들었다. 그래서 이를 애니메이션에도 써볼까 했다.

애니메이션 블루프린트에서 호출할 함수를 구현하기 위해 애님 인스턴스 클래스를 만들었다. 그리고 여기서 AI컨트롤러를 불러오려고 했는데 생각보다 고려할 것이 많았다. 컨트롤러는 클라이언트에는 없고 서버에만 있기 때문에 발생하는 문제가 제일 컸다. 그 외에도 코드가 다소 지저분해짐을 보고 이 방법은 틀렸다고 생각했다. 이후 방법을 찾다 내린 결론은 다음과 같다.

public:
	virtual void NativeBeginPlay() override;;

private:
	UFUNCTION(BlueprintPure)
	bool IsMoving() const;
	
	UPROPERTY()
	UPawnMovementComponent* MovementComponent;
void UAICharAnimation::NativeBeginPlay()
{
	Super::NativeBeginPlay();

	const AMainGameAIChar* AIChar = Cast<AMainGameAIChar>(GetOwningActor());
	if (!IsValid(AIChar))
	{
		UE_LOG(LogTemp, Error, TEXT("%s : MainGameAIChar가 유효하지 않습니다."), *GetNameSafe(this));
	}

	MovementComponent = AIChar->GetMovementComponent();
}

bool UAICharAnimation::IsMoving() const
{
	if (!IsValid(MovementComponent))
	{
		return false;
	}
	
	return MovementComponent->Velocity != FVector::ZeroVector;
}

AI 컨트롤러의 이동 여부를 보는 것이 아니라 캐릭터가 가진 무브먼트 컴포넌트의 Velocity를 보는 것으로 구현했다. 캐릭터가 움직이면 Velocity가 (0, 0)인 벡터가 아닐테니 이를 이용한 bool 함수로 구현했다.

 

B. AI 전용 애니메이션 블루프린트

금방 만든 애님 인스턴스 클래스를 기반으로 애니메이션 블루프린트를 생성한다.

스테이트 머신을 하나 만들고 Output Pose에 연결한다.

스테이트 머신에는 2개의 스테이트를 만들고 각각 Idle, Walk라고 명명한다. 그리고 이 2개의 스테이트 사이를 양방 이동이 가능하도록 연결한다. Entry에서는 Idle로 연결한다.

Idle 스테이트에서는 정지 상태 애니메이션을, Walk 스테이트에서는 걷는 애니메이션을 실행하게 만든다.

 

Idle에서 Walk로 이동하는 규칙과 Walk에서 Idle로 이동하는 규칙에는 각각 클래스에서 만든 함수를 적용한다. 단, Walk에서 Idle로 이동하는 규칙에서는 중간에 NOT을 경유하여 적용한다.

 

그런데 여기까지 하고 컴파일을 해보니 경고가 발생한다. 클래스에서 구현한 함수에 문제가 있다고 한다.

문제가 무엇인고 하니 스레드 세이프(Thread Safe)에 대한 보장이 되지 않는 함수라고 한다.

 

private:
	UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe))
	bool IsMoving() const;

함수의 내용은 무브먼트 컴포넌트를 통해 Velocity의 값을 읽기만 하는데다가 무브먼트 컴포넌트가 null이면 그냥 false를 반환한다. 그래서 스레드 관련 문제가 없을거라 판단하고 BlueprintThreadSafe키워드를 추가했다.

 

이 키워드가 정확하게 무엇인지 검색해보니 비게임 스레드에서도 문제 없이 부를 수 있다고 표시하는 키워드라고 한다. (참고)

 

AI 캐릭터 블루프린트에서 내가 만든 애니메이션으로 교체한다.

그리고 실행해보니 움직이면 걷고 아니면 가만히 있음을 확인했다.

 

● 자연스러운 회전

AI컨트롤러로 이동할 때 회전 없이 순간적으로 이동 방향으로 캐릭터를 회전하는 문제는 생각보다 간단했다. 그냥 컨트롤러에 의한 회전을 하지 않으면 해결되는 문제였다.

그리고 무브먼트 방향으로 회전 조정(Orient Rotation To Movement)이 켜진 상태라면 이동 하는 방향으로 회전하게 된다. (아마 기본값이 true일 것이다.) 회전 속도의 기본값은 500도로 적혀있던데 이것도 내가 조종하는 캐릭터와 비교하면서 조절하면 될 것이다.

 

● 걸어간다고 했는데 빠르게 움직인다. 그리고 나도 천천히 움직이자

AI 캐릭터를 만들 때 캐릭터의 코드를 그대로 긁어가서 만들다보니 블루프린트에서도 그 값이 반영됐다. 그래서 뛰어가는 이동 속도로 걷는 모션을 실행하는 어색한 연출을 보여주고 있다.

그래서 바로 밑에 있는 웅크린 상태의 속도와 동일하게 300으로 맞춰줬다. 그러니 자연스럽게 걷는 것 처럼 보인다.

그리고 플레이어 캐릭터도 동일하게 변경해줬다. 그래야 NPC와 동일하게 움직이니 구분이 안될 것이다.

 

미리 만들어진 캐릭터 애니메이션을 분석해보면 이동 속도에 따라 뛰거나 걷는 것이 바뀐다. 그래서 캐릭터의 이동 속도를 줄이기만 한다면 알아서 뛰는 모습이 아닌 걷는 모습이 출력된다. 따라서 걷기만 하는 AI 캐릭터와 동일하게 행동할 수 있다.

걷기/뛰기 전환 기점이 300이니 내가 설정한 값이 딱 최대 걷는 속도임을 알 수 있다.

 

왼쪽 : 인풋 액션, 오른쪽 : 인풋 매핑 컨텍스트

다만 계속 걷기만 하면 답답하니 뛸 수 있게 만들었다. 쉬프트를 누르는 동안 달리고 아니면 걷게 만드려고 한다. 달리면 빠른 이동이 가능하지만 NPC와 다른 행동을 하는 것이니 들킬 위험이 있는 행동이니 신중하게 뛰어야 할 것이다.

 

public:
	//달리기 시작
	UFUNCTION(BlueprintCallable, Category="Input")
	virtual void DoRunStart();

	//달리기 중지
	UFUNCTION(BlueprintCallable, Category="Input")
	virtual void DoRunEnd();

protected:
	//달리기 입력
	UPROPERTY(EditAnywhere, Category="Input")
	UInputAction* RunAction;
    
private:
	//걷기/달리기 이동 속도 (X : 걷기, Y : 달리기)
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Character State", meta = (AllowPrivateAccess = true))
	FVector2D MovementSpeed;

달리기 관련 내용을 플레이어 캐릭터의 헤더에 선언해준다. (걷기/달리기 이동 속도는 생성자에서 300, 600으로 기본값을 지정해줬다.)

 

//점프
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AMainGameCharacter::DoJumpStart);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AMainGameCharacter::DoJumpEnd);

//이동
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMainGameCharacter::Move);

//카메라
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AMainGameCharacter::Look);

//마우스 클릭
EnhancedInputComponent->BindAction(MouseClickAction, ETriggerEvent::Started, this, &AMainGameCharacter::Attack);

//달리기
EnhancedInputComponent->BindAction(RunAction, ETriggerEvent::Started, this, &AMainGameCharacter::DoRunStart);
EnhancedInputComponent->BindAction(RunAction, ETriggerEvent::Completed, this, &AMainGameCharacter::DoRunEnd);

다른 입력과 동일하게 바인딩을 해준다.

 

void AMainGameCharacter::DoRunStart()
{
	GetCharacterMovement()->MaxWalkSpeed = MovementSpeed.Y;
}

void AMainGameCharacter::DoRunEnd()
{
	GetCharacterMovement()->MaxWalkSpeed = MovementSpeed.X;
}

선언한 각 함수의 내용은 쉬프트를 누르면 빠르게, 아니면 느리게 이동하도록 이동 속도를 지정해준다.

 

잘 작동한다. 그리고 이 내용은 네트워크 플레이에도 잘 반영된다. 캐릭터 위치 및 애니메이션이 리플레케이션 되기 때문이다.

 

● 너도 죽어

플레이어 캐릭터가 공격받으면 쓰러지는데 NPC는 그렇지 않으니 불공평하다. NPC도 공격 받게 만들고 쓰러지게 만들자.

 

protected:
	virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
    
   private:
	//사망 연출 실행
	UFUNCTION(NetMulticast, Reliable)
	void DeathMotion();

플레이어 캐릭터에서 했던 것 처럼 TakeDamage함수와 NetMulticast인 사망 연출 함수를 생성한다. AI 캐릭터는 서버에서 작동하기 때문에 서버용 RPC함수를 선언할 필요는 없다.

 

float AMainGameAIChar::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	if (bIsDeath)
	{
		return 0.0f;
	}

	DeathMotion();

	return 1.0f;
}

TakeDamage 함수에서는 서버 RPC 함수가 없으니 즉시 사망 연출 함수를 실행한다.

 

void AMainGameAIChar::DeathMotion_Implementation()
{
	//사망 처리
	bIsDeath = true;

	//더 이상 조종 불가
	GetCharacterMovement()->DisableMovement();
	GetMesh()->bPauseAnims = true;
	GetMesh()->SetSimulatePhysics(true);

	//(서버) 빙의 해제
	if (IsLocallyControlled())
	{
		if (AController* AIController = GetController(); IsValid(AIController))
		{
			AIController->UnPossess();
		}
	}
}

사망 연출은 플레이어 캐릭터와 동일하다 다만 서버라면 빙의 해제하는 기능이 추가된다.

 

public:
	virtual void OnUnPossess() override;
void AMainGameAIController::OnUnPossess()
{
	Super::OnUnPossess();

	StateTreeAIComp->StopLogic(TEXT("Death"));
	StopMovement();
}

AI 컨트롤러에서는 빙의 해제시 이동 및 스테이트 트리를 중지하도록 만든다.

 

그리고 실행해보니 얘도 땅 밑으로 사라진다. 콜리전 설정에서 콜리전 프리셋을 Ragdoll로 설정해준다.

 

둘 다 죽어

실행해보니 플레이어, NPC 할 것 없이 잘 작동한다. 네트워킹 환경에서도 문제없이 작동한다.