발전하는 춘배
[원고, C++/OOP] 로그라이크 게임 만들어보기 1 - 클래스 헤더 파일 분리, enum class, 공격과 피해 책임 분리 본문
0. 시작
GPT한테 웹 기반 IDE로 개발하려 하니깐 웹IDE 추천해달라 부탁했다.
replit.com을 추천해줘서 오 하고 있었는데 얘가 무슨플젝하면 좋을지도 추천해준다.
5개 추천받았는데 **(강력 추천)**까지 받아서 재밌겠다 싶어 선택해봤다.

**(중요)**한 목표 정의

일단 딱 이정도만 해보자.

일단 젤 만만해 보이는 게 Map이라 Map.cpp랑 Map.h 먼저 만들기로 함
1. 클래스 헤더 파일 분리
예전에 분명히 알고 했었는데 또 시간 한참 지났다고 cpp랑 h 분리하는 걸 까먹어버림
C++ 클래스 파일 분리(헤더파일,cpp파일,클래스.cpp파일)
클래스를 메인함수에서만 선언하는 것이 아닌, 헤더파일을 이용하여 파일 분리하는 방법
velog.io
이거 보고 기억남
class는 헤더파일에 선언하고 cpp파일에서 구현한다. 그리고 메인에선 #include "time.h"로 헤더 파일을 include 해서 사용.
참고로
#include "time.h" 를 여러 .cpp 파일에서 쓰면
컴파일 과정에서 같은 선언이 중복 정의돼서 에러가 날 수 있는데 이를 방지하기 위해 한 번만 컴파일되도록 아래 구문을 넣는다.
#ifndef TIME_H
#define TIME_H
// 클래스 구현 ...
#endif
또 이렇게 헤더랑 구현이랑 분리하면 cpp파일에서 스코프 문제가 발생하므로
void Time::setHour(int h) {}
이렇게 스코프 지정 연산자(scope resolution operator) ::을 사용해서 Time 클래스의 setHour함수임을 알려줘야 한다.
2. C++ 열거형 (enum, enum class)
https://boycoding.tistory.com/179
C++ 05.04 - 열거형, enum
05.04 - 열거형, enum C++에는 많은 자료형이 내장되어 있다. 하지만 이 자료형들이 원하는 걸 표현하기에 항상 충분하지는 않다. 그래서 C++은 프로그래머들이 자신만의 자료형을 만들 수 있게 해 주
boycoding.tistory.com
왜쓰냐면.. Entity 구현할 때 죽은 상태랑 산 상태랑 뭐 기타등등이 있을 텐데 이걸 int형으로 한다면
entity.state = 3;
entity.state = -1;
1이 뭐고 2가 뭐고 직접 정의한 문서를 만들지 않는 이상 뭐가 뭔상태인지 모르겠고 버그여도 컴파일이 된다는 문제가 생긴다.그래서
enum State {
ALIVE,
DEAD
};
class Entity {
State state;
};
이렇게 enum을 사용해주면
1. 읽기가 좋다.
entity.state = DEAD;
2. 확장성이 좋다.만약 bool로 상태를 관리한다고 하면...
bool isAlive = true;
이런 식으로 될텐데 지금이야 상태가 2개니 괜찮지만 여기서 기절 상태를 추가한다 치면 벌써 기분이 안좋다.
3. 상태머신
void Entity::update() {
switch (state) {
case State::Alive:
// 정상 행동
// 이동, 공격 가능
break;
case State::Stunned:
// 아무 행동 못 함
// 턴 넘김
break;
case State::Dead:
// 완전히 비활성
// 입력 무시, 렌더만 시체로
return; // 아예 함수 종료
}
}
switch랑 같이 쓰기 좋다.
참고로 C++에선 위 예시에서 사용한 enum class가 좋다.
일반 enum은 전역에 풀려버리므로 의미상 불가능한 일(Direction d = ALIVE)을 컴파일러가 못 잡는다.
enum class는
enum class State {
Alive,
Dead
};
enum class Direction {
Left,
Right
};
스코프 자체가 분리되므로 Direction d = State::Alive처럼 의미상 불가능한 일을 하려고 하면 컴파일 에러를 띄울 수 있다.
또 enum은 사실상 int취급이므로 암묵적 형변환이 되어버릴 수 있는데 enum class는 class라서 자동으로 int로 바뀌거나 하는 불상사가 일어나지 않는다.
3. 공격과 피해의 책임 분리 (OOP)
쭉 구현하다가
막힌 부분이 Entity 클래스를 만들 때였다.
void Entity::attack(Entity *entity) {
entity->setHp(entity->getHp() - damage);
if (entity->getHp() <= 0) {
entity->setState(EntityState::DEAD);
entity->setSymbol('X');
}
}
void Entity::takeDamage(int damage) {
hp -= damage;
if (hp <= 0) {
state = EntityState::DEAD;
symbol = 'X';
}
}
처음에 이렇게 하려 하니까 뭔가 기분이 불쾌하다. 근데 어떻게 해야 좋을진 마당히 생각이 안 나서 gpt형님한테 물어봤다. 공부는 gpt로..
문제 1: 로직 중복:
HP 감소 + 죽음 처리 로직이 중복된다
즉, 데미지를 받아서 죽었을 때 일어나는 일(로그남기기, 아이템떨구기, 애니메이션 등등등.)을 나중에 추가/변경하려고 할 때 이 두 군데를 다 바꿔줘야 하는데 당연히 실수와 버그로 이어진다는 것.
문제 2: OOP 책임 분리 위반
attack() 메서드가 "상대 entity"의 내부 상태를 직접 조작한다.
문제 3: 캡슐화 깨짐
attack()이 상대방의 hp, symbol, state를 모두 알고 직접 제어함
Entity의 내부 구현에 강하게 의존하고 있으므로 나중에 state, symbol 구조 바꾸면 전부 깨진다.
결국 문제는 attack()이 상대방의 상태를 알고 제어한다는 점에서 기인하므로 아래와 같이 바꾼다.
void Entity::attack(Entity* entity) {
entity->takeDamage(damage);
}
void Entity::takeDamage(int damage) {
hp -= damage;
if (hp <= 0) {
state = EntityState::DEAD;
symbol = 'X';
}
}
"공격자는 “데미지를 준다”까지만. 피해자는 “피해를 어떻게 처리할지”를 결정."
“attack은 메시지, takeDamage는 규칙”
보면 gpt가 이해하기 좋게 말을 참 잘하는 것 같다.
