Unreal 개발 본 캠프 24일차

HappyCircle·2025년 12월 31일

Unreal 개발

목록 보기
41/163

오늘 진행 내용

[CH2: 팀 프로젝트] 텍스트 콘솔 RPG

C++ 콘솔 RPG 아이템·인벤토리 구조 리팩터링 (SOLID 기반)

오늘은 C++ 텍스트 콘솔 RPG 프로젝트에서
아이템, 인벤토리, 캐릭터, 효과 처리 구조를 전반적으로 정리하고 리팩터링했다.
단순 구현이 아니라, SOLID 원칙을 최대한 지키는 구조를 목표로 설계를 다시 잡았다.

1️⃣ Item 설계 리팩터링 – Effect 중심 구조

기존에는:

회복 / 버프 / 장비 아이템이 서로 다른 방식으로 효과를 가질 가능성이 있었고

장비 전용 StatBonus 같은 구조가 따로 존재해 확장 시 복잡해질 여지가 있었다.

개선 방향

모든 아이템 효과를 Effect 하나로 통합

Item은 효과 데이터를 제공만 하고, 실제 적용은 다른 책임으로 분리

핵심 코드

enum class EffectType {
    HealFlat,
    AddStatFlat,
    InvincibleHits
};

enum class StatType {
    Attack,
    MaxHealth
};

struct Effect {
    EffectType type;
    StatType stat;   // AddStatFlat일 때만 의미
    int value;       // 회복량 / 증감량 / 무적 횟수
    int duration;    // 지속 턴 (0이면 즉시)
};

✔ 아이템 종류가 늘어나도
→ Effect 조합만 추가하면 확장 가능

2️⃣ EffectManager 도입 – 효과 처리 책임 분리
문제점

Inventory 내부에서 캐릭터 스탯을 직접 수정하면

SRP 위반

Effect가 늘어날수록 Inventory가 비대해질 위험

해결

EffectManager 클래스를 별도로 분리

Effect 해석과 캐릭터 스탯 반영을 전담

핵심 코드 요약

class EffectManager {
public:
    static void apply(Character& c, const Effect& e, int sign);
    static void applyAll(Character& c,
                         const std::vector<Effect>& effects,
                         int sign);
};

// 적용 / 원복 공용 처리
EffectManager::applyAll(character, item->getEffects(), +1);
EffectManager::applyAll(character, item->getEffects(), -1);


sign = +1 → 장착/사용

sign = -1 → 해제/원복

✔ 장착/해제 로직을 대칭 구조로 처리
✔ Effect 추가 시 Inventory 수정 최소화

3️⃣ Inventory 역할 정리 및 헤더/CPP 분리

Inventory는 다음 책임만 갖도록 정리했다.

Inventory 책임

아이템 보관

소비 아이템 사용

장비 아이템 장착 / 해제

Shop과의 구매 / 판매 중개

Character는 포인터로 참조만 받고 소유하지 않는다.

Inventory.h (선언만)
class Item;
class Character;

class Inventory {
public:
    Inventory();
    ~Inventory();

    void add(Item* item);
    void useItem(Character* c, int index);
    void equipItem(Character* c, int index, EquipSlot slot);
    void unequipItem(Character* c, EquipSlot slot);

private:
    std::vector<Item*> items_;
    std::vector<Item*> equipped_;
};

Inventory.cpp (구현)
void Inventory::useItem(Character* c, int index) {
    Item* item = items_[index];
    EffectManager::applyAll(*c, item->getEffects(), +1);
    delete item;
    items_.erase(items_.begin() + index);
}

void Inventory::equipItem(Character* c, int index, EquipSlot slot) {
    int slotIdx = (slot == EquipSlot::Weapon) ? 0 : 1;

    if (equipped_[slotIdx]) {
        EffectManager::applyAll(*c, equipped_[slotIdx]->getEffects(), -1);
        items_.push_back(equipped_[slotIdx]);
    }

    equipped_[slotIdx] = items_[index];
    items_.erase(items_.begin() + index);

    EffectManager::applyAll(*c, equipped_[slotIdx]->getEffects(), +1);
}

✔ 역할이 명확해지고
✔ 컴파일 의존성과 협업 충돌 가능성 감소

4️⃣ Character 책임 재정의

Character는:

스탯 보관

게임 규칙을 포함한 “행동 API”만 담당

핵심 API 요약
void heal(int amount);
void addAttack(int delta);
void addMaxHealth(int delta);
void addGold(int delta);

void takeDamage(int dmg);
void tickEffects(); // 턴 종료 시 호출

✔ 직접 setter 사용 ❌
✔ 상태 변경 규칙을 Character 내부에 캡슐화

5️⃣ InvincibleHits / duration 처리 방향

InvincibleHits

기본 설계: 아이템 사용 즉시 활성화

duration > 0

timedEffects_에 등록

턴 종료 시 tickEffects()에서 감소 및 만료 처리

struct TimedEffect {
    Effect effect;
    int remaining;
};

“다음 턴부터 적용” 정책이 필요하면
→ pending 상태를 두는 방식으로 확장 가능하도록 설계만 잡아둠

6️⃣ Shop 연동 규칙 고려 사항 (반영 X)

메모리 소유권 문제를 명확히 정리

Shop은 아이템 프로토타입만 보관

구매 시:

Item* buyItem(int index) { return items[index]->clone(); }

Inventory가 소유권 관리(delete 책임)

판매 시:

아이템 delete

금액만 반환

✔ 댕글링 포인터 / 중복 delete 방지

7️⃣ SOLID 관점 정리

이번 리팩터링 결과:

SRP
Character / Inventory / EffectManager 책임 분리

OCP
Effect 추가로 기능 확장 가능

LSP
Item 파생 클래스 치환 가능

ISP
Item 인터페이스 최소화

DIP
Effect 처리 로직 분리로 의존성 완화

profile
개발합시다!

0개의 댓글