발전하는 춘배
[원고, C++/OOP] 로그라이크 게임 만들어보기 5 - 전투 : 공격 및 플레이어 사망 본문
1. 공격
이동이 가능해졌으므로 이제 전투를 해보자.
플레이어가 이동한 곳에 적이 있다면, 서로 맞딜하여 피를 깎아본다.
이를 위해 tryMoveEntity 로직에 공격 로직을 추가해준다.
때려보고 잡았으면 그 자리로 이동하고, 못 잡았으면 원래 자리에서 한대 맞는다.
int newX = entity->getX() + x;
int newY = entity->getY() + y;
if (currentMap->getTile(newX, newY).props.walkable) {
entity->move(x, y);
Entity *target = getEntityAt(newX, newY);
if (target != nullptr && target->getState() != EntityState::DEAD) {
entity->attack(target);
if (target->getState() != EntityState::DEAD) {
target->attack(entity);
entity->move(-x, -y);
}
}
}
이렇게 했더니 문제 하나가 발견되었는데, 맵을 출력할 때 죽은 개체와 산 개체의 좌표가 겹칠 때 산 개체가 우선적으로 출력되어야 하는데 그런 고려가 없었다는 것이다. 반영해서 수정해준다. (x,y)의 엔티티를 찾아 리턴하는 함수인데 산 엔티티끼리는 서로 겹치지 않음이 보장되었다 치고 코드를 작성했다.
Entity *World::getEntityAt(int x, int y) {
auto it = entities.find(currentMapId);
if (it == entities.end())
return nullptr;
auto &vec = it->second;
std::vector<Entity*> deadEntities;
for (auto &e : vec) {
if (e->getX() == x && e->getY() == y) {
if (e->getState() == EntityState::DEAD)
deadEntities.push_back(e.get());
else
return e.get();
}
}
if (!deadEntities.empty()) {
return deadEntities[0];
}
return nullptr;
}
테스트: 플레이어(@) 체력은 30, 공격력은 10. 적($) 체력은 15, 공격력은 10
Enter command: s
# # # # # # # # # # # # # # #
# . . . . . . . . . . . . . #
# . . @ . . . . . . . . . . #
# . . $ . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# # # # # # # # # # # # # # #
Enter command: s
# # # # # # # # # # # # # # #
# . . . . . . . . . . . . . #
# . . @ . . . . . . . . . . #
# . . $ . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# # # # # # # # # # # # # # #
Enter command: s
# # # # # # # # # # # # # # #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . @ . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# # # # # # # # # # # # # # #
Enter command: d
# # # # # # # # # # # # # # #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . X @ . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# # # # # # # # # # # # # # #
굿..
2. 사망
플레이어가 죽으면 세계는 어떻게 될까?
로그라이크이므로 World를 초기화한다. 로그라이크의 특징과는 별개로 내맘대로 정한 규칙이다. 죽으면 끝
플레이어의 죽음을 어디서 체크해야할까 고민하다가 Game의 run 루프에서 매 턴마다 체크하기로 했다. 이유는 사실 간단하게 생각해 본건데
1. World를 초기화하는데 World에서 체크하고 스스로를 초기화한다? 뭔가 이상스럽다.
2. World는 맵과 엔티티들을 관리한다. 그러므로 엔티티를 이동시키거나 그에 따른 공격까지는 처리할 수 있으나, 그 결과로 엔티티가 사망한 경우에의 로직은 World에 담기 뭣하다.
근데 또 2번에 대해 생각해보다가 이러면 공격을 World가 처리하는 게 맞나 싶긴 했는데 그렇다고 이것도 Game으로 올린다는 건 더 이상했다. 뭔가 이상한데 왜 이상한지 말로 설명을 못하겠을 땐 GPT형님한테 물어본다.

조언에 따라 "상태 변화"라는 측면에서 접근해본다.
플레이어나 엔티티의 좌표(상태) 변화: World에서 일어난다.
엔티티의 HP(상태) 변화: World에서 일어난다.
플레이어의 사망(상태) 변화: World에서 일어난다.
그러므로 사망 자체는 World에서 체크하지만, 그 사망에 따른 결과로 초기화는 Game에서 한다. 이를 위해 World는 플레이어의 사망 유무를 Game에게 보고하는 일종의 getter를 가져야 한다.
bool World::isPlayerDead() { return player->getState() == EntityState::DEAD; }
void Game::run() {
isRunning = true;
world->populateMap("map1", 15, 10);
player= world->getPlayer();
while (isRunning) {
world->drawCurrentMap();
handleInput();
if (world->isPlayerDead()) {
std::cout << "You died!\n";
isRunning = false;
break;
}
}
}
테스트:
# # # # # # # # # # # # # # #
# . . . . . . . . . . . . . #
# . . $ . . . . . . . . . . #
# . . @ . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# . . . . . . . . . . . . . #
# # # # # # # # # # # # # # #
Enter command: w
You died!
