이전에는 캐릭터 선택창과 새 캐릭터 생성을 구현했다.

이번에는 캐릭터 선택창에서 특정 캐릭터를 선택하면 필드에 접속하고, 오브젝트를 스폰하도록 만들어보자.


 

우측 이미지가 Basic Level

캐릭터 선택창에서 캐릭터 포트레잇을 클릭하면 메인 메뉴 맵에서(현재는 TestLevel) 다른 맵(Basic Level)으로 이동하고, 오브젝트를 스폰시키도록 만들어보자. (일단은 아무 타원형 오브젝트)

void Handle_GS_ENTER_ROOM(const PacketSessionRef& session, const Protocol::GS_ENTER_ROOM& pkt)
{
    UE_LOG(LogTemp, Warning, TEXT("Handle_GS_ENTER_ROOM"));
    if (UClientGameInstance* GI = Cast<UClientGameInstance>(GWorld->GetGameInstance()))
    {
       if (pkt.success())
       {
          UGameplayStatics::OpenLevel(GI->GetWorld(), TEXT("BasicLevel"));
          
          //HandleSpawn(pkt.character_info());
          GI->PendingPlayerInfo.CopyFrom(pkt.character_info());
          GI->bHasPendingSpawn = true;
       }
       else
       {
          
       }
    }
}

처음에는 주석친 부분처럼 맵 이동 이후 바로 spawn하도록 만들었는데, 맵 이동 후에 봐도 스폰되지 않았다. 알고보니 언리얼에서 OpenLevel은 비동기로 작동하기 때문에 호출 이후 오브젝트가 TestLevel에 스폰되고, 그 뒤에 BasicLevel로 이동하기 때문에 생기는 문제였다. (추후에 확인해보니 이 문제가 아니었다. 아래 빨간글씨 부분 확인)

그래서 GameInstance에 소환할 캐릭터정보와 PendingSpawn값을 저장해놨다가 맵을 이동한 다음 BeginPlay에서 스폰되도록 설정했다.

저장한 데이터를 토대로 BP_TestPlayer를 스폰시킨다.

 

가 아니라 BasicLevel 에서 EventTick에서 HandleRecvPacket을 호출하고있지 않아서 Spawn을 안시켜서 생긴 문제였다...

TestLevel에서처럼 HandleRecvPackets를 매 틱마다 호출하도록 만들어주자. 나를 스폰하는 문제는 앞선 방법으로 해결되긴 했지만 어차피 해줘야한다.


입장을 다른플레이어에게 알리기, 입장 시 다른 플레이어 스폰시키기

여러 클라이언트를 켜보면 지금은 플레이어를 스폰시키는데, 다른 플레이어의 존재는 확인할 수 없다.

Room의 Enter를 아래와 같이 수정해서 새 플레이어의 접속을 Room에 접속한 모든 유저에게 알리고, 이미 접속해있는 유저들의 정보도 새로 접속한 유저에게 전달해준다.

 

게임서버의 Room 클래스

void Room::Enter(PlayerRef player, int32 characterIndex, int32 roomId)
{
	//TODO DB쿼리때문에 렉 심하면 비동기로 바꿔야할 듯
	/*...*/

	//입장
	player->_info.CopyFrom(characterInfo);
	_players.insert(make_pair(player->_info.id(), player));
	player->_room.store(static_pointer_cast<Room>(shared_from_this()));

	//입장을 유저에게 알림
	response.mutable_character_info()->CopyFrom(characterInfo);
	response.set_success(true);
	gameSession->Send(ClientPacketHandler::MakeSendBuffer(response));

	//해당 플레이어의 입장을 다른 플레이어들에게 알림
	for (auto& data : _players)
	{
		int32 userId = data.first;
		PlayerRef& otherPlayer = data.second;
		if (userId == player->_info.id())
			continue;
		Protocol::GS_SPAWN pkt;
		Protocol::PlayerInfo* otherPlayerInfo = pkt.add_players();
		otherPlayerInfo->CopyFrom(characterInfo);

		GameSessionRef otherSession = otherPlayer->_owner.lock();
		if (otherSession == nullptr)
			continue;
		otherSession->Send(ClientPacketHandler::MakeSendBuffer(pkt));
	}

	//해당 플레이어의 클라에 다른 플레이어의 존재를 알림
	{
		Protocol::GS_SPAWN pkt;
		for (auto& data : _players)
		{
			PlayerRef& otherPlayer = data.second;
			if (otherPlayer->_info.id() == player->_info.id())
				continue;
			Protocol::PlayerInfo* otherPlayerInfo = pkt.add_players();
			otherPlayerInfo->CopyFrom(otherPlayer->_info);
		}
		gameSession->Send(ClientPacketHandler::MakeSendBuffer(pkt));
	}
}

 

 

Spawn 패킷을 받으면 캐릭터 블루프린트를 소환시킨다.

void Handle_GS_SPAWN(const PacketSessionRef& session, const Protocol::GS_SPAWN& pkt)
{
	UE_LOG(LogTemp, Warning, TEXT("Handle_GS_SPAWN"));

	if (UClientGameInstance* GI = Cast<UClientGameInstance>(GWorld->GetGameInstance()))
	{
		for (auto& Player : pkt.players())
			GI->HandleSpawn(Player);
	}
	else
	{
	}
}

void UClientGameInstance::HandleSpawn(const Protocol::PlayerInfo& PlayerInfo)
{
    if (GameServerSession == nullptr)
       return;

    auto* World = GetWorld();
    if (World == nullptr)
       return;
    
    FVector SpawnLocation(PlayerInfo.pos().x(), PlayerInfo.pos().y(), PlayerInfo.pos().z());
    ACharacter* SpawnedPawn = World->SpawnActor<ACharacter>(PlayerClass, SpawnLocation, FRotator::ZeroRotator);

    Players.Add(PlayerInfo.id(), SpawnedPawn);
}

(캐릭터가 바뀌었는데 새 캐릭터 클래스를 만들고 언리얼 무료 에셋 ParagonPhase을 사용했다)

이렇게 하면 여러 클라이언트로 접속하면 모든 클라에서 접속한 숫자만큼 캐릭터들이 보인다.


디스폰

Q를 눌러서 메인메뉴로 돌아가도록 만들어보자.

 

클라이언트

void UClientGameInstance::LeaveRoom()
{
    if (GameServerSession == nullptr)
       return;
    
    Protocol::GC_LEAVE_ROOM LeaveRoomPkt;
    GameServerSession->SendPacket(ServerPacketHandler::MakeSendBuffer(LeaveRoomPkt));
}

BasicLevel(게임플레이 맵)에서 Q를 누르면 LeaveRoom 함수를 호출하도록 만든다. 이벤트는 keyboard q 를 검색하면 나온다.

 

게임서버

나가는 당사자는 LeaveRoom 패킷을, 접속한 다른 플레이어들에게는 Despawn 패킷을 보내준다.

void Handle_GC_LEAVE_ROOM(const PacketSessionRef& session, const Protocol::GC_LEAVE_ROOM& pkt)
{
	Utils::LockPrint("Handle_GC_LEAVE_ROOM");
	GameSessionRef gameSession = static_pointer_cast<GameSession>(session);

	PlayerRef player = gameSession->_player;
	if (player == nullptr)
		return;
	RoomRef room = player->_room.load().lock();
	if (room == nullptr)
		return;
	room->DoAsync(&Room::Leave, player);
}

void Room::Leave(PlayerRef player)
{
	if (player == nullptr)
		return;
	const uint64 objectId = player->_info.id();

	_players.erase(objectId);
	player->_room.load().reset();


	//퇴장 사실을 퇴장하는 플레이어에게 알림
	{
		Protocol::GS_LEAVE_ROOM pkt;

		if (GameSessionRef gameSession = player->_owner.lock())
			gameSession->Send(ClientPacketHandler::MakeSendBuffer(pkt));
	}

	//퇴장 사실을 다른 플레이어들에게 알림
	{
		Protocol::GS_DESPAWN pkt;
		pkt.add_object_ids(objectId);

		Broadcast(ClientPacketHandler::MakeSendBuffer(pkt));
	}
}

 

클라이언트

LeaveRoom 패킷을 받으면 TestLevel(로비)로 이동시킨다.

void Handle_GS_LEAVE_ROOM(const PacketSessionRef& session, const Protocol::GS_LEAVE_ROOM& pkt)
{
    UE_LOG(LogTemp, Warning, TEXT("Handle_GS_LEAVE_ROOM"));

    if (UClientGameInstance* GI = Cast<UClientGameInstance>(GWorld->GetGameInstance()))
    {
       UGameplayStatics::OpenLevel(GI->GetWorld(), TEXT("TestLevel"));
    }
}

TestLevel 블루프린트에서 게임서버에 접속중이라면(로그인 상태) LogonPage 위젯을 띄우도록, 접속중이 아니라면 (로그인 전) 메인페이지를 띄우도록 해서 LeaveRoom을 했을 때 로그인을 또 할 필요 없게 했다.

 

다른 클라이언트에서는 디스폰 패킷에 포함된 id를 사용해 플레이어 목록에서 해당 플레이어 캐릭터를 DestroyActor 한다.

void Handle_GS_DESPAWN(const PacketSessionRef& session, const Protocol::GS_DESPAWN& pkt)
{
	UE_LOG(LogTemp, Warning, TEXT("Handle_GS_DESPAWN"));

	if (UClientGameInstance* GI = Cast<UClientGameInstance>(GWorld->GetGameInstance()))
	{
		GI->HandleDespawn(pkt);
	}
}

void UClientGameInstance::HandleDespawn(const Protocol::GS_DESPAWN& DespawnPkt)
{
    for (auto& ObjectId : DespawnPkt.object_ids())
    {
       HandleDespawn(ObjectId);
    }
}

void UClientGameInstance::HandleDespawn(uint64 ObjectId)
{
    if (GameServerSession == nullptr)
       return;
    
    auto* World = GetWorld();
    if (World == nullptr)
       return;
    
    AActor** FindActor = Players.Find(ObjectId);
    if (FindActor == nullptr)
       return;
    
    World->DestroyActor(*FindActor);
}

 

 

q를 누르면

UClientGameInstance::LeaveRoom() 서버로 LeaveRoom 패킷 전송

서버에서 퇴장 처리 후 나간 당사자는 HandleLeaveRoom함수로 메인메뉴 맵으로 이동한다.

Room에 접속해있는 유저들은 HandleDespawn으로 해당 유저를 디스폰시킨다.

 


현재까지의 git 버전

언리얼 클라이언트 ParagonPhase 에셋은 용량 문제로 git ignore했다.

 

Feat: 스폰, 디스폰 · Dodontak/Project_Island_Client@3690b49

Room 접속 시 이미 접속한 다른 유저를 스폰하게 수정. Q를 눌러 Room에서 퇴장해 메인 메뉴로 돌아갈 수 있도록 수정. ParagonPhase 에셋 추가함. (용량 문제로 git ignore)

github.com

 

게임 서버

 

Feat: 스폰, 디스폰 · Dodontak/Project_Island_GameServer@2cfbcad

- 다른 유저의 Room 접속 처리 - 디스폰과 LeaveRoom 처리

github.com

 

'프로젝트 > Project_Island' 카테고리의 다른 글

56. 몬스터 소환 및 이동 동기화  (0) 2026.05.27
55. 이동 동기화  (0) 2026.05.26
53. 캐릭터 선택, 생성  (0) 2026.05.12
52. 회원가입과 로그인  (0) 2026.05.08
51. 언리얼 클라에서 OpenSSL 사용하기  (0) 2026.05.01

+ Recent posts