2차 프로젝트 일지 - 1~2일차
1일차: 기획
- 2월 6일간 개발했던 '크레이지아케이드' 소스에서 출발
- 완성 못했던 적 AI를 강화할 예정
- 비헤이비어트리 + A스타 알고리듬 추가로 실제 게임에서 느낄 수 있는 똑똑한 적 만들기
- 시간이 허락한다면 공격까지 추가
2일차: 개발 시작
코드 정리부터
-
폴더 정리
Actorㄴ CharacterBase.h/cppㄴ Player.h/cppㄴ Enemy.h/cppㄴ Bubble.h/cppㄴ BubbleExplosion.h/cppㄴ ExplosionTile.h/cppㄴ Box.h/cppㄴ Bush.h/cppㄴ Ground.h/cppㄴ Wall.h/cppㄴ Item.h/cppㄴ MoveSpeed.h액터 폴더에 정리 안된 채로 액터 상속받는 리소스들이 방치돼있음 → Copilot에게 정리해달라고 요청
Actorㄴ Characters ㄴ CharacterBase.h/cpp ㄴ Player.h/cpp ㄴ Enemy.h/cppㄴ Effects ㄴ Bubble.h/cpp ㄴ BubbleExplosion.h/cpp ㄴ ExplosionTile.h/cppㄴ Environment ㄴ Box.h/cpp ㄴ Bush.h/cpp ㄴ Ground.h/cpp ㄴ Wall.h/cppㄴ Items ㄴ Item.h/cppㄴ MoveSpeed.h -
코드에서 똥냄새 나기 시작함
Q. 지금 Enemy라는 NPC와 유저가 이동시키는 Player라는 캐릭터가 CharacterBase라는 공통의 클래스를 상속받고 있어. CharacterBase는 캐릭터가 가질 수 있는 상태(일반, 버블트랩상태, 사망) , 인벤토리, 무브먼트, 버블상태처리, 스탯, 쉴드(아이템 상태), 그리고 게임매니저 싱글톤 인스턴스를 참조하는 프라이빗 멤버변수를 가지고 있고 이것들을 조작하는 메소드/메시지들을 여러개 가지고 있어. 이 설계가 이유는 정확히 모르겠는데 어디선가 나쁜 냄새가 난다고 느껴. 그 이유가 뭘까? 고연차 게임 개발자로서 조언해줘.
AI의 답변)
- CharacterBase가 너무 많은 시스템을 소유한다(God Object)
- 단일 책임 원칙 위반: 상태 관리, 아이템 시스템, 전투 시스템, 이동 시스템까지 너무 많은 책임을 관장하고 있음
- 강결합: 이 시스템 중 하나만 바꿔도 Base를 확인해야한다
- Base를 상속받는 파생 클래스 또한 너무 많은 기능을 상속받고 있다
- 상속이 “행동 차이”를 표현하는 수단이 되고 있다
- CharacterBase의 기능이 일부 파생 클래스에서만 의미가 있다면 그 기능은 Base에 있어서는 안된다
- 짬통이 되어버렸음
- Character가 게임 월드 흐름을 일부 알고 있다(어정쩡한 GameManager 인터페이스)
- CharacterBase가 GameLevel의 내부 내용을 모르게하기 위한 구조라는 관점에서는 괜찮은 설계일 수 있으나, 여전히 GameManager의 HasBubbleAt, CanMove 같은 인터페이스를 직접 호출하고 있기 때문에 완벽하게 GameLevel과 분리되었다고 말하기 어렵다
AI의 제안)
- CharacterBase의 사이즈 다이어트 → Component 구조로 스탯, 아이템, 인벤토리 등등을 분리
- Observer Pattern 도입 → 완벽한 Character와 GameLevel의 분리(Event Driven이란 용어는 서버쪽 용어에 가까워보인다)
- Controller Pattern 도입 → Character는 틀만, 실제 행동 구현 책임은 Controller로 분리 + Player/AIController가 Character.Controller를 상속
- State Pattern 도입 → 상태로 파생되는 효과 구현 분리 및 일원화
- CharacterBase가 너무 많은 시스템을 소유한다(God Object)
개편된 Character 구조
CrazyArcade/Actor/Character/├── Character.h (메인 클래스)├── Character.cpp├── State/│ ├── CharacterState.h (상태 인터페이스 & 구현)│ └── CharacterState.cpp├── Component/│ ├── CharacterComponent.h (컴포넌트 기본 클래스 & 4가지 구현체)│ └── CharacterComponent.cpp // 얘도 그냥 컴포넌트별로 파일 나누면 안되나├── Controller/│ ├── PlayerController.h│ ├── PlayerController.cpp│ ├── EnemyController.h│ └── EnemyController.cpp└── Playable/ // Controller랑 합쳐야하는거 아닌가? ├── Player.h ├── Player.cpp ├── Enemy.h └── Enemy.cpp여담: 타입스크립트(자바스크립트)에선 {폴더명}/index.ts로 명명하면 import {폴더명}으로 축약이 되는데 왜 C++에는 그런 기능이 없을까
- C++20 Module 조언을 받은 결과 혼자서 하는 프로젝트에선 사용에 문제가 없고 빌드 결과 또한 문제가 없다고 한다!
IGameRuleManager 삭제
현재: CharacterController가 GameLevel의 자원을 직접 쓰지 않을 뿐이지 여전히 특정 자원의 존재 여부/행동 가능 여부를 묻고 있기 때문에 완벽하게 GameLevel을 몰라도 된다고 할 수 없음.
CharacterController가 "물풍선이 있는지 확인 → 버블 직접 생성 → 액터 직접 추가"하고 있음
새로운 설계: Controller는 의도만 표현하고, 세부 검증/실행은 옵저버(GameLevel)만 처리하게.
Controller: "나 스페이스 눌렸다 캐릭터야" → Character::RequestGenerateBubble()Character: "ㅇㅋ. 탄약 있나?" → Observer::OnRequestGenerateBubble(나, 좌표, 사거리)GameLevel: "그 자리에 버블 없네, 만들어줄게" → trueCharacter: "탄약 차감"? 읽어보니까 옵저버 아닌 것 같은데 옵저버는 일대다로 단방향 소통이고 이건 콜백 아닌가 그래서 알아보니 자바스크립트의 콜백에 해당하는 개념은 'Delegate'라는게 있더라
Delegate: 위임
'대리자'라고도 일컫는 이 기능은 로직 처리의 일부를 다른 객체에게 위임하기 위한 기능이다
C계열에 비하면 상당히 'MZ' 언어인 자바스크립트에선 사실 '델리게이트'라고 부르지도 않고 밥먹듯이 로직을 서로에게 위임한다. 그 이유는 자바스크립트에서 함수는 일급 객체(first-class citizen)이기 때문.
// C++ — 함수를 넘기려면 인터페이스로 감싸야 함class CharacterDelegate{public: virtual bool OnRequestMove(Character* c, const Vector2& pos) = 0;};// 누군가 이 클래스를 상속해서 구현체를 만들고// 그 포인터를 넘겨줘야 함character->SetDelegate(concreteDelegate);C++에선 이렇게 똥꼬쇼해야하는게
// 위의 C++ 코드 전체가 이거 한 줄character.onRequestMove = (c, pos) => gameLevel.canMove(c, pos);자바스크립트에선 이거 한 줄이면 끝 ...

GOAT
내가 잘 몰라서 그러는데, State에 작성한 ICharacterState는 역할이 뭐야? 인터페이스에 불과한거고 Character 클래스에서 상태는 각각 생성해서 들고 있어야 하는거야? 이것도 컴포넌트라는 단위로 분리할 순 없어? 이렇게 묻는 이유는 StatsComponent를 봐서 그래. Character의 특성이지만 컴포넌트로 분리돼서 StatsComponent 안에서만 관리되고 있잖아. 그리고 아직도 Character가 너무 많은 책임을 여전히 관리하고 있는 것처럼 보여. RequestMode, RequestGenerateBubble도 컴포넌트화되어야 하지 않나 싶어. 스탯이나 다른 컴포넌트들과 다른 특별한 것이 없는데 여전히 Character 클래스에 남아있다고 생각해. 코드를 보니까 다른 부분이라면 다른 특정 컴포넌트를 알아야한다는 조건정도? 이건 컴포넌트 안에서 못하나? 어쨌든 난 캐릭터를 프레임만 남기고 나머진 다 조립하는 식으로 하고 싶어.매우 마음에 들어. 고마워. 근데 조금만 더 수정하고 싶어.1. 어떤 부분은 GetCurrentState를 호출해서 ECharacterState중 하나와 비교하는 조건문을 사용하고 있는데, IsDead처럼 메시지를 제공해주는게 더 가독성을 챙길 수 있을 것 같아. 상태 확인 메시지들을 추가해줘.2. CharacterComponent를 컴포넌트 별로 파일을 나눠줘. 하나의 CharacterComponent에 들어가있으니까 찾기가 너무 어려워. 이렇게 하면 include 구문이 엄청 늘어날 수 있는데 이건 어떻게 개선할 수 있을까? index.h? 아님 그대로 두는게 좋으려나? 이건 업계에서 어떻게 하는지 알려줘.업계 관행: 파일 분리 시 include 관리 C++ 업계에서는 크게 3가지 접근법이 있습니다:
- IWYU (Include What You Use) — Google, Chromium 등 대형 코드베이스의 표준. 각
Bubble.cpp가 필요한 헤더만 정확히 포함. 컴파일 속도 최적화에 유리하지만#include줄이 많아짐. - Aggregate/Umbrella Header — Unreal Engine 스타일. 개별 헤더로 분리하되, 하나의 대표 헤더가 전체를 포함.
<Windows.h>가 대표적 예시. 편의성과 분리를 동시에 달성. - PCH (Precompiled Header) — 자주 쓰는 헤더를 미리 컴파일. 게임 엔진에서 보편적.
이 프로젝트 규모에서는 2번(Aggregate Header) 이 실용적입니다. 개별 파일로 분리하되,
CharacterComponent.h를 모든 컴포넌트를 포함하는 aggregate로 유지하면 기존#include를 변경할 필요가 없습니다. 특정 컴포넌트만 필요한 곳은 개별 헤더를 직접 포함할 수도 있고요.
bool BubbleComponent::RequestGenerateBubble(){ if (!owner) return false; auto stats = owner->GetComponent<StatsComponent>(); if (!stats || stats->GetBubbleAmmo() <= 0) return false; auto delegate = owner->GetDelegate(); if (!delegate) return false; if (delegate->OnRequestGenerateBubble(owner, owner->GetPosition(), stats->GetBubbleRange())) { stats->SetBubbleAmmo(stats->GetBubbleAmmo() - 1); return true; } return false;}너무 깔끔해졌다