지난번엔 플레이어를 소환하고, 움직임을 동기화해서 다른 플레이어들이 움직이는걸 내 클라이언트에서 확인해봤다.
이번엔 몬스터를 소환, 동기화하는 것을 목표로 만들어보자.
서버의 Room Update 함수
지금까지 Enter나 Move를 구현해봤는데 클라이언트가 서버로 패킷을 보내면 패킷핸들러에서
GRoom->DoAsync(&Room::Enter, player, pkt.character_index(), pkt.room_id());
room->DoAsync(&Room::Move, player->GetObjectId(), pkt.dest(), pkt.speed());
이런식으로 Room의 Enter를 DoAsync함수로 호출하고, Move함수 안에서 패킷을 다시 다른 플레이어들에게 뿌려주는 식으로 동작했다. 그런데 이 방법은 플레이어가 패킷을 보낼 때 에만 동작하기 때문에 만약 월드에 플레이어만 있다면 전혀 문제가 없지만 내가 만들 게임에는 몬스터나 NPC를 넣고싶기 때문에 어떻게 만들지 고민해봐야 한다.
게임서버 강의(2) 에서는 이 부분을 Room에 Update 함수를 만들고, DoTimer와 DoAsync 함수를 사용해 Update 함수를 주기적으로 (0.1초) 호출하는 방식으로 해결할 수 있다고 소개했다. 다만 강의가 여기에서 마무리 됐기 때문에 구체적으로 몬스터를 서버사이드에서 소환하고 어쩌고 하는 부분은 내가 직접 도전해야할 과제이다.
우선 Update 함수를 만들고, 0.1초마다 호출시키는걸 확인해봤다.
GameServer
void Room::Update()
{
Utils::LockPrint("Update Room ", _roomId, " tick");
DoTimer(100, &Room::Update);
}
int main()
{
cout << "=== Server ===" << endl;
/*...*/
GRoom->DoAsync(&Room::Update);
}

잘 된다!
프로토콜 수정
이전에는 플레이어만 소환해서 사용했기 때문에 spawn 패킷이 오면 그냥 무조건 ClientPlayer 를 소환시켰다.
그런데 이젠 Spawn패킷이 오면 스폰하려는게 플레이어인지 스켈레톤인지 뭔지 알 수 있어야한다. 그래서 우선 Ptoro파일을 수정해야했다.
struct에서 PlayerInfo를 좀 더 보편적으로 쓸 수 있는 네이밍인 ObjectInfo로 변경했고, type 대신 template_id를 넣었다.
처음에는 objectinfo, creatureinfo, playerinfo, monsterinfo .... 이런식으로 만들어볼려 했는데, 그러면 spawn패킷도 몬스터 스폰 플레이어 스폰 프로젝타일 스폰 상자스폰 등등 늘어나야 할 것 같아서 이건 좀 아니다 싶어서 고민한 결과가 template_id다.
간단하게 4바이트 공간의 각 바이트에 타입정보를 넣어서 아래와 같이 저장해두는 것.
[0][OBJECT_TYPE_CREATURE][CREATURE_TYPE_PLAYER][PLAYER_TYPE_KNIGHT]
[0][OBJECT_TYPE_CREATURE][CREATURE_TYPE_MONSTER][MONSTER_TYPE_SKELETON]
이렇게 하면 각 template_id에는 매칭되는 오브젝트가 있을테니 꼭 monsterinfo 같은 구조체로 특정 몬스터의 정보를 넘기지 않아도 되니까 좋은 방법일거라 생각한다.
Struct.proto
syntax = "proto3";
package Protocol;
import "Enum.proto";
message ObjectInfo
{
uint64 object_id = 1;
string name = 2;
uint32 level = 3;
uint32 template_id = 4;
Position pos = 5;
}
/* 기존 플레이어인포 (삭제함)
message PlayerInfo
{
uint64 id = 1;
string name = 2;
uint32 level = 3;
PlayerType playerType = 4;
Position pos = 5;
}
*/
Enum.proto
syntax = "proto3";
package Protocol;
enum ObjectType
{
OBJECT_TYPE_NONE = 0;
OBJECT_TYPE_CREATURE = 1;
OBJECT_TYPE_PROJECTILE = 2;
OBJECT_TYPE_ENV = 3;
}
enum CreatureType
{
CREATURE_TYPE_NONE = 0;
CREATURE_TYPE_PLAYER = 1;
CREATURE_TYPE_MONSTER = 2;
CREATURE_TYPE_NPC = 3;
}
enum PlayerType
{
PLAYER_TYPE_NONE = 0;
PLAYER_TYPE_KNIGHT = 1;
PLAYER_TYPE_MAGE = 2;
PLAYER_TYPE_ARCHER = 3;
}
enum MonsterType
{
MONSTER_TYPE_NONE = 0;
MONSTER_TYPE_WEREWOLF = 1;
MONSTER_TYPE_STONEGOLEM = 2;
MONSTER_TYPE_SKELETON = 3;
}
enum MoveState
{
MOVE_STATE_NONE = 0;
MOVE_STATE_IDLE = 1;
MOVE_STATE_RUN = 2;
MOVE_STATE_JUMP = 3;
MOVE_STATE_SKILL = 4;
}
Protocol.proto
protocol에서도 playerinfo 대신 objectinfo로 전부 변경해준다.
/*...*/
message GS_ENTER_ROOM {
bool success = 1;
ObjectInfo character_info = 2;
string reason = 3;
}
/*...*/
변경하고나면 이름도 바뀌고 내용물도 많이 바뀌었으니 서버나 클라나 당연히 빌드가 안될 것이다. 이 부분을 전부 해결해준다. 주로 Room과 패킷핸들러에서 수정해야한다. 특히 type은 더이상 쓰지 않고 template_id 로 변경됐으니 이부분을 위해서 유틸함수를 추가했다.
정의
uint32 Utils::GetTemplateId(uint8 a, uint8 b, uint8 c, uint8 d)
{
uint32 templateId = (static_cast<uint32>(a) << 24) | (static_cast<uint32>(b) << 16)
| (static_cast<uint32>(c) << 8) | static_cast<uint32>(d);
return templateId;
}
사용 (Handle_GC_CHARACTER_LIST 함수에서의 예시)
pg->Getvalue로 가져오는건 db에 저장된 플레이어 클래스 타입이다.
uint32 templateId = Utils::GetTemplateId(0, Protocol::ObjectType::OBJECT_TYPE_CREATURE,
Protocol::CreatureType::CREATURE_TYPE_PLAYER, stoi(pg->GetValue(i, 2)));
이외에는 이름만 바뀌고 대부분 똑같으니 수정만 잘 해주자.
몬스터 블루프린트 준비 (Client)
먼저 소환시킬 몬스터를 준비해줬다. 캐릭터를 상속받은 Monster 클래스를 만들어주고, 블루프린트를 만든다.
몬스터 클래스 내용물은 ClientPlayer 에서 가져왔다. 몬스터나 내가 아닌 다른 플레이어나 이동 동기화는 비슷하게 작동할 것 같아서 우선 그렇게 했다. 다만 ClientPlayer에서는 있었던 생성자 부분의 대부분 코드가 사라졌는데, 애초에 ClientPlayer는 빙의를 안하는데 왜 GetCharacterMovement()->AirControl = 0.35f; 이런 설정들이 필요한지 이해가 안됐어서 몬스터에서는 제거해봤다.
기억을 되짚어보니 강의에서는 idle, run 상태와 yaw를 받아서 캐릭터를 부드럽게 움직이게 하고, 언리얼 물리/충돌 시스템을 활용 했는데 이걸 해볼 때 언리얼 상에서 콜리전끼리 비비면 내 화면 상의 내 위치와 다른 클라에서 보이는 내 위치가 틀어지는 문제가 발생해서 나는 캐릭터 무브먼트를 안쓰고 서버에서 보내는 위치데이터에 setactorlocation과 움직임 보간으로 부드럽게 보이게 만든 차이였다. 서로 장단이 있는거니까 그냥 난 일단 캐릭터 무브먼트는 안쓰는걸로 하자.
Monster.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Monster.generated.h"
namespace Protocol
{
class ObjectInfo;
class Position;
}
UCLASS()
class CLIENT_API AMonster : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AMonster();
virtual ~AMonster() override;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UFUNCTION(BlueprintPure)
float GetSpeed() const { return Speed; }
uint64 GetObjectId() const;
void SetMonsterInfo(const Protocol::ObjectInfo& MonsterInfo_);
void SetDestInfo(const Protocol::Position& DestInfo_);
void SetSpeed(float Speed_) { Speed = Speed_; }
protected:
Protocol::ObjectInfo* ObjectInfo;
Protocol::Position* DestInfo;
float Speed;
};
Monster.cpp
#include "Monster.h"
#include "Struct.pb.h"
// Sets default values
AMonster::AMonster()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
ObjectInfo = new Protocol::ObjectInfo();
DestInfo = new Protocol::Position();
}
AMonster::~AMonster()
{
delete ObjectInfo;
delete DestInfo;
}
// Called when the game starts or when spawned
void AMonster::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AMonster::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
//몬스터 움직임 구현
FVector Location = GetActorLocation();
FVector DestLocation = FVector(DestInfo->x(), DestInfo->y(), DestInfo->z());
FRotator DestRotation = FRotator(DestInfo->pitch(), DestInfo->yaw(), DestInfo->roll());
// Location 보간
FVector MoveDir = (DestLocation - Location);
const float DistToDest = MoveDir.Length();
MoveDir.Normalize();
float MoveDist = (MoveDir * FMath::Max(Speed, 450.0f) * DeltaTime).Length();
MoveDist = FMath::Min(MoveDist, DistToDest);
SetActorLocation(Location + MoveDir * MoveDist);
// Rotation 보간
FRotator CurrentRotation = GetActorRotation();
FRotator NewRotation = FMath::RInterpTo(CurrentRotation, DestRotation, DeltaTime, 10.0f);
SetActorRotation(NewRotation);
}
// Called to bind functionality to input
void AMonster::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
uint64 AMonster::GetObjectId() const
{
return ObjectInfo->object_id();
}
void AMonster::SetMonsterInfo(const Protocol::ObjectInfo& MonsterInfo_)
{
ObjectInfo->CopyFrom(MonsterInfo_);
}
void AMonster::SetDestInfo(const Protocol::Position& DestInfo_)
{
DestInfo->CopyFrom(DestInfo_);
}
그리고 적당한 에셋을 받아서 블루프린트로 만들고 적용시켰다. 애니메이션도 ClientPlayer 에서처럼 적용시켜줬다.
애니메이션은 귀찮아서 일단 스켈레톤에만 적용시켜줬다.


몬스터 클래스 준비 (GameServer)
게임서버쪽에도 몬스터 클래스를 만들고 사용해야한다. Protocol::Enum에서 분류해놓은 것 처럼 Object - Creature - Monster 이런식으로 클래스를 상속관계로 만들어보자. (강의에서 이렇게 했는데 아직은 잘 써먹는 법은 잘 모르겠다)
Object ~ Creature 는 추상클래스로 있는게 좋을 것 같아서 추상함수로 넣어줄 적당한 함수를 생각해봤는데 Update가 적절해서 Update함수와 OnEnterGame, OnLeaveGame 을 추상함수로 만들어줘봤다.
Creature는 지금 아무 역할이 없는데 만들어보다가 '이건 Creature에 있는게 적절하겠다' 싶은 변수들을 하나씩 옮기다 보면 해결 될 것 같다.
Object
#pragma once
#include "Enum.pb.h"
namespace Protocol
{
class ObjectInfo;
class Position;
}
class Object : public std::enable_shared_from_this<Object>
{
public:
Object();
virtual ~Object();
virtual void OnEnterGame() abstract;
virtual void OnLeaveGame() abstract;
virtual void Update() abstract;
RoomRef GetRoom() { return _room.load().lock(); }
void SetRoom(RoomRef room) { _room.store(room); }
void ClearRoom() { _room.load().reset(); }
public:
Protocol::ObjectInfo* GetObjectInfo() { return _objectInfo; }
void SetObjectInfo(const Protocol::ObjectInfo& info);
uint64 GetObjectId();
void SetObjectId(uint64 id);
string GetName();
void SetName(const string& name);
uint32 GetLevel();
void SetLevel(uint32 level);
uint32 GetTemplateId();
void SetTemplateId(uint32 templateId);
const Protocol::Position& GetPosition();
void SetPosition(const Protocol::Position& pos);
void SetPosition(const float x, const float y, const float z);
protected:
atomic<weak_ptr<Room>> _room;
Protocol::ObjectInfo* _objectInfo = nullptr;
};
#include "pch.h"
#include "Object.h"
#include "Struct.pb.h"
Object::Object()
{
_objectInfo = new Protocol::ObjectInfo();
}
Object::~Object()
{
delete _objectInfo;
}
void Object::SetObjectInfo(const Protocol::ObjectInfo& info)
{
_objectInfo->CopyFrom(info);
}
uint64 Object::GetObjectId()
{
return _objectInfo->object_id();
}
void Object::SetObjectId(uint64 id)
{
_objectInfo->set_object_id(id);
}
const Protocol::Position& Object::GetPosition()
{
return _objectInfo->pos();
}
void Object::SetPosition(const Protocol::Position& pos)
{
_objectInfo->mutable_pos()->CopyFrom(pos);
}
void Object::SetPosition(const float x, const float y, const float z)
{
_objectInfo->mutable_pos()->set_x(x);
_objectInfo->mutable_pos()->set_y(y);
_objectInfo->mutable_pos()->set_z(z);
}
string Object::GetName()
{
return _objectInfo->name();
}
void Object::SetName(const string& name)
{
_objectInfo->set_name(name);
}
uint32 Object::GetLevel()
{
return _objectInfo->level();
}
void Object::SetLevel(uint32 level)
{
_objectInfo->set_level(level);
}
uint32 Object::GetTemplateId()
{
return _objectInfo->template_id();
}
void Object::SetTemplateId(uint32 templateId)
{
_objectInfo->set_template_id(templateId);
}
Creature
#pragma once
#include "Object.h"
namespace Protocol
{
class CreatureInfo;
}
class Creature : public Object
{
public:
Creature();
virtual ~Creature() override;
virtual void OnEnterGame() abstract;
virtual void OnLeaveGame() abstract;
virtual void Update() abstract;
protected:
};
#include "pch.h"
#include "Creature.h"
#include "struct.pb.h"
Creature::Creature() {}
Creature::~Creature() {}
Monster
#pragma once
#include "Creature.h"
#include "Struct.pb.h"
class Monster : public Creature
{
public:
Monster(uint8 type);
virtual ~Monster() override;
virtual void OnEnterGame() override;
virtual void OnLeaveGame() override;
virtual void Update() override;
protected:
int32 _maxHp;
int32 _hp;
int32 _damage;
float _speed;
};
#include "pch.h"
#include "Monster.h"
#include "Room.h"
#include "Struct.pb.h"
Monster::Monster(uint8 type)
{
_objectInfo->set_object_id(Utils::GetObjectId());
uint32 templateId = Utils::GetTemplateId(0, Protocol::ObjectType::OBJECT_TYPE_CREATURE,
Protocol::CreatureType::CREATURE_TYPE_MONSTER, type);
_objectInfo->set_template_id(templateId);
_objectInfo->set_level(1);
switch (type)
{
case Protocol::MonsterType::MONSTER_TYPE_SKELETON:
_maxHp = 100;
_damage = 10;
_speed = 300.f;
_objectInfo->set_name("Skeleton");
break;
case Protocol::MonsterType::MONSTER_TYPE_WEREWOLF:
_maxHp = 200;
_damage = 20;
_speed = 350.f;
_objectInfo->set_name("Werewolf");
break;
case Protocol::MonsterType::MONSTER_TYPE_STONEGOLEM:
_maxHp = 500;
_damage = 30;
_speed = 200.f;
_objectInfo->set_name("Stone Golem");
break;
default:
_maxHp = 100;
_damage = 10;
_speed = 300.f;
_objectInfo->set_name("Unknown Monster");
break;
}
_hp = _maxHp;
}
Monster::~Monster() {}
void Monster::OnEnterGame() {}
void Monster::OnLeaveGame() {}
void Monster::Update() {}
이외에도 Player 클래스도 Creature의 하위클래스로 변경해줬다.
#pragma once
#include "Struct.pb.h"
#include "Creature.h"
class Room;
class Player : public Creature
{
public:
Player(GameSessionRef session);
virtual ~Player() override;
virtual void OnEnterGame() override;
virtual void OnLeaveGame() override;
virtual void Update() override;
public:
void ChatTest(const string& msg);
shared_ptr<GameSession> GetOwner() { return _owner.lock(); }
protected:
weak_ptr<GameSession> _owner;
};
#include "pch.h"
#include "Player.h"
#include "Room.h"
#include "GameSession.h"
#include "ClientPacketHandler.h"
Player::Player(GameSessionRef session) : _owner(session) {}
Player::~Player() {}
void Player::OnEnterGame() {}
void Player::OnLeaveGame() {}
void Player::Update() {}
void Player::ChatTest(const string& msg)
{
if (RoomRef room = _room.load().lock())
{
Protocol::GS_CHAT pkt;
string chatMsg = GetName() + " : " + msg;
pkt.set_msg(chatMsg);
room->DoAsync(&Room::Broadcast, ClientPacketHandler::MakeSendBuffer(pkt));
}
}
몬스터는 info를 생성자에서 set 하고 player는 외부에서 setter로 set하는 일관성이 없는데 이건 이슈에 올려놓고 나중에 고치자.
몬스터 스폰시키기
서버쪽 코드
스켈레톤 한마리를 Room에 스폰시키자.
지금 Room에 오브젝트를 추가하는 함수는 Enter 뿐인데 이건 플레이어의 접속을 위해 만든거니까 오브젝트용 플레이어가 아닌 으로 따로 하나 만들어줬다. AddObject 함수와 _objects.
class Room : public JobQueue
{
public:
/*...*/
void AddObject(ObjectRef object);
void Broadcast(SendBufferRef sendBuffer);
private:
uint32 _roomId;
map<uint64, PlayerRef> _players;
map<uint64, ObjectRef> _objects;
};
void Room::AddObject(ObjectRef object)
{
Protocol::GS_SPAWN pkt;
_objects.insert(make_pair(object->GetObjectId(), object));
object->SetRoom(static_pointer_cast<Room>(shared_from_this()));
Protocol::ObjectInfo* obj = pkt.add_objects();
obj->CopyFrom(*object->GetObjectInfo());
Broadcast(ClientPacketHandler::MakeSendBuffer(pkt));
}
AddObject는 오브젝트를 Room에 추가하고, 추가됐음을 모든 플레이어에게 알린다.
그리고 새로 접속한 플레이어가 플레이어 외의 오브젝트의 존재도 알아야 하기 때문에 Room::Enter에도 아래와 같이 추가해줬다.
void Room::Enter(PlayerRef player, int32 characterIndex, int32 roomId)
{
/*...*/
//해당 플레이어의 클라에 플레이어 외의 다른 오브젝트의 존재를 알림
{
Protocol::GS_SPAWN pkt;
for (auto& data : _objects)
{
ObjectRef& otherObject = data.second;
Protocol::ObjectInfo* otherObjectInfo = pkt.add_objects();
otherObjectInfo->CopyFrom(*otherObject->GetObjectInfo());
}
if (pkt.objects_size() > 0)
gameSession->Send(ClientPacketHandler::MakeSendBuffer(pkt));
}
}
그리고 생성된 Room에 처음부터 스켈레톤이 한마리 있도록 main문에서 아래와 같이 추가해줬다.
int main()
{
cout << "=== Server ===" << endl;
GRoom = make_shared<Room>();
/*...*/
//추가된 부분 - 몬스터 생성, Room에 넣어주기
MonsterRef monster = make_shared<Monster>(Protocol::MonsterType::MONSTER_TYPE_SKELETON);
monster->SetPosition(900, 900, 200);
monster->SetObjectId(Utils::GetObjectId());
GRoom->AddObject(static_pointer_cast<Object>(monster));
GRoom->DoAsync(&Room::Update);
}
클라쪽 코드
서버에 스켈레톤을 한마리 넣어놨으니 이제 새 클라가 접속하면 spawn 패킷에 스켈레톤이 한마리 담겨있을 것이다.
언리얼에서는 template_id를 사용해서 스켈레톤 블루프린트를 spawn 시키도록 만들어보자.
/*...*/
UCLASS()
class CLIENT_API UClientGameInstance : public UGameInstance
{
/*...*/
public:
UPROPERTY(EditAnywhere)
TSubclassOf<ACharacter> MyPlayerClass;
UPROPERTY(EditAnywhere)
TSubclassOf<ACharacter> OtherPlayerClass;
UPROPERTY(EditAnywhere)
TSubclassOf<ACharacter> MonsterSkeletonClass;
UPROPERTY(EditAnywhere)
TSubclassOf<ACharacter> MonsterWerewolfClass;
UPROPERTY(EditAnywhere)
TSubclassOf<ACharacter> MonsterStoneGolemClass;
TMap<uint64, AActor*> Objects;
/*...*/
};

게임 인스턴스 헤더에 새로 추가한 블루프린트를 넣을 수 있게 변수를 추가해준다.
그리고 HandleSpawn에서 template_id 에 따라 적절한 블루프린트를 스폰시키도록 코드를 짰다.
void UClientGameInstance::HandleSpawn(const Protocol::ObjectInfo& ObjectInfo)
{
if (GameServerSession == nullptr)
return;
auto* World = GetWorld();
if (World == nullptr)
return;
FVector SpawnLocation(ObjectInfo.pos().x(), ObjectInfo.pos().y(), 200);
ACharacter* SpawnedPawn;
switch ((ObjectInfo.template_id() & 0x0000FF00) >> 8)
{
case Protocol::CreatureType::CREATURE_TYPE_MONSTER:
switch (ObjectInfo.template_id() & 0x000000FF)
{
case Protocol::MonsterType::MONSTER_TYPE_SKELETON:
SpawnedPawn = World->SpawnActor<ACharacter>(MonsterSkeletonClass, SpawnLocation, FRotator::ZeroRotator);
break;
case Protocol::MonsterType::MONSTER_TYPE_WEREWOLF:
SpawnedPawn = World->SpawnActor<ACharacter>(MonsterWerewolfClass, SpawnLocation, FRotator::ZeroRotator);
break;
case Protocol::MonsterType::MONSTER_TYPE_STONEGOLEM:
SpawnedPawn = World->SpawnActor<ACharacter>(MonsterStoneGolemClass, SpawnLocation, FRotator::ZeroRotator);
break;
default:
SpawnedPawn = World->SpawnActor<ACharacter>(MonsterSkeletonClass, SpawnLocation, FRotator::ZeroRotator);
break;
}
break;
case Protocol::CreatureType::CREATURE_TYPE_PLAYER:
SpawnedPawn = World->SpawnActor<ACharacter>(OtherPlayerClass, SpawnLocation, FRotator::ZeroRotator);
break;
default:
SpawnedPawn = World->SpawnActor<ACharacter>(MonsterSkeletonClass, SpawnLocation, FRotator::ZeroRotator);
break;
}
Objects.Add(ObjectInfo.object_id(), SpawnedPawn);
}
switch case를 난무해서 가독성이 매우 안좋으니 나중에 정상화를 좀 해야할 것 같다. 그리고 비트마스크도 지금은 그냥 00FF00 이런식으로 써서 나중에 보면 이게 뭐지? 싶을 것 같으니 enum이나 상수로 설정하도록 하자. (이슈에 올려놓자)

서버로부터 패킷 수신 -> 패킷핸들러 -> 핸들 스폰 함수를 통해 스켈레톤을 스폰시켰다.
그런데 공중에 떠있다. 이 문제는 해결이 쉽지 않을 것 같아서 다음 게시글로 미루고, 일단은 이동 동기화부터 해보자.
이동 동기화
Monster의 Update 함수를 만들어서

이런 느낌으로 스켈레톤이 빙글빙글 돌게 만들어봤다.
void Monster::Update()
{
RoomRef room = GetRoom();
if (room == nullptr)
return;
Protocol::Position* pos = _objectInfo->mutable_pos();
float x = pos->x();
float y = pos->y();
if (x < 800 && y > 800)
{
//Right
x += _speed * 0.1f;
pos->set_yaw(0);
}
else if (x > 800 && y > -800)
{
//Down
y -= _speed * 0.1f;
pos->set_yaw(-90);
}
else if (x > -800 && y < -800)
{
//Left
x -= _speed * 0.1f;
pos->set_yaw(180);
}
else if (x < -800 && y < 800)
{
//Up
y += _speed * 0.1f;
pos->set_yaw(90);
}
else
{
//Down
y -= _speed * 0.1f;
pos->set_yaw(-90);
}
pos->set_x(x);
pos->set_y(y);
room->DoAsync(&Room::Move, GetObjectId(), *pos, _speed);
}
마치 플레이어의 이동 패킷을 받았을 때 Move 패킷을 모든 플레이어에게 뿌려줘서 내 클라에서 다른 플레이어의 이동을 동기화 시켯던 것 처럼 Room::Move 함수를 사용해 몬스터의 이동 위치를 모든 플레이어에게 뿌려주도록 한다.
클라이언트에서 Move 패킷을 받은 다음의 처리
void Handle_GS_MOVE(const PacketSessionRef& session, const Protocol::GS_MOVE& pkt)
{
if (UClientGameInstance* GI = Cast<UClientGameInstance>(GWorld->GetGameInstance()))
{
AActor** Found = GI->Objects.Find(pkt.object_id());
if (Found == nullptr)
return;
AClientPlayer* Player = Cast<AClientPlayer>(*Found);
if (Player == nullptr)// 플레이어가 아니라면
{
AMonster* Monster = Cast<AMonster>(*Found);
if (Monster == nullptr)
return;
Monster->SetDestInfo(pkt.dest());
Monster->SetSpeed(pkt.speed());
}
else // 플레이어라면
{
if (GI->MyPlayer->GetObjectId() == pkt.object_id())
return;
Player->SetDestInfo(pkt.dest());
Player->SetSpeed(pkt.speed());
}
}
}
위와 같이 만들어서 처리해줬다.
좀 지저분한 코드지만 일단 돌아는 간다. 지금은 ClientPlayer와 Monster가 완전 다른 클래스라서 이렇게 처리했는데, 게임서버에서처럼 Object , Creature 클래스를 만들어 공통클래스를 갖게 하면 훨씬 더 깔끔할 것이다. 이것도 이슈로 남겨놓자.
이렇게 하고, Room의 Update에서 0.1초마다 _objects 를 순회하며 Monster의 Update를 호출하도록 만들어주면 된다.
void Room::Update()
{
Utils::LockPrint("Update Room ", _roomId, " tick");
for (auto Object : _objects)
{
Object.second->Update();
}
DoTimer(100, &Room::Update);
}

의도한 대로 스켈레톤이 방향을 전환하며 움직이고있다.
다만 물체를 통과하는 부분이나 공중에 떠있는 부분은 해결해야할 과제. 이걸 해결하려면 데디케이트 서버가 아니라 완전히 서버(?)이기 때문에 지형데이터가 서버에도 있어야 하고, 조금 알아보니 아직 확실치 않지만 Recast와 Detour 라는 라이브러리를 서버에 포함시켜야 할 것 같다.
지금까지의 git 버전
게임서버
Feat: 몬스터 스폰, · Dodontak/Project_Island_GameServer@9bc7bcd
github.com
클라이언트 (fab에서 받은 언리얼 에셋은 포함하지 않음)
Feat: 몬스터 spawn, 이동 동기화 · Dodontak/Project_Island_Client@fb43d4d
@@ -74,3 +74,6 @@ Plugins/**/Intermediate/*
github.com
'프로젝트 > Project_Island' 카테고리의 다른 글
| 57. RecastNavigation 라이브러리 설치하기 (0) | 2026.06.04 |
|---|---|
| 55. 이동 동기화 (0) | 2026.05.26 |
| 54. 스폰, 디스폰 (0) | 2026.05.18 |
| 53. 캐릭터 선택, 생성 (0) | 2026.05.12 |
| 52. 회원가입과 로그인 (0) | 2026.05.08 |