Field(또는 World)는 다음을 책임집니다.
AddObject)FindObject)Update)RemoveObject, FlushDestroyQueue)핵심은 다형성 + 수명 관리(ownership) 입니다.
class Object {
public:
virtual ~Object() = default; // 매우 중요
virtual void Update(float dt) = 0;
int GetId() const { return id_; }
protected:
int id_ = 0;
};
class Player : public Object {
public:
void Update(float dt) override { /* 입력/이동 처리 */ }
};
class Monster : public Object {
public:
void Update(float dt) override { /* AI 처리 */ }
};
Object 계층 구조
[Object] (virtual dtor)
/ | \
[Player][Monster][Projectile]
(모두 Update override)
권장 기본:
std::unordered_map<int, std::unique_ptr<Object>> objects;
왜 unique_ptr가 좋은가?
Field가 단독 소유)delete 누락/중복 delete를 크게 줄임raw pointer(Object*)는 참조 용도로만 잠깐 사용하고, 소유는 unique_ptr로 유지하는 것이 안전합니다.
class Field {
public:
bool AddObject(std::unique_ptr<Object> obj) {
int id = obj->GetId();
return objects_.emplace(id, std::move(obj)).second;
}
Object* FindObject(int id) {
auto it = objects_.find(id);
return (it == objects_.end()) ? nullptr : it->second.get();
}
void RemoveObject(int id) { objects_.erase(id); } // 소멸 자동 처리
private:
std::unordered_map<int, std::unique_ptr<Object>> objects_;
};
dynamic_cast vs enumdynamic_castif (auto* p = dynamic_cast<Player*>(obj)) {
// Player 전용 로직
}
ObjectType enum 태그if (obj->GetType() == ObjectType::Player) {
// 태그 기반 분기
}
실무 팁:
업데이트 루프에서 바로 erase하면 이터레이터 무효화/로직 꼬임이 생길 수 있습니다.
안전 패턴:
1) Update 중에는 destroyQueue에 id만 기록
2) 루프 종료 후 한 번에 erase
void Field::Update(float dt) {
for (auto& [id, obj] : objects_) {
obj->Update(dt);
// if (obj->IsDead()) destroyQueue_.push_back(id);
}
for (int id : destroyQueue_) {
objects_.erase(id);
}
destroyQueue_.clear();
}
| 실수 | 문제 |
|---|---|
| 베이스 소멸자 non-virtual | 파생 소멸자 미호출, 자원 누수 |
| Update 중 컨테이너 직접 erase | 이터레이터 무효화/스킵 버그 |
| 소유권과 참조를 둘 다 raw pointer로 관리 | 이중 해제/댕글링 포인터 위험 |
| 타입 분기문 if-else 체인 남발 | 새 타입 추가 때 수정 범위 폭증 |
Field에서 unique_ptr<Object>를 쓰면 어떤 버그를 예방할 수 있는가?dynamic_cast와 virtual 함수 오버라이드 중, 언제 무엇이 더 적합한가?map 대신 unordered_map을 선택할 때 얻는 것/잃는 것은 무엇인가?