본 프로젝트는 C++ 베이스의 Text RPG로 3D, Isometric 뷰를 가지는 RPG 게임입니다.
Tick 분배
컴포넌트 구조를 쓴 이유는 단순히 파일을 나누기 위해서가 아니라, 업데이트 책임을 분리하기 위해서다.AObject::Tick()은 공통 상태를 처리하고, 그 다음 컴포넌트들에게 Tick을 분배한다.
void AObject::Tick(float DeltaTime)
{
if (HitFlashTime > 0.0f) { ... }
if (DamageTextTime > 0.0f) { ... }
if (SlowTime > 0.0f) { ... }
const float ScaledDeltaTime = DeltaTime * GetSlowRatio();
if (!Components.empty())
{
for (int i = 0; i < Components.size(); ++i)
{
if (Components[i])
{
Components[i]->Tick(ScaledDeltaTime);
}
}
}
}
이 구조 덕분에 이동은 MoveComponent, 공격 판정은 CombatComponent, 경험치나 레벨업은 다른 컴포넌트가 나눠서 처리할 수 있었다.
특히 Slow 같은 상태 이상을 공통 처리한 뒤, ScaledDeltaTime으로 각 컴포넌트에 내려보내는 방식은 오브젝트 단위 상태와 기능 단위 상태를 분리하기 좋았다.
이동과 전투
MoveComponent는 입력과 위치 변경만 담당한다.
void UMoveComponent::Tick(float DeltaTime)
{
if(nullptr == PlayerPtr)
PlayerPtr = dynamic_cast(GetOwner());
if (PlayerPtr == nullptr)
{
return;
}
MoveElapsedTime += DeltaTime;
TurnElapsedTime += DeltaTime;
if (MoveElapsedTime < MoveInterval)
{
return;
}
if (InputManager::GetInstance()->IsKeyPressed(KeyCode::UP))
{
PlayerPtr->SetPrevPosition(PlayerPtr->GetPosition());
PlayerPtr->SetPosition({ PlayerPtr->GetPosition().X, max(PlayerPtr->GetPosition().Y - 1, 0) });
SetFacingDirection(EDirection::UP);
MoveElapsedTime = 0.0f;
}
...
}
여기서는 맵 판정이나 전투를 같이 하지 않는다.
오로지 입력, 이전 위치, 현재 위치, 방향 전환만 처리한다.
반대로 CombatComponent는 공격 타이밍과 공격 범위만 담당한다.
void UCombatComponent::Tick(float DeltaTime)
{
TotalTime += DeltaTime;
if (TotalTime < DelayTime)
return;
if (InputManager::GetInstance()->IsKeyTap(KeyCode::Z))
{
AttackValue.clear();
switch (MoveComponentPtr->GetFacingDirection())
{
case EDirection::UP:
AttackValue.push_back({ OwnerPosition.X, OwnerPosition.Y - 1 });
break;
...
}
}
}
AttackValue.clear()가 최근에 다시 들어간 것도 같은 맥락이다.
공격 범위를 매 프레임 누적해두면 이전 공격 정보가 남아서 판정이 틀어지기 때문에, 입력이 발생하는 시점마다 현재 공격 상태만 다시 계산하도록 정리한 것이다.
풀링 구조
프로젝트가 커질수록 몬스터나 투사체를 계속 new / delete 하는 방식은 관리가 지저분해진다.
그래서 여기서는 ObjectPoolManager를 따로 두고, 타입별 풀을 만드는 방식으로 정리했다.
핵심 구조는 type_index를 키로 해서 타입별 풀을 분리하는 부분이다.
unordered_map> Pools;
unordered_map PoolByObject;
unordered_map ObjectByID;
각 풀은 실제 객체 목록과 사용 가능한 객체 목록을 따로 가진다.
template
struct TypePool : ITypePool
{
vector> Objects;
vector Available;
unordered_set AvailableSet;
};
Get()은 먼저 사용 가능한 객체가 있는지 확인하고, 있으면 꺼내서 다시 활성화한다.
template
T* Get(Args&&... args)
{
TypePool* pool = GetOrCreatePool();
if (!pool->Available.empty())
{
T* object = pool->Available.back();
pool->Available.pop_back();
pool->AvailableSet.erase(object);
ObjectByID[object->GetID()] = object;
object->OnSpawnFromPool();
return object;
}
unique_ptr newObject(new T(std::forward(args)...));
T* rawObject = newObject.get();
pool->Objects.push_back(move(newObject));
PoolByObject[rawObject] = pool;
ObjectByID[rawObject->GetID()] = rawObject;
rawObject->OnSpawnFromPool();
return rawObject;
}
반대로 반환할 때는 OnReturnToPool()을 먼저 호출해서 상태를 초기화한다.
void ReturnObject(AObject* object) override
{
T* typedObject = static_cast(object);
if (typedObject == nullptr || AvailableSet.count(typedObject) > 0)
{
return;
}
typedObject->OnReturnToPool();
Available.push_back(typedObject);
AvailableSet.insert(typedObject);
}
오브젝트 쪽에서도 풀링 생명주기를 받을 수 있게 훅을 넣어뒀다.
void AObject::OnSpawnFromPool()
{
bIsDestroy = false;
LastDamage = 0;
HitFlashTime = 0.0f;
DamageTextTime = 0.0f;
SlowTime = 0.0f;
}
void AObject::OnReturnToPool()
{
bIsDestroy = true;
LastDamage = 0;
HitFlashTime = 0.0f;
DamageTextTime = 0.0f;
SlowTime = 0.0f;
}
풀 매니저는 객체의 수명 관리만 담당하고, 객체는 자기 상태 초기화만 신경 쓰면 된다.
Render 분리
ViewportManager는 무엇을 그릴지 정리하고, RenderManager는 실제 콘솔 버퍼에 찍어낸다.
void ViewportManager::Render()
{
RenderObject();
RenderUI();
}
void ViewportManager::RenderObject()
{
Render2DtoISO();
}
그 다음 RenderManager가 마지막 출력만 담당한다.
void RenderManager::Render()
{
DrawScreen();
ClearScreen();
}
SceneManager, BattleManager, MapManager가 Tick에서 상태를 바꾸고 나면, ViewportManager는 그 상태를 읽어서 ISO 시점으로 조립하고, RenderManager는 조립된 결과를 화면에 밀어 넣는다.
'TIL' 카테고리의 다른 글
| C++ 프로그램에서 Cloudflare Worker로 R2에 GET, POST 하기 (0) | 2026.05.28 |
|---|---|
| 타일 기반에서 실시간 이동 표현을 위한 이동 보간 처리 (0) | 2026.05.27 |
| Text RPG 협업 5/21~ (0) | 2026.05.22 |
| 일주일 게임잼 제작 ~ 발표 (0) | 2026.05.21 |
| Delegate 쓰면 좋은 구조, 주의점 (0) | 2026.05.19 |