언리얼엔진 학습 및 과제

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

park94128 2025. 12. 24. 23:49

● 방해꾼 등장

아직 만드는 중이긴 한데, 아무래도 접속한 클라이언트의 캐릭터만 있으니 심심해보인다. 그래서 클라이언트가 아닌 AI가 담당하는 캐릭터인 NPC를 추가할 것이다. 이 NPC는 플레이어의 캐릭터와 동일한 모습이다.

이런 생각을 하게된 이유는 이 게임의 룰이 신기해서 내 손으로 구현해보고 싶은 마음에서 출발했다. 이 게임은 NPC사이에 여러 플레이어가 섞인 상태에서 다른 플레이어에게 들키지 않아야 하는 게임이다. 그리고 NPC인척 하다가 다른 플레이어를 찾아 처치하여 최종 생존자가 되어야 하는 규칙을 가지고 있다.

 

나는 이 게임의 특징을 응용한 경찰과 도둑 게임을 만들 생각이다. 그냥 일반적인 술래잡기처럼 만들면 재미가 없을것 같아서 이렇게 플레이어와 똑같은 모습을 한 NPC를 섞기로 했다.

 

UCLASS()
class HIDDENTHIEF_API AMainGameAIController : public AAIController
{
	GENERATED_BODY()

public:
	virtual void OnPossess(APawn* InPawn) override;
};

일단 AI 컨트롤러가 있어야 AI 캐릭터가 움직일 수 있다. 기능을 만들기 전에 클래스만 미리 만든다.

 

UCLASS()
class HIDDENTHIEF_API AMainGameAIChar : public ACharacter
{
	GENERATED_BODY()

public:
	AMainGameAIChar();

protected:
	virtual void BeginPlay() override;

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

AI용 클래스를 만든다. 플레이어가 조종하는 캐릭터 코드 중 당장에 필요한 것만 가져왔다.

 

AMainGameAIChar::AMainGameAIChar()
{
	PrimaryActorTick.bCanEverTick = false;

	//캡슐 컴포넌트 기본 크기
	GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
	
	//캐릭터 이동에 대한 기본값
	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
	GetCharacterMovement()->JumpZVelocity = 500.f;
	GetCharacterMovement()->AirControl = 0.35f;
	GetCharacterMovement()->MaxWalkSpeed = 500.f;
	GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
	GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
	GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;

	//기타 기본값 세팅
	bIsDeath = false;
}

생성자에는 기본적인 캡슐 컴포넌트, 무브먼트 설정만 지정한다.

 

코드 기반으로 캐릭터 블루프린트 하나 만든다.

처음에 제공된 캐릭터 블루프린트를 참고하여 스켈레탈 메시를 지정한다. 여기서 AI 캐릭터는 공격 등 다른 행동을 하지 않고 돌아다니기만 할 것이다. 그래서 내가 만든 애니메이션은 안 쓸 생각이며 처음에 제공된 애니메이션을 사용한다.

 

디테일에서 플레이어 자동 빙의는 못하게 막고 AI 자동 빙의는 Spawned(게임 시작후 생성시 AI 컨트롤러가 자동빙의)로 지정한다. AI 컨트롤러 클래스는 미리 생성했던 AI 컨트롤러 클래스를 지정한다.

 

리플리케이션과 관련해서 건드려야 할 것이 있나 살펴봤는데 기존의 캐릭터와 비교했을때 딱히 건드릴건 없어보인다.

 

● NPC 생성하기

NPC는 AI컨트롤러가 담당하는 캐릭터라서 서버에서 생성해야 한다. 그래서 NPC를 생성하는 액터도 서버에서만 작동하게 만들 것이다.

 

코딩하기 전에 우선 맵에 내비메시 바운드 볼륨을 추가한다. 맵의 바닥만 포함하고 벽을 넘어가지 않게, 그리고 네 귀퉁이의 땅 위쪽을 포함하지 않도록 위치와 스케일을 조절한다. 이는 NPC가 스스로 이동할 때에 반드시 있어야 한다. 지금은 NPC 소환용으로 사용할것이다.

배치한김에, 맵의 네 귀퉁이에 있는 바닥은 내비게이션에 영향을 주는지 확인한다. 저 위치에 NPC를 생성하지 않고 이동시 저 지형을 피하게 만들기 위해서다. 네비메시 바운드 볼륨을 맵에 배치시 저 지형의 위쪽이 포함하지 않게 하는 이유이기도 하다.

네비게이션 표시를 해서 의도한 대로 잘 적용됐는지 확인

 

class AMainGameAIChar;
class UNavigationSystemV1;

UCLASS()
class HIDDENTHIEF_API AAICharSpawner : public AActor
{
	GENERATED_BODY()
	
public:	
	AAICharSpawner();

protected:
	virtual void BeginPlay() override;

private:
	//적 생성하기
	void SpawnCharacters();

	//네비게이션 시스템
	UPROPERTY()
	UNavigationSystemV1* NavigationSystem = nullptr;
	
	//AI 캐릭터 클래스
	UPROPERTY(EditAnywhere, Category = "AI Character", meta = (AllowPrivateAccess = true))
	TSubclassOf<AMainGameAIChar> AICharClass;

	//맵에 생성할 AI 캐릭터 수
	UPROPERTY(EditAnywhere, Category = "AI Character", meta = (AllowPrivateAccess = true))
	int32 MaxCharCount;

	//캐릭터 생성시 높이 보정 값
	const FVector ADD_LOCATION_VALUE = FVector(0, 0, 90);
};

액터 생성시 미리 선언되는 함수 중 BeginPlay와 생성자를 제외한 나머지는 필요없으니 지운다.

NPC 생성용 함수와 함께 네이게이션 시스템, 캐릭터 클래스, 생성할 캐릭터 수에 대한 변수도 선언한다. (0, 0, 90)인 FVector도 있는데 이는 후술한다.

 

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

	//클라이언트 사용 불가, 서버에서만 사용
	if (GetNetMode() == NM_Client)
	{
		return;
	}

	//네비게이션 시스템
	NavigationSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());
	if (!IsValid(NavigationSystem))
	{
		UE_LOG(LogTemp, Error, TEXT("%s : NavigationSystem이 유효하지 않습니다."), *GetNameSafe(this));
		return;
	}

	//NPC 생성
	SpawnCharacters();
}

BeginPlay내부에서, 이 액터가 클라이언트에서 쓰지 못하도록 막는다. 이는 GetNetMode함수를 호출하여 반환값이 NM_Client일경우 리턴하는 것으로 구현했다.

 

GetNavigationSystem함수를 사용하여 네이게이션 시스템도 가져온다. 맵에 배치한 네비매시 바운드 볼륨을 이용할 것이다.

 

클라이언트가 아니고 네비게이션 시스템이 정상이라면 NPC를 생성할 것이다.

 

void AAICharSpawner::SpawnCharacters()
{
	int CharCount = 0;
	FNavLocation NavLocation;
	while (CharCount < MaxCharCount)
	{
		//네비 시스템 내의 무작위 위치 정하기
		bool Spawned = NavigationSystem->GetRandomPoint(NavLocation);
		if (!Spawned)
		{
			continue;
		}
		FVector SpawnPoint = NavLocation.Location + ADD_LOCATION_VALUE;

		//정해진 위치에서 NPC 생성
		FActorSpawnParameters SpawnParam = FActorSpawnParameters();
		SpawnParam.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
		GetWorld()->SpawnActor<AMainGameAIChar>(AICharClass, SpawnPoint, FRotator::ZeroRotator, SpawnParam);

		++CharCount;
	}
}

네비게이션 시스템에는 GetRandomPoint라는 함수가 있는데 맵에 배치한 네베 메시 바운드 볼륨이 감싸고 있는 바닥에서 무작위 위치를 지정할 것이다. 이 위치에 NPC를 생성한다. 이 과정을 변수로 지정한 수만큼 반복한다.

 

이 코드는 2가지 문제점을 수정한 것인데 다음과 같다.

- 무작위 위치에 (0, 0, 90)을 추가하는 이유 : 무작위 위치에 정확하게 맞춰서 소환하면 캐릭터가 바닥에 끼는 문제점이 있다. 캐릭터의 영점이 발 아래가 아니라 몸통 중간에 있기 때문이다. 그래서 캐릭터의 절반 높이만큼 더해줬다. 여기서는 그 값이 90이다.

- NPC 생성시 우연히 동일한 위치에서 생성하려고 하면 생성에 실패하는 문제를 발견했다. 이를 방지하려면 FActorSpawnParameters를 사용해야 한다. 위 코드의 경우, 만약 지정한 위치에서 생성에 실패할 경우 생성에 성공하는 근처 위치에서 생성한다.

여기에 더하여 Build.cs파일에서 NavigationSystem 모듈을 추가해야 한다. 안그러면 컴파일시 에러가 발생할 것이다.

이 액터는 블루프린트를 거치지 않고 바로 맵에 배치했다. 배치후 AI 캐릭터와 최대 생성수를 지정했다.

 

생성 성공, 테스트를 위해 플레이어수를 1로 만들어서 실행한 다음에 다른 플레이어를 추가해봤는데 나중에 접속한 클라이언트 화면에서도 동일하게 잘 보였다. 맵 대비 NPC 수가 적어보이지만 아직 만드는 중이니 일단 놔둔다.