실습: Field·Manager·다형성

Jaemyeong Lee·2024년 12월 5일

게임 서버1

목록 보기
94/220

목표: 게임 월드의 객체를 안전하게 관리하기

Field(또는 World)는 다음을 책임집니다.

  • 객체 생성/등록 (AddObject)
  • 조회 (FindObject)
  • 프레임 업데이트 (Update)
  • 제거/정리 (RemoveObject, FlushDestroyQueue)

핵심은 다형성 + 수명 관리(ownership) 입니다.


Object 계층 설계

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를 크게 줄임
  • 예외가 나도 자동 정리되어 안전함(RAII)

raw pointer(Object*)는 참조 용도로만 잠깐 사용하고, 소유는 unique_ptr로 유지하는 것이 안전합니다.


기본 API 스케치

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 enum

방법 A. dynamic_cast

if (auto* p = dynamic_cast<Player*>(obj)) {
    // Player 전용 로직
}
  • 장점: 타입 안전성 높고 상속 구조와 자연스럽게 맞음
  • 단점: RTTI 비용/의존, 과사용 시 설계 냄새일 수 있음

방법 B. ObjectType enum 태그

if (obj->GetType() == ObjectType::Player) {
    // 태그 기반 분기
}
  • 장점: 단순하고 빠름
  • 단점: 새 타입 추가 때 분기문 수정 누적 가능

실무 팁:

  • 가능한 한 가상 함수 오버라이드(다형성)로 분기 자체를 줄이는 것이 유지보수에 유리합니다.

순회 중 삭제 금지: Destroy Queue 패턴

업데이트 루프에서 바로 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>를 쓰면 어떤 버그를 예방할 수 있는가?
  • Update 중 삭제를 지연(queue)해야 하는 이유는?
  • dynamic_cast와 virtual 함수 오버라이드 중, 언제 무엇이 더 적합한가?
  • map 대신 unordered_map을 선택할 때 얻는 것/잃는 것은 무엇인가?

profile
李家네_공부방

0개의 댓글