언리얼엔진 학습 및 과제

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

park94128 2025. 12. 22. 23:46

● 제일 큰 문제

이전 글에 애니메이션 블루프린트에 공격 모션을 넣었다면 알아서 리플리케이션이 될 것이라고 적었다. 그러니까, 지금과 같이 그냥 애니메이션 몽타주를 실행하면 리플레케이션이 안된다. 즉, 자신은 주먹을 휘두르는데 다른 사람이 보기엔 그냥 가만히 있는 것으로 보인다.

private:
	//서버에서 실행하는 공격 요청
	UFUNCTION(Server, Reliable)
	void AttackOrder();

	//공격 모션 실행
	UFUNCTION(NetMulticast, Reliable)
	void AttackMotion();

나의 선택은 RPC를 사용하는 것이다. 서버용 함수와 모든 곳에서 실행하는 넷멀티캐스트 함수를 선언했다.

 

void AMainGameCharacter::Attack()
{
	//중복 공격 불가
	if (CurrentAttackDelay > 0)
	{
		return;
	}

	CurrentAttackDelay = AttackDelayTime;

	//서버에 공격 요청하기
	AttackOrder();
}

void AMainGameCharacter::AttackOrder_Implementation()
{
	AttackMotion();
}

void AMainGameCharacter::AttackMotion_Implementation()
{
	if (IsValid(MainCharAnimation))
	{
		MainCharAnimation->PlayAttackAnimation();
	}
}

기존 공격 함수에서 몽타주 실행 구간을 모든 곳에서 실행하는 함수로 옮겼다. 그리고 이 함수를 서버 함수에서 실행하도록 했다. 기존의 공격 함수는 서버 함수를 실행하게 했다.

 

이렇게 하면 다음과 같은 순서대로 동작할 테니 문제가 해결될 것이다.

- 먼저 한 캐릭터에서 공격시 서버 함수를 실행한다.

- 서버에서는 넷멀티캐스트 함수를 실행한다.

- 모든 곳에서 그 캐릭터의 모션을 실행한다.

 

그리고 그 예상은 정확했다.

 

● 사망 모션 만들기

공격 모션에 대한 문제를 해결했으니 사망 모션을 만든다. 이 모션도 모든 클라이언트에게 동일하게 보여줘야 하니 위 해결방법처럼 서버 함수와 넷멀티캐스트 함수를 이용하려고 한다.

 

public:
	virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
    
private:
	//서버에서 실행하는 사망 연출 요청
	UFUNCTION(Server, Reliable)
	void DeathMotionOrder();

	//사망 연출 실행
	UFUNCTION(NetMulticast, Reliable)
	void DeathMotion();

	//사망 여부
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character State", meta = (AllowPrivateAccess = true))
	bool bIsDeath;

일단 사망 모션의 시작점은 TakeDamage함수다. 이 함수는 Pawn에서 선언된 가상 함수인데 이를 이용하여 구현해야 앞으로의 개발이 편해보인다. 그리고 언급한대로 서버 함수와 넷멀티캐스트 함수를 선언했다.

그리고 사망 연출을 중복 실행하면 안되니, 그리고 사망시 다른 행동을 하면 안될 것을 위해 전용 bool 변수도 선언했다.

 

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

	//서버를 통한 사망 모션
	DeathMotionOrder();
	
	return 1.0f;
}

원래 TakeDamage 함수는 특정 수치의 피해를 받아 적용하는 함수라서 그런지 반환값의 타입이 float다. 다만 나는 이 함수를 그냥 사망 모션의 출발함수로 쓰일 것이니 반환값은 무의미하다.

 

어쨌든, 만약 이 함수가 실행되면 더이상 움직이지 못하도록 해야한다. 그러기 위해 서버 함수를 호출한다.

 

void AMainGameCharacter::DeathMotionOrder_Implementation()
{
	DeathMotion();
}

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

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

서버에서는 넷멀티캐스트 함수를 실행할 것이다.

 

넷멀티캐스트 함수는 캐릭터의 사망 연출을 위해 물리 시뮬레이션을 적용한다. 또한 사망후 애니메이션을 실행하지 않을 것이니 애니메이션을 Tick을 사용하지 않도록 메시의 bPauseAnims를 true로 만든다. 사망했으면 움직이면 안되니 무브먼트를 비활성화한다.

void AMainGameCharacter::Move(const FInputActionValue& Value)
{
	//죽거나 공격 상태에서는 못움직인다.
	if (bIsDeath || CurrentAttackDelay > 0)
	{
		return;
	}
	
	//입력을 Vector2D로 변환
	FVector2D MovementVector = Value.Get<FVector2D>();

	//이동 적용
	DoMove(MovementVector.X, MovementVector.Y);
}

void AMainGameCharacter::DoJumpStart()
{	
	TakeDamage(0, FDamageEvent(), nullptr, nullptr);
    
	//if (bIsDeath)
	//{
	//	return;
	//}
	
	//Jump();
}

void AMainGameCharacter::Attack()
{
	//사망하거나 이미 공격 상태라면 공격 불가
	if (bIsDeath || CurrentAttackDelay > 0)
	{
		return;
	}

	CurrentAttackDelay = AttackDelayTime;

	//서버에 공격 요청하기
	AttackOrder();
}

어차피 못움직이지만,  그래도 입력이 생길 수 있으니 사망 상태에서는 이동 관련 함수를 실행하지 못하게 막는다. 조금 전에 만든 bool 변수를 이용한다. 공격 함수 또한 사망 상태시 실행을 막도록 동일하게 조치를 취한다.

 

그리고 사망 모션 테스트를 위해 임시로 스페이스바(점프) 함수에 TakeDamage함수를 호출하게 만들고 나머지를 주석처리한다.

 

그런데 문제가 발생했다. 물리 적용을 실행하는 순간 캐릭터가 바닥을 뚫고 아래로 떨어진다.

 

그래서 해결 방법을 찾아봤는데, 메시의 콜리전 프리셋을 Ragdoll로 바꾸면 해결된다고 한다. 원래는 CharacterMesh라는 것이 선택된 상태였다.

 

이 둘을 비교해보니 원인을 알 수 있었는데 '콜리전 활성화됨'이라 적힌 Collision Enabled가 원인이었다. CharacterMesh프리셋에서는 'Query Only (No Physics Collision)'인데 Ragdoll에서는 'Collision Enabled (Query and Physics)'다.

어쨌든 프리셋을 Ragdoll로 바꾸고 테스트 해보니 잘 쓰러진다. 멀티에서도 연동이 잘 된다.

 

void AMainGameCharacter::DoJumpStart()
{
	if (bIsDeath)
	{
		return;
	}
	
	Jump();
}

테스트가 끝났으면 함수 원상복구