반응형

 

 

이제 길찾기를 수행할 몬스터(NPC)를 만들어 주자.

캐릭터 클래스를 상속받는 간단한 NPC 클래스를 생성해서 블루프린트로 만들어 주었다.

 

	SpawnFuncMap.Add(Protocol::CreatureType::CREATURE_TYPE_PLAYER, TFunction<void(bool, Protocol::ObjectInfo)>([=](bool IsMine, Protocol::ObjectInfo ObjectInfo)
		{	
			...
		}));

	SpawnFuncMap.Add(Protocol::CreatureType::CREATURE_TYPE_NPC, TFunction<void(bool, Protocol::ObjectInfo)>([=](bool IsMine, Protocol::ObjectInfo ObjectInfo)
		{
        		...
		}));

 

void UMyGameInstance::HandleSpawn(const Protocol::ObjectInfo& ObjectInfo, bool IsMine)
{
	if (Socket == nullptr || GameServerSession == nullptr)
		return;

	auto* World = GetWorld();
	if (World == nullptr)
		return;

	// 중복 처리 체크
	const uint64 ObjectId = ObjectInfo.object_id();

	if (Players.Find(ObjectId) != nullptr)
		return;

	SpawnByCreatureType(ObjectInfo.creature_type(), IsMine, ObjectInfo);
}


void UMyGameInstance::SpawnByCreatureType(const Protocol::CreatureType& type, bool isMine, const Protocol::ObjectInfo& ObjectInfo)
{
	if (SpawnFuncMap.Contains(type))
		SpawnFuncMap[type](isMine, ObjectInfo);
}

 

그리고 게임인스턴스에서 스폰하는 부분의 코드를 수정해주었다.

 


 

void Room::CreateNPC()
{
	cout << "npc 생성" << endl;
	auto npc = ObjectsUtils::CreateNPC();
	AddObject(static_pointer_cast<Object>(npc), _objects_npc);

	npc->posInfo->set_x(0.f);
	npc->posInfo->set_y(0.f);
	npc->posInfo->set_z(0.f);
	npc->posInfo->set_yaw(45.f);
	npc->SetVector3D(npc->posInfo);
}

 

서버쪽에서도 Object를 상속받는 간단한 Npc 클래스를 생성해서 생성해주었다.

그리고 서버가 띄워질 때 Npc를 생성하도록 함수를 호출해주었다.

 

for (auto n : _objects_npc)
{
    Protocol::S_SPAWN spawnPkt;

    Protocol::ObjectInfo* playerInfo = spawnPkt.add_players();
    playerInfo->set_creature_type(Protocol::CreatureType::CREATURE_TYPE_NPC);

    Protocol::PosInfo* posInfo = playerInfo->mutable_pos_info();
    posInfo->CopyFrom(*n.second->posInfo);

    SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(spawnPkt);
    if (auto session = player->session.lock())
        session->Send(sendBuffer);
}

 

그리고 클라이언트가 접속 했을 때, 생성된 npc를 스폰하는 패킷을 접속한 클라이언트로 보내주었다.

 

 

 

일단 소환은 정상적으로 작동한다.


이제 이 몬스터(NPC)를 이용해서 길찾기를 수행해야 한다.

나는 간단하게 만들기 위해 서버에 만들어 두었던, 정해진 틱마다 업데이트 하는 함수로 랜덤 좌표를 뽑아서

현재 위치에서 랜덤 좌표까지 길찾기를 수행하도록 해줄 것이다.

 

void Room::UpdateTick()
{
	cout << "Update Room" << endl;

	for (auto n : _objects_npc)
	{
		auto npc = dynamic_pointer_cast<NPC>(n.second);
		auto sendBuffer = npc->DoPathFinding(npc);
		Broadcast(sendBuffer);
	}

	DoTimer(10000, &Room::UpdateTick);
}

 

SendBufferRef NPC::DoPathFinding(NPCRef npc)
{	
	Protocol::S_PATHFINDING pkt;
	
	float x = GPathFinder->ReturnApproximationToInt(npc->vector3D.x);
	float y = GPathFinder->ReturnApproximationToInt(npc->vector3D.y);
	float z = GPathFinder->ReturnApproximationToInt(npc->vector3D.z);

	//현재 npc의 위치에서 가장 가까운 노드 구해서 시작점에 넣어주기.
	Vector start = GPathFinder->CreateVec(x, y, z);

	//랜덤 위치
	Vector goal = GPathFinder->ReturnRandomPos();

	auto coroutine = CoroutineJob::CoroutineFunc(GPathFinder, start, goal);

	while (true)
	{
		if (GPathFinder->IsRead)
		{
			coroutine.resume();
			break;
		}
	}

	for (auto p : GPathFinder->Path)
	{
		Protocol::PosInfo* path = pkt.add_info();
		path->set_x(p.x);
		path->set_y(p.y);
		path->set_z(p.z);
		cout << "(" << p.x << "," << p.y << "," << p.z << ") ->";
	}
	cout << endl;

	//경로를 패킷으로 만들어서 리턴


	SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(pkt);
	return sendBuffer;
}

 

10초 주기로 랜덤 위치를 구한 다음,

현재 위치에서 랜덤 위치까지의 경로를 구해서 패킷으로 만들어준다.

그리고 그 패킷을 모든 클라이언트에게 브로드 캐스팅 해주도록 하였다.

 

void UMyGameInstance::HandlePathFinding(const Protocol::S_PATHFINDING& pathPkt)
{
	UE_LOG(LogTemp, Warning, TEXT("HandlePathFinding"));

	for (auto path : pathPkt.info())
	{
		UE_LOG(LogTemp, Warning, TEXT("(%f,%f,%f)"), path.x(), path.y(), path.z());
	}
}

 

클라이언트에서 경로를 받아주는 함수.

 

 

 

10초 단위로 랜덤한 위치로 경로를 잘 찾고 클라이언트도 그 경로를 클라이언트도 잘 받고있는걸 확인할 수 있다.


이제 진짜진짜 모든 준비가 끝났으니 npc 액터를 저 좌표로 이동시켜주기만 하면 된다.

 

이 부분에서 많은 실수와 삽질이 있었다. ㅠㅠ

 

처음에는 클라이언트의 NPC 클래스에 경로를 저장할 배열을 생성해주고,

서버로 부터 경로를 받아서 저장해주었다.

그리고 해당 경로로 이동하면서 경로에 도착할 때 마다 인덱스를 1개씩 늘려서 다음 경로로 이동하게 하였다.

그런데 당연히 위와 같이 하면 각 클라이언트에서 NPC는 따로따로 이동할 것이다.

 

또 굳이 경로를 클라이언트로 전송을 해야하는 건가? 라는 생각이 들었다.

그래서 이를 해결하기 위해 경로에 도착했는지 확인하는 부분도 서버에서 처리하도록 수정하고,

서버에서 인덱스를 통해 목표 경로를 지정해서 클라이언트로 보내주도록 하였다.

 

이 간단한 해결방법을 왜 생각하지 못해서 며칠을 삽질한지 모르겠다.

 

수정한 코드는 다음과 같다.

서버

//bool Room::EnterRoom(ObjectRef object, bool randPos) 함수 중

    for (auto n : _objects_npc)
    {
        Protocol::S_SPAWN spawnPkt;
        Protocol::S_NPCMOVE npcMovePkt;

        Protocol::ObjectInfo* npcInfo = spawnPkt.add_players();
        npcInfo->set_creature_type(Protocol::CreatureType::CREATURE_TYPE_NPC);
        npcInfo->set_object_id(n.second->objectInfo->object_id());

        Protocol::PosInfo* posInfo = npcInfo->mutable_pos_info();
        posInfo->CopyFrom(*n.second->posInfo);
        cout << "npc 아이디 : " << posInfo->object_id() << endl;

        SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(spawnPkt);
        if (auto session = player->session.lock())
        {
            session->Send(sendBuffer);

            if (auto npc = dynamic_pointer_cast<NPC>(n.second))
            {
                {
                    npcMovePkt.set_canrendering(true);
                    Protocol::PosInfo* info = npcMovePkt.mutable_info();
                    //info->CopyFrom(pkt.info());

                    auto dest = npc->GetDestByIdx(npc->idx);
                    info->set_x(dest.x);
                    info->set_y(dest.y);
                    info->set_z(dest.z);
                    info->set_object_id(npc->objectInfo->object_id());
                    info->set_state(Protocol::MoveState::MOVE_STATE_RUN);
                }

                SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(npcMovePkt);
                session->Send(sendBuffer);
            }
        }
    }

 

클라이언트가 접속했을 때 서버의 npc를 소환해줌과 동시에 npc의 위치와 현재 목표 경로등의 정보도 함께 보내줌.

 

void Room::HandleNpcMove(Protocol::C_NPCMOVE pkt)
{
	const uint64 objectId = pkt.info().object_id();

	NPCRef npc = dynamic_pointer_cast<NPC>(_objects_npc[objectId]);
	auto info = npc->objectInfo->mutable_pos_info();
	info->CopyFrom(pkt.info());
	npc->posInfo->CopyFrom(pkt.info());
	npc->SetVector3D(npc->posInfo);

	npc->CheckIdx();

	if (!npc->isArrvied)
		npc->posInfo->set_state(Protocol::MoveState::MOVE_STATE_RUN);

	Protocol::S_NPCMOVE npcMovePkt;
	npcMovePkt.set_canrendering(true);
	Protocol::PosInfo* sendInfo = npcMovePkt.mutable_info();
	//info->CopyFrom(pkt.info());

	auto dest = npc->GetDestByIdx(npc->idx);
	sendInfo->set_x(dest.x);
	sendInfo->set_y(dest.y);
	sendInfo->set_z(dest.z);
	sendInfo->set_object_id(npc->objectInfo->object_id());
	sendInfo->set_state(npc->posInfo->state());

	SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(npcMovePkt);
	Broadcast(sendBuffer);
}

 

클라이언트의 npc 위치를 받아와서 서버의 npc 클래스에 정보를 저장해준다.

그리고 해당 정보로 CheckIdx() 호출.

 

void NPC::CheckIdx()
{
	if (!DestVec.empty() && idx < size)
	{
		curDest = DestVec[idx];
		auto dist = vector3D.DistanceTo2D(curDest);
		//cout << "거리 : " << dist << endl;
		//cout << "경로 idx : " << idx << endl;
		//cout << "경로 갯수 : " << size << endl;

		if (dist < 100.f)
		{
			idx++;

			if (idx == size)
				idx--;

			posInfo->set_state(Protocol::MoveState::MOVE_STATE_RUN);
		}

		if (vector3D.DistanceTo2D(lastDest) < 100.f)
		{
			isArrvied = true;
			posInfo->set_state(Protocol::MoveState::MOVE_STATE_IDLE);
		}
	}
}

 

npc의 위치와 목표 경로의 위치를 비교해서 도착했다면, 다음 경로로 인덱스를 증가시켜준다.

 

void NPC::DoPathFinding(NPCRef npc)
{	
	DestVec.clear();
	isArrvied = false;

	Protocol::S_PATHFINDING pkt;
	
	float x = GPathFinder->ReturnApproximationToInt(npc->vector3D.x);
	float y = GPathFinder->ReturnApproximationToInt(npc->vector3D.y);
	float z = 0;

	//현재 npc의 위치에서 가장 가까운 노드 구해서 시작점에 넣어주기.
	Vector start = GPathFinder->CreateVec(x, y, z);

	//랜덤 위치
	Vector goal = GPathFinder->ReturnRandomPos();

	auto coroutine = CoroutineJob::CoroutineFunc(GPathFinder, start, goal);

	while (true)
	{
		if (GPathFinder->IsRead)
		{
			coroutine.resume();
			break;
		}
	}

	for (auto p : GPathFinder->Path)
	{
		DestVec.push_back(p);
		cout << "(" << p.x << "," << p.y << "," << p.z << ") ->";
	}

	idx = 1;
	size = DestVec.size();
	lastDest = DestVec[DestVec.size()-1];
	posInfo->set_state(Protocol::MoveState::MOVE_STATE_RUN);

	cout << endl;
}

 

경로 찾기에서 패킷을 만들어 주는 부분을 제거.

 

 

 

클라이언트

void UMyGameInstance::HandleNpcMove(const Protocol::S_NPCMOVE& NpcMovePkt)
{
	if (Socket == nullptr || GameServerSession == nullptr)
		return;

	auto* World = GetWorld();
	if (World == nullptr)
		return;

	const uint64 ObjectId = NpcMovePkt.info().object_id();

	ANPC** FindActor = Npcs.Find(ObjectId);
	if (FindActor == nullptr)
		return;

	ANPC* Npc = (*FindActor);

	const Protocol::PosInfo& Info = NpcMovePkt.info();
	UE_LOG(LogTemp, Warning, TEXT("DestInfo : (%f,%f,%f)"), Info.x(), Info.y(), Info.z());

	Npc->SetDestInfo(Info);
}

 

void ANPC::SetDestInfo(const Protocol::PosInfo& Info)
{
	if (NPCInfo->object_id() != 0)
	{
		assert(NPCInfo->object_id() == Info.object_id());
	}
	DestInfo->CopyFrom(Info);
	curDest = { DestInfo->x(),DestInfo->y() ,0 };

	if (curDest == FVector::Zero())
		return;

	DestInfo->CopyFrom(Info);

	if (NPCInfo->state() != Info.state())
		NPCInfo->set_state(Info.state());

	NPCRot = UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), curDest);
}

 

서버로 부터 받아온 목표 경로를 저장해준다.

 

void ANPC::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	{
		FVector Location = GetActorLocation();
		NPCInfo->set_x(Location.X);
		NPCInfo->set_y(Location.Y);
		NPCInfo->set_z(Location.Z);
		NPCInfo->set_yaw(GetControlRotation().Yaw);
	}

	const Protocol::MoveState State = DestInfo->state();

	if (State == Protocol::MOVE_STATE_RUN)
	{
		NPCInfo->set_state(Protocol::MOVE_STATE_RUN);
		UE_LOG(LogTemp, Warning, TEXT("State == MOVE_STATE_RUN"));

		//if (FVector::Distance(GetActorLocation(), curDest) > 300)
		//	SetActorLocation(curDest);

		SetActorRotation(FRotator(0, NPCRot.Yaw, 0));
		AddMovementInput(GetActorForwardVector());
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("State == NOT_RUN"));
	}

	if (FVector::Dist(GetActorLocation(), curDest) < 200.f)
	{
		//npc 전용 이동 패킷
		Protocol::C_NPCMOVE pkt;
		{
			Protocol::PosInfo* Info = pkt.mutable_info();
			Info->CopyFrom(*NPCInfo);
			Info->set_yaw(NPCRot.Yaw);
			Info->set_state(NPCInfo->state());
			Info->set_object_id(NPCInfo->object_id());
		}

		SEND_PACKET(pkt);
	}
}

 

목표 경로 방향으로 이동


 

이렇게 해주면 모든 클라이언트 화면에서 동일한 NPC(AI) 모습을 확인할 수 있다.

A가 먼저 접속하고 나중에 B가 접속하더라도 서버에 NPC의 정보가 저장되어 있기 때문에, 동일한 위치에서 소환이 되고,

동일한 경로로 경로찾기를 수행한다.

 

여기에 간단한 fsm을 추가하거나 언리얼의 비헤이비어 트리를 이용하면 진짜 몬스터나 npc와 같은 기능들도 만들 수 있을 것이다.

 

 



 

 

반응형