개요
친구가 옆에서 다섯번째 띄어쓰기가 다섯 번째 아니냐고 하던데 사실 저도 알고 있습니다. (진짜임)
근데 블로그 첫글에서부터 띄어쓰기를 안해서 그냥 쭉 띄어쓰기 없이 가는겁니다.
절대 몰랐던거 아니에요. 진짜로요.
Enemy 움직임
일단 적을 움직여 주기 위해 Enemy 클래스에 변수랑 함수를 조금 추가해주겠습니다.
class Enemy : public Entity
{
public:
Enemy();
virtual ~Enemy();
protected:
int _hp;
int _rewardGold;
int _moveTime;
int _lastMoveTime = 0;
Vector2 _facingDir = Vector2(1,0);
clock_t _moveTimer;
public:
void tryMove();
void move();
void modifyHP(int value);
bool checkDead();
void dead();
};
대충 설명하자면...
_moveTime : 몇초마다 움직일 것인지
_lastMoveTime : 마지막에 움직인 시간
_moveTimer : 움직일 시간을 재는 타이머
_facingDir : 움직일 방향
_moveTimer를 통해 시간을 재면서 _moveTimer에 _lastMoveTime을 더한 값이 _moveTime을 넘어섰을때 _lastMoveTime에 _moveTimer를 넣어주고 현재 위치에 _facingDir을 더해주면 되겠죠.
그리고 유일한 적인 고블린의 생성자를 적지 않았으니까 생성자를 우선 적어주겠습니다.
//Goblin.h
public:
Goblin(ENTITY_TYPE type, std::string renderString, COLOR color, int hp, int moveTime, int rewardGold);
적은 생성자를 기반으로 EntityManager에서 맵에 넣어주는 Goblin을 수정합니다.
//EntityManager.cpp::init()
_enemyMap.insert(std::make_pair(ENEMY_TYPE::GOBLIN, []() -> Enemy* { return new Goblin(ENTITY_TYPE::ENEMY,"■", COLOR::GREEN, 10, 100, 10); }));
그 뒤에 InGameScene의 update 내에선 모든 Enemy의 tryMove 함수를 계속해서 실행시키고 tryMove 함수 내에선 이동 조건이 활성화 됐다면 move함수를 호출하여 움직여 주면 끝입니다.
적을 각자 따로 움직여 주기 위해서 이런 유사 비동기 같은 방식을 사용합니다.
적어보자면...
//Enemy.cpp
void Enemy::tryMove()
{
_moveTimer = clock();
if (_moveTimer - _lastMoveTime > _moveTime)
{
move();
_lastMoveTime = _moveTimer;
}
}
void Enemy::move()
{
GET_SINGLETON(MapManager)->deregisterEntityInCell(this, _currentPos);
_currentPos += _facingDir;
GET_SINGLETON(MapManager)->registerEntityInCell(this, _currentPos);
}
움직이기 전에 본인이 있는 Cell에서 탈출 하고 움직인 뒤에 다시 본인 위치에 맞는 Cell 들어가는거 빼먹어선 안됩니다.
움직이는건 됐으니 이제 InGameScene에서 돌려줄 enemyMove함수를 만들어주고,
//InGameScene.h
public:
void enemyMove();
구현 해준 뒤 InGameScene의 update에서 호출해주면되겠죠.
void InGameScene::update()
{
enemyMove();
}
void InGameScene::enemyMove()
{
for (auto& i : GET_SINGLETON(EntityManager)->getEnemies())
{
i->tryMove();
}
}
Entity 렌더링
적을 움직이는걸 보기 위해 Entity 렌더링을 해줍시다.
맵보다 앞에 보여야 하기 때문에 맵 먼저 렌더링 한 뒤에 Entity를 그려주겠습니다.
먼저 가독성을 높여주기 위해 함수로 묶겠습니다.
//InGameScene.h
public:
void mapRender();
void entityRender(const Vector2& pos);
그 뒤에 맵을 렌더링 하는 부분을 mapRender 함수 내로 옮겨 주고나서 맵 한칸을 렌더링 한 뒤에 바로 따라서 Entity를 렌더링 해주겠습니다.
void InGameScene::mapRender()
{
gotoxy(30, 6);
for (int i = 0; i < MAP_HEIGHT; i++)
{
for (int j = 0; j < MAP_WIDTH; j++)
{
Vector2 pos = Vector2(j, i);
Cell* cell = GET_SINGLETON(MapManager)->getCell(pos);
setColor((int)cell->charColor, (int)cell->bgColor);
entityRender(Vector2(j, i));
}
gotoxy(30, 7 + i);
}
setColor((int)COLOR::WHITE, (int)COLOR::BLACK);
}
왜 이렇게 하냐면 맵을 모두 렌더링 한뒤에 적을 렌더링 하면 배경색을 보존할 수가 없어서 그렇습니다.
글자색만 설정하거나 배경색만 설정할 수는 없거든요.
따로 배경색을 저장해놓으면 되겠지만 별로 내키진 않습니다.
void InGameScene::entityRender(const Vector2& pos)
{
cout << GET_SINGLETON(MapManager)->getCell(pos)->renderString;
}
적은 그냥 renderString을 가져와서 출력만 하면 됩니다. 색은 함수 호출 전에 정해뒀거든요.
그러면 소환 했을때 이런식으로 잘 보이게 됩니다.
Enemy Rotate
Gif를 봤을때 문제가 하나 있죠.
바로 적이 벽을 뚫습니다.
ROAD_TYPE
당연하게도 진행 방향을 돌려주지 않아서 그렇기 때문인데 이걸 해결하려면 조금 구조를 바꿔야 합니다.
왜냐하면 제가 원하는 적의 진행방향이 2갈래로 나뉘기 때문이죠.
말이 두갈래지 그냥 칸이 두개입니다.
아무튼 이렇게 이동을 하려면 내가 가려는 방향의 칸이 내 길인지, 벽은 아닌지 판단해줄 필요성이 있습니다.
근데 지금 Cell클래스는 그냥 본인이 무엇인지만 정의해놓았고 어떠한 길인지는 정의해두지 않았으니 enum 하나를 만들어주겠습니다.
//Type.h
enum class ROAD_TYPE
{
FIRST,
SECOND,
NONE
};
그리고 Cell의 생성자를 수정해서 ROAD_TYPE또한 생성할때 정해주도록 합시다.
NONE이 있는 이유도 길이 아닌 Cell이 있기 때문이죠.
//Cell.h
Cell(COLOR col, string str, MAP_TYPE type, ROAD_TYPE roadType) : bgColor{ col }, renderString{ str }, type{ type }, roadType{ roadType } {};
그리고 Map.txt를 뜯어서 길마다 차이를 준 뒤에
적절하게 ROAD_TYPE을 넣어주면 됩니다.
...솔직히 이 구조 맘에 안들어요. 각자의 셀이 모두 ROAD_TYPE을 가져야 하는 구조는 별로지만 솔직히 이거 아니면 거의 불가능 합니다. MAP_TYPE에 병합 해버릴까 고민해봤지만 그러면 또 MAP_TYPE::ROAD가 2개(ROAD_1, ROAD_2 라던가...)로 나뉘기 때문에 길만을 범위로한 무언가를 하기 어려워집니다.
그래서 그냥 이렇게 했죠.
Rotate
이제 맵 구조를 바꿨으니 적을 회전시키겠습니다.
개쩌는 알고리즘은 없고 4방향 돌아가면서 하나하나 판단해보고 올바른 길이라고 판단되면 그곳으로 회전하는 초라한 알고리즘입니다.
먼저 Enemy를 수정해서
내가 따라가야하는 길인 _roadType,
회전을 시도할 tryRotate(),
가려는 방향이 올바른 방향인지 판단할 isOnRoad(),
들어온 방향으로 회전하는 rotate()
를 만들어줍니다.
//Enemy.h
ROAD_TYPE _roadType;
void tryRotate();
bool isOnRoad(Vector2 dir);
void rotate(Vector2 dir);
어떠한 방식으로 회전하는지를 대충 설명하자면...
Goblin이라는 Enemy의 _facingDir이 1,0입니다. 이러면 이동 방향이 (1, 0)이라는 뜻이고 오른쪽으로 움직이겠죠.
Goblin이 가지고 있는 ROAD_TYPE이 FIRST라고 했을때 만약 이동방향에 있는 Cell의 ROAD_TYPE이 SECOND혹은 NONE이라면...
회전을 해주는겁니다.
현재 보고있는 방향 기준 뒷방향 및 보고있는 방향을 제외하고 전부 확인해본 뒤에 본인과 똑같은 ROAD_TYPE이 있는 곳으로요.
뒷방향을 제외하는 이유는 역행을 막기 위함입니다. 제외 안하면 비교 순서에 따라 뒤로 갈수도 있거든요.
보고있는 방향은 굳이 연산할 필요가 없기 때문에 제외입니다.
isOnRoad부터 구현해보죠.
//Enemy.cpp
bool Enemy::isOnRoad(Vector2 dir)
{
return GET_SINGLETON(MapManager)->
getCell(_currentPos + dir)->roadType == _roadType;
}
쉽죠? 현재 위치에서 들어온 방향을 더했을때 나온 Cell의 roadType이 내 _roadType과 같은지 판단합니다.
rotate또한 쉬운편입니다. _facingDir만 바꾸면 알아서 회전하거든요.
//Enemy.cpp
void Enemy::rotate(Vector2 dir)
{
_facingDir = dir;
}
tryRotate를 볼까요.
//Enemy.cpp
void Enemy::tryRotate()
{
if (!isOnRoad(_facingDir))
{
for (int i = 0; i < 4; i++)
{
Vector2 dir = Direction::fourDirection[i];
if (_facingDir == dir || _facingDir * -1 == dir)
continue;
if (isOnRoad(dir))
{
rotate(dir);
break;
}
}
}
}
먼저 보고있는 방향의 Cell이 내가 가는 길과 동일한지 판단하고 만약 동일하지 않다면 회전해야할 케이스이니
미리 for문에서 돌리기 쉽게 만들어둔 Direction 클래스에서 fourDirection을 받아와서 하나하나 isOnRoad에 넣어서 판단해줍니다.
이 과정에서 보고있는 방향과 뒷방향은 연산에서 제외합니다. 이유는 위에서 설명했죠?
그리고 만약 적합하다면 그 방향으로 회전해줍니다.
쉽죠?
참고로 Direction 클래스는 요래 생겼슴둥.
//Direction.h
struct Direction
{
static const Vector2 fourDirection[4];
};
//Direction.cpp
const Vector2 Direction::fourDirection[4] =
{
{0, 1},
{0, -1},
{1, 0},
{-1, 0}
};
그리고 이걸 이동할때마다 실행해주면 끝입니다.
//Enemy.cpp
void Enemy::tryMove()
{
_moveTimer = clock();
if (_moveTimer - _lastMoveTime > _moveTime)
{
move();
tryRotate();
_lastMoveTime = _moveTimer;
}
}
EntityManager
Enemy를 스폰할때 길을 정해주도록 합시다.
먼저 Enemy에 길을 정해주는 함수를 넣어주고
//Enemy.h
void setRoad(ROAD_TYPE type);
Enemy를 소환할때 RoadType을 받도록 해준 뒤에
//EntityManager.h
Enemy* spawnEntity(ENEMY_TYPE type, const Vector2& pos, ROAD_TYPE road);
소환한 enemy에 setRoad를 해줍니다.
//EntityManager::SpawnEntity()
enemy->setRoad(road);
그리고 spawnPos를 정해주면 됩니다.
spawnPos = road == ROAD_TYPE::FIRST ? spawnPos : spawnPos + Vector2(0, 1);
Second인데 왜 +나면 콘솔창에선 Y축이 늘어날수록 아래로 가기 때문입니다.
...생각해보니 그러면 위에 그림판은 표기 오류군요. 일단 넘어가죠. 보기는 저게 더 편안해요.
저렇게 spawnPos를 맘대로 수정해주는 이유는 define을 사용해서 소환할 위치를 정해주기 떄문입니다.
ENEMY_SPAWNPOS를 노가다를 통해 찾아준 뒤 기록해주고
//Define.h
#define ENEMY_SPAWNPOS Vector2(0,9)
pos자리에 ENEMY_SPAWNPOS만 넣어준 뒤에 ROAD_TYPE을 정해주면...
//InGameScene::Init()
GET_SINGLETON(EntityManager)->spawnEntity(ENEMY_TYPE::GOBLIN, ENEMY_SPAWNPOS, ROAD_TYPE::FIRST);
GET_SINGLETON(EntityManager)->spawnEntity(ENEMY_TYPE::GOBLIN, ENEMY_SPAWNPOS, ROAD_TYPE::SECOND);
잘 작동하죠?
마치는 말
엔진 갠프가 망했습니다. 조만간 망한 이유 글로 찾아뵙죠.
'개발 > Default Defense' 카테고리의 다른 글
Default Defense | 다섯번째 프로젝트 # 7 : 구조 수정 (0) | 2024.06.30 |
---|---|
Default Defense | 다섯번째 프로젝트 # 6 : Ally Attack (0) | 2024.06.29 |
Default Defense | 다섯번째 프로젝트 # 4 : Entity (2) | 2024.06.15 |
Default Defense | 다섯번째 프로젝트 # 3 : 맵 (2) | 2024.06.15 |
Default Defense | 다섯번째 프로젝트 # 2 : Transition (0) | 2024.06.14 |