하아찡
[언리얼5, C++] 몬스터 생성 본문
언리얼 버전 5.5.3
서버언어 C++로 구성했습니다.
서버 프로그램을 공부하는데 기분이 알쏭달쏭 합니다.
뭔가 프로그래밍을 처음 문법을 다 배우고나서 뭔가 만들어봐야겠다 싶은 마음으로 프로그램을 만들라고 했지만 벽에 막혀버린 그런느낌을 또 한번 느끼네요...
몬스터 생성을위해 상당한 고민을 했습니다.. 거의 일주일정도 걸린거같습니다...
클라이언트와 서버를 동시에 작업을 하는데 제가 봤던 루키스님 강의에서 "클라이언트를 절대 믿지말라" 라는 말이 있어서 모든 데이터 처리는 서버에서 해야겠구나 라고 생각을 해봤는데 문제는 거기서부터 시작이 됐습니다.
현재 몬스터 생성을 하고싶은데 클라이언트에서 몬스터 스포너를 만들어서 해당 위치에다가 스폰을 하게 하면 되지 않을까? 그러면 서버에서도 굳이 몬스터 생성위치를 저장하지않아도 스포너 고유값만 받아서 해당 스포너 위치에다가 소환을 해주게되면 되지않을까? 했는데...
그러면 클라이언트에서 스포너 위치를 바꾸면 몬스터 스폰위치가 동기화가 되지 않겠구나 라고해서 작업을하다가 싹 갈아 엎어버렸습니당! 키야~
하소연은 여기까지하고!
저는 어떠한 방식으로 작업을 했냐!
일단 이전글에서 맵을 나누고 채널을 생성하게 했습니다.
이제 맵별로 클래스를 생성할껀데 Room을 상속받은 클래스를 생성합니다.
그래서 RoomManager에서 관리도 가능하기에
그러면 맵별로 클래스를 왜 다시 생성을 했냐?
몬스터 스폰위치를 전부다 서버에 올려놓고 클라이언트는 해당 정보를 받고 몬스터를 스폰해주는 역할만 하게 만들기 위해서입니다.
그러다보니 맵별로 서버에서 정해준 스포너 위치가 달라지게 되다보닌깐 기존 Room 클래스를 상속받은 맵 클래스들을 만들 예정입니다.
저는 어떻게 작업을 했는지 코드로 먼저 보시죠
//서버에 스포너 위치 설정
void Room::SetMonsterSpawner(uint64 spawnerID, uint64 monsterType, float x, float y, float z)
{
SpawnLocation location;
location.x = x;
location.y = y;
location.z = z;
location.yaw = 1;
_levelMonsterSpawn.insert({ spawnerID, monsterType});
_levelMonsterSpawnLocation.insert({ spawnerID ,location });
//몬스터를 생성해서 서버에 저장시킴
MonsterRef monster = ObjectUtils::CreateMonster(spawnerID, x, y, z);
_monsters[monster->objectInfo->object_id()] = monster;
}
위 코드는 맵에서 몬스터 스폰위치를 생성과 동시에 몹을 생성을 해줍니다. 그래서 클라이언트는 접속하면 바로 몬스터 스폰패킷을 전달받아 해당 몬스터를 소환하게 됩니다.
파라미터값으로
- spawnerID값은 고유한 값으로 중복이 불가능합니다.
- monsterType값은 해당 스포너가 어떤 몬스터를 소환할지 정해줍니다.
- 나머지는 몬스터 스폰위치입니다.
Room클래스를 상속받은 맵클래스입니다.
BaseLevel.h
#pragma once
#include "Room.h"
class BaseLevel : public Room
{
public:
BaseLevel();
~BaseLevel();
void Init() override;
void UpdateTick() override;
// 현재 공격기능이 없기때문에 그냥 서버에서 때림
void TestHit();
};
BaseLevel.cpp
#include "pch.h"
#include "BaseLevel.h"
#include "ObjectUtils.h"
#include "Monster.h"
#include "ClientPacketHandler.h"
BaseLevel::BaseLevel()
{
uint64 index = 0;
// TODO 어디에 맵별로 데이터를 저장해두고 불러다 써야할듯 너무 지저분함.
SetMonsterSpawner(index++, Protocol::MONSTER_ID_GOBLIN, 100, 200, -5);
SetMonsterSpawner(index++, Protocol::MONSTER_ID_GOBLIN, 200, 200, 1);
SetMonsterSpawner(index++, Protocol::MONSTER_ID_GOBLIN, 300, 300, -5);
SetMonsterSpawner(index++, Protocol::MONSTER_ID_GOBLIN, 500, 400, 1);
SetMonsterSpawner(index++, Protocol::MONSTER_ID_GOBLIN, 600, 400, -5);
SetMonsterSpawner(index++, Protocol::MONSTER_ID_GOBLIN, 700, 400, 1);
SetMonsterSpawner(index++, Protocol::MONSTER_ID_GOBLIN, 800, 400, -5);
SetMonsterSpawner(index++, Protocol::MONSTER_ID_GOBLIN, 900, 400, 1);
SetMonsterSpawner(index++, Protocol::MONSTER_ID_GOBLIN, 1000, 400, -5);
SetMonsterSpawner(index++, Protocol::MONSTER_ID_GOBLIN, 1100, 400, 1);
}
BaseLevel::~BaseLevel()
{
cout << "~BaseLevel" << endl;
}
void BaseLevel::Init()
{
MyRoom = GetRoomRef();
UpdateTick();
}
void BaseLevel::UpdateTick()
{
TestHit();
if (bRoomActive)
MyRoom->DoTimer(updateTick, &BaseLevel::UpdateTick);
}
void BaseLevel::TestHit()
{
// 플레이어 주변에 몬스터가있으면 피가담
vector<uint64> DeSpawnObjectIDs;
vector<uint64> SpawnIDs;
Protocol::S_SKILL pkt;
pkt.set_caster_id(1);
pkt.set_skill_id(Protocol::SKILL_NONE);
float Damage = 100.f;
bool bMonsterDie = false;
for (auto monster : _monsters) {
Protocol::ObjectInfo* objectinfo = monster.second->objectInfo;
uint64 objectindex = objectinfo->object_id();
Protocol::SkillHitResult* r = pkt.add_hit_results();
r->set_damage(Damage);
r->set_object_id(objectindex);
r->set_is_critical(0);
//TODO 해당 함수에서 실제 몬스터 체력을깍음
MonsterHit(objectindex, Damage);
if (IsMonsterDie(objectindex)) {
bMonsterDie = true;
// TODO 디스폰 objectid값과 스포너값을 추출해서 벡터로 각각 가지고있다가 디스폰과 스폰에 넘겨줌
DeSpawnObjectIDs.push_back(objectindex);
SpawnIDs.push_back(monster.second->SpawnerID);
}
}
SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(pkt);
Broadcast(sendBuffer, NotObjectID);
if (bMonsterDie) {
LOG("DeSpawnObject : " << DeSpawnObjectIDs.size() << " SpawnObject : " << SpawnIDs.size());
MonsterDeSpawn(DeSpawnObjectIDs); //디스폰 해주고
MonsterSpawn(SpawnIDs); //스폰 대기열 추가
}
}
실행화면

BaseLevel클래스로 불러와진 맵입니다.
몬스터가 피가 다깍이면 서버에서 디스폰 명령을 내리고, 몬스터별로 정해진 스폰시간으로 해당 몬스터를 다시 해당위치로 스폰시켜줍니다.
현재는 고블린만 몬스터 테이블에 등록해둔 상태라 리스폰시간이 일정합니다.

Room클래스로 불러와진 맵입니다.
기본적인 Room 클래스는 몬스터 스폰하는 코드가 없기때문에 몬스터가 스폰되지 않는 모습을 볼 수 있습니다.
이번에 사실 고민을 엄청 많이했습니다. 실제로 작업한시간보단 "이렇게 구현하는게 맞을까?" 이런저런 생각이 많더라구요.
늘 프로그램을 공부할때마다 느끼지만, 사실 코드가 맞다 틀리다는 없다고 생각합니다.
어떠한 방식이 좀더 효율적일까? 현재 내가쓴 방식이 효율적인가? 비효율적인가?
"비효율적이면 좀더 나은 방식이 어떤게 있을까?" 하면서 이것저것 찾아보닌깐 오랜만에 또 재밌더라구요.
아무튼 이번엔 사실 몬스터 생성도 고민을 많이 해봤지만 해당 몬스터를 어떻게 움직이게 할까? 플레이어 어그로는 어떻게해야할까? 이런저런 고민도 있었습니다.
그래서 다음글은 생성된 몬스터가 플레이어를 어그로를 잡는 기능을 추가해볼 예정입니다~
'C++ > 이것저것서버테스트' 카테고리의 다른 글
[언리얼5, C++] 몬스터 어그로 (0) | 2025.03.12 |
---|---|
[언리얼5, C++] 몬스터 움직임 (0) | 2025.03.06 |
[언리얼5, C++] 맵 이동 - 2 (0) | 2025.02.25 |
[언리얼5, C++] 맵 이동 (0) | 2025.02.23 |
[C++서버] 서버 채널 분할 (0) | 2025.02.21 |