개요
그동안 바쁜 상황에도 개발일지를 시간 잘 짬내서 적고 있었다고 생각했는데 다시 생각해보니 진짜 바쁘면 개발일지를 적지도 못해요.
그래서 이미 개발이 다 끝난 프로젝트 개발일지를 적는 우스운일을 하고 있는거겠죠.
사실 이때쯤 오면 회고록에 가깝긴 합니다.
그리고 블로그 카테고리를 조금 세분화 했습니다.
큰 카테고리를 세분화 한건 아니구요 개발 카테고리 내의 카테고리를 세분화 했습니다.
각각의 프로젝트마다 소개글을 하나씩은 쓸텐데 솔직히 카테고리를 각 프로젝트 단위로 나누는게 맞다고 생각했습니다.
그 외는... 아마 폐기한 프로젝트들의 개발일지가 비공개로 들어가 있을겁니다.
Wave
아이디어 정리
디펜스 게임, 그것도 타워 디펜스다보니까 적들이 몰려오는 웨이브라는 개념이 필요하겠죠.
최소한 50웨이브 정돈 있어야 디펜스 빌드업 하는 느낌이 들겠죠?
이렇게 큰 데이터를 코드 내에 저장할 수는 없으니 txt파일에 저장해둔 뒤에 가져오는게 낫습니다.
저의 능지로 '랜덤임과 동시에 합리적인 웨이브 시스템'따윈 구현 불가하기에 간단하게 한줄마다 웨이브의 정보를 간단하게
적이 나오는 수, 적이 나오는 간격으로 간단하게 구성하겠습니다.
이런식으로 적이 5마리 있으니 만약 고블린을 5마리, 오우거를 10마리 100ms 간격으로 소환하는 웨이브를 적는다면
5 0 10 0 0 100
으로 적으면 되는거죠.
구현
구현 자체는 쉬운편입니다.
일단 WaveInfo라는 구조체를 적겠습니다.
말 그대로 웨이브의 정보를 담고 있고 정보만 담고 있기때문에 클래스일 이유는 없죠.
//WaveInfo.h
struct WaveInfo
{
WaveInfo() : spawnDelay{ 0 } {};
map<ENEMY_TYPE, int> spawnEnemyMap;
int spawnDelay;
};
EntityManager에서 소환을 enum으로 하기 때문에 map에서 enum과 나올 숫자를 짝지어주었습니다.
그리고 적 소환 딜레이도 같이 저장해주었죠.
그 후 텍스트 파일을 양산해줍니다.
그리고 C++에서 저걸 띄어쓰기 기준으로 한줄씩 읽어오면 되는데 Split같은 함수가 C++엔 없기 때문에 비슷한 istringstream이란걸 사용해볼겁니다.
별로 어려울 건 없습니다. 조금 난해하긴한데 제가 알던 Split 함수처럼 이 istringstream이라는 놈도 띄어쓰기를 기준으로 나눠주는게 기본값이거든요.
그 전에 이걸 읽기도하고 웨이브 전반을 관리할 WaveManager를 적어줍니다.
//WaveManager.h
class WaveManager
{
DECLARE_SINGLETON(WaveManager)
public:
void init();
public:
int getCurrentWave() { return _currentWave; }
void initWave() { _currentWave = -1; }
void nextWave();
void spawnEnemy();
bool isSpawnEnd() { return _currentSpawnEnemy == ENEMY_TYPE::END; }
void reductWave() { _currentWave--; };
private:
vector<WaveInfo> _waveInfoVec;
ENEMY_TYPE _currentSpawnEnemy = ENEMY_TYPE::GOBLIN;
ROAD_TYPE _spawnRoad = ROAD_TYPE::FIRST;
int _leftSpawnEnemy = 0;
int _currentWave = -1;
int _spawnDelay = 800;
clock_t _lastSpawnTime = 0;
clock_t _spawnTimer = 0;
};
뭐가 많은데 실제로 하나하나 다 중요해서 뭘 걸러오기 좀 그랬습니다.
WaveInfo를 담는 벡터, 현재 소환중인 Enemy의 타입, 소환할 길, 현재 웨이브 등등...
스폰 딜레이를 구현하기 위한 clock_t 변수도 2개입니다.
아무튼 이제 읽어올까요.
//WaveInfo::init()
for (int i = 0; i < 50; i++)
{
WaveInfo info = WaveInfo();
string waveStr;
std::getline(waveRead, waveStr);
std::istringstream iss(waveStr);
string buffer;
vector<string> result;
ENEMY_TYPE type = ENEMY_TYPE::GOBLIN;
while (iss >> buffer)
{
if (type == ENEMY_TYPE::END)
{
info.spawnDelay = std::stoi(buffer);
continue;
}
info.spawnEnemyMap.insert(std::make_pair(type, std::stoi(buffer)));
type = (ENEMY_TYPE)((int)type + 1);
}
_waveInfoVec.push_back(info);
}
string변수 안에 한 줄을 읽어서 넣어주고 istringstream으로 띄어쓰기 기준으로 하나하나 잘라서 미리 만들어둔 WaveInfo 변수에 하나하나 묶어줍니다.
한번의 루프가 끝나면 다음 Enemy로 넘어가고 만약 마지막이라면 spawnDelay차례라는거니까 spawnDelay를 넣어주고 끝내면 됩니다.
그리고 웨이브를 넘기는걸 적어보죠.
//WaveManager.cpp
void WaveManager::nextWave()
{
_currentWave++;
if (_currentWave > LAST_WAVE)
{
GET_SINGLETON(SceneManager)->loadScene("EndScene");
return;
}
for (auto& ally : GET_SINGLETON(EntityManager)->getAllies())
{
ally->resetAttackCooltime();
}
_currentSpawnEnemy = ENEMY_TYPE::GOBLIN;
_leftSpawnEnemy = _waveInfoVec[_currentWave].spawnEnemyMap[_currentSpawnEnemy];
}
일단 현재 웨이브를 늘려주고 만약 웨이브가 끝났다면 결과씬으로 이동합니다.
아니라면 아군들을 받아와서 공격 쿨타임을 초기화 해줍니다. 이거 관련해선 나중에 할말이 있죠.
아무튼 현재 소환할 Enemy를 Goblin으로 초기화하고 스폰할 Enemy의 남은 수도 map에서 꺼내와줍니다.
//WaveManager.cpp
void WaveManager::spawnEnemy()
{
if (_currentSpawnEnemy == ENEMY_TYPE::END) return;
_spawnTimer = clock();
if (_lastSpawnTime + _waveInfoVec[_currentWave].spawnDelay < _spawnTimer)
{
_spawnRoad = _spawnRoad == ROAD_TYPE::FIRST ? ROAD_TYPE::SECOND : ROAD_TYPE::FIRST;
_lastSpawnTime = _spawnTimer;
_leftSpawnEnemy--;
if (_leftSpawnEnemy >= 0)
GET_SINGLETON(EntityManager)->spawnEntity(_currentSpawnEnemy, ENEMY_SPAWNPOS, _spawnRoad);
if (_leftSpawnEnemy <= 0)
{
_currentSpawnEnemy = (ENEMY_TYPE)((int)_currentSpawnEnemy + 1);
if (_currentSpawnEnemy == ENEMY_TYPE::END) return;
_leftSpawnEnemy = _waveInfoVec[_currentWave].spawnEnemyMap[_currentSpawnEnemy];
}
}
}
그리고 _leftSpawnEnemy를 하나하나 줄여가며 _currentSpawnEnemy를 소환해줍니다.
_leftSpawnEnemy가 0보다 같거나 작아진다면 그때 다음 Enemy로 넘어간 뒤에 소환할 남은 적의 수도 초기화 합니다.
길은 그냥 첫번째, 두번째 번갈아가면서 나가도록 해줍니다. 굳이 복잡하게 랜덤같은거 써봤자 억까만 늘어날 뿐이죠.
그리고 스폰 딜레이는... 말 안해도 하실거라고 믿습니다.
그동안 설명을 좀 많이해서 또 적기 조금 지치네요.
InGameState
아이디어 정리
웨이브 자체는 잘 나옵니다.
이제 인게임 구조를 탄탄하게 잡을 차례입니다.
인게임에서 해야할 행동은 아군 설치, 삭제, 전투 등이 있겠죠?
근데 이걸 하나의 씬에서 모두 해주려면 if else문이 불가피하지 않겠습니까?
그렇기 때문에 제가 아주 좋아하는 State 디자인 패턴을 사용하여 InGameState를 만들어주겠습니다.
기본적인 맵, UI 렌더링은 InGameScene에서 자체적으로 해주고 State에 따른 UI렌더링이나 키입력 같은건 각각의 State에서 해주는 식으로 구현하겠습니다.
구현
//InGameState.h
class InGameState
{
public:
virtual ~InGameState() {}
virtual void update() abstract;
virtual void render() abstract;
protected:
virtual KEY keyController();
protected:
InGameScene* _inGameScene;
};
각각 돌려줄 update, render이 필요하구요. 추상 클래스기 때문에 소멸자 가상함수화는 필수입니다.
그리고 모두 키입력을 사용하기 때문에 키 입력 받는 함수를 가상함수화 해놓고 InGameScene에 접근할 수 있도록 하기 위해 포인터 변수를 넣어주었습니다.
아, 참고로 키입력 함수는 이렇게 생겼습니다.
KEY InGameState::keyController()
{
if (_kbhit())
{
int key = _getch();
if (key == 0 || key == 224)
{
key = _getch();
return (KEY)key;
}
return (KEY)key;
}
return KEY::FAIL;
}
간단하죠?
GetAsync... 어쩌구... 비동기 입력 함수를 안쓴 이유는 그건 입력 받는게 한 프레임단위라서 너무 빨라요.
Sleep을 사용하거나 bool 변수로 flag를 세우면 되기야 하겠지만 그렇게 하기 너무 귀찮아서 그냥 conio.h 헤더에 있는 _kbhit, _getch를 사용했습니다.
아무튼 이제 InGameScene에서 각각의 State를 관리해줘야겠죠.
private:
InGameState* _currentState;
std::map<INGAMESCENE_STATE, InGameState*> _stateMap;
어디서 많이 보지 않았습니까?
맞아요! 바로 SceneManager에서 보던 구조죠.
구현도 비슷하게 했다고 생각하시면 됩니다.
Default Defense | 다섯번째 프로젝트 # 1 : Scene 구조, 싱글톤
개요글을 시작하기 전에 간단하게 할말을 적고 시작하곤 했는데 지금은 별로 적을게 없습니다.바로 가죠/ 구조 구상 게임의 전체적인 구조는 간단하...진 않지만 Unity처럼 Scene을 나누는 구
sundg0162.tistory.com
이렇게 열심히 State를 만들어서
State를 변경하는 함수도 만들어주고
//InGameScene.cpp
void InGameScene::changeState(INGAMESCENE_STATE state)
{
system("cls");
_currentState = _stateMap[state];
}
update와 render에서 현재 State의 update와 render를 돌려주면 됩니다.
void InGameScene::update()
{
_currentState->update();
}
void InGameScene::render()
{
mapRender();
uiRender();
_currentState->render();
}
끝!
마치는 말
아군 설치할때 사용한 마우스 입력은 스킵하겠습니다.
어짜피 Handle넣었다 뺐다 Input Event타입 구분하는게 전부에요.
DefaultDefense/DefenceGame/DefenceGame/console.cpp at main · SundG0162/DefaultDefense
2024학년도 1학기 게임 프로그래밍 팀 프로젝트 산출물. Contribute to SundG0162/DefaultDefense development by creating an account on GitHub.
github.com
이러면 게임플로우가 끝났습니다. 다음 글도 개발일지긴 합니다. 근데 조금 쉬어가는 느낌? 그게 Default Defense의 마지막 개발일지입니다.
'개발 > Default Defense' 카테고리의 다른 글
C++ 콘솔에서 Bad Apple!! 출력하기 | Default Defense # 9 (1) | 2024.07.08 |
---|---|
Default Defense | 다섯번째 프로젝트 # 7 : 구조 수정 (0) | 2024.06.30 |
Default Defense | 다섯번째 프로젝트 # 6 : Ally Attack (0) | 2024.06.29 |
Default Defense | 다섯번째 프로젝트 # 5 : Enemy Movement (1) | 2024.06.21 |
Default Defense | 다섯번째 프로젝트 # 4 : Entity (2) | 2024.06.15 |