언리얼엔진 학습 및 과제

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

park94128 2025. 12. 23. 23:58

● 폼으로만 공격하지말고 진짜 공격을 하자

지금은 아무리 공격을 해봤자 공격 애니메이션만 실행할 뿐 공격 판정이 발생하지 않는다. 그래서 이제 진짜 공격을 하려고 한다.

 

잠깐 하나 생각을 해보자면, 서버-클라이언트 구조로 된 언리얼 엔진 게임에서는 각종 액터가 서버에 있으며 클라이언트에 서버에 있는 액터의 복제본을 가지고 있다. 그리고 서버에 있는 액터의 움직임을 클라이언트의 복사본이 따라하는 구조다.

 

그럼 여기서 공격 판정은 어디서 만드는 것이 올바를까? 바로 원본 액터를 가지고 있는 서버에서 공격 판정을 만들어야 한다. 서버에서 공격 판정을 만들고, 이 공격 판정을 이용해 다른 액터를 사망하게 만들면 된다. 클라이언트에서 보이는 복사본에서는 그저 공격 모션, 사망 모션을 실행하기만 하면 그만이다.

 

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

	//서버 전용 공격 함수
	void AttackActionByServer();

	//공격 모션 실행
	UFUNCTION(NetMulticast, Reliable)
	void AttackMotion();
    
	//공격 거리
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Character State", meta = (AllowPrivateAccess = true))
	float AttackRange;
    
	//공격 판정용 캡슐의 반경
	const float CapsuleRadius = 20.0f;
	//공격 판정용 캡슐의 높이
	const float CapsuleHalfHeight = 50.0f;

RPC함수 2개 말고 추가로 공격 판정을 만드는 함수를 선언한다. 이 함수는 서버에서 서버에 있는 액터의 함수를 호출할 것이므로 RPC함수로 만들지 않는다.

 

그리고 공격 거리를 블루프린트에서 받도록 한다. 공격 모션시 발생하는 공격 판정의 길이라고 생각하면 된다.

공격 범위의 모습까지 블루프린트로 받을까 생각했는데 관뒀다. 대신 이전에 개인적으로 학습할 때 사용했던 수치를 가져왔다.

 

void AMainGameCharacter::AttackOrder_Implementation()
{
	//공격 실행
	AttackActionByServer();

	//공격 모션
	AttackMotion();
}

서버 실행 함수에서 금방 선언한 함수를 호출한다.

 

void AMainGameCharacter::AttackActionByServer()
{
	//캐릭터 앞쪽에 트레이스 진행
	FHitResult Hit;
	FVector StartPoint = GetActorLocation();
	FVector EndPoint = StartPoint + GetActorForwardVector() * AttackRange;

	FCollisionObjectQueryParams ObjectQueryParams;
	ObjectQueryParams.AddObjectTypesToQuery(ECollisionChannel::ECC_Pawn);

	//자신은 무시
	FCollisionQueryParams QueryParams;
	QueryParams.AddIgnoredActor(this);

	//트레이스 시작
	bool TraceResult = GetWorld()->SweepSingleByObjectType(
		Hit, StartPoint, EndPoint, FQuat::Identity, ObjectQueryParams, FCollisionShape::MakeCapsule(CapsuleRadius, CapsuleHalfHeight), QueryParams);

	if (!TraceResult)
	{
		return;
	}

	AActor* ResultActor = Hit.GetActor();
	if (!IsValid(ResultActor))
	{
		return;
	}

	APawn* ResultPawn = Cast<APawn>(ResultActor);
	if (!IsValid(ResultPawn))
	{
		return;
	}

	ResultPawn->TakeDamage(0, FDamageEvent(), nullptr, nullptr);
}

어떤 액터가 다른 액터를 감지하기 위해 쓰는 방법은 여러가지 있다. 여기서는 공격 범위를 지정하고 그 범위내 액터 하나를 감지하는 방법을 사용했다. SweepSingleByObjectType 함수는 지정한 범위에 조건에 맞는 액터 하나를 감지하는 함수다. 함수 이름대로, 오브젝트 타입이 조건이다. 이 캐릭터의 캡슐 컴포넌트가 사용하는 콜리전 프리셋이 Pawn인데 여기서 사용하는 오브젝트 타입이 Pawn이니 코드에서도 감지 조건을 Pawn으로 했다.

이와 함께 FCollisionQueryParams을 이용하여 액터 자신은 감지할 조건에서 제외했다.

 

액터 감지시 감지한 액터를 Pawn으로 변환하여 TakeDamage함수를 실행하게 만들었다. TakeDamage함수는 캐릭터가 사망하는 기능을 가진 함수로 오버라이드 했으니 이 방법이면 될 것이다.

 

블루프린트에서 공격 거리를 적당히 지정해주고 테스트 해보니 공격 당한 캐릭터가 쓰러짐을 확인했다.