https://github.com/hki2345/MetalSlug
1. 소개
- 횡스크롤 슈팅게임
- 스테이지 클리어 방식
- 피봇 시스템
- 바닥 충돌 시스템
- 2D 도트게임
- FWFont, FMOD 라이브러리
2. 게임 방법
적 처치로 맵 뚫기 |
최종 보스 잡기 + 점수 집계 |
3. 구현
3.1 바닥 충돌
인게임 맵 |
픽셀 충돌 구현 |
하늘색 픽셀 바닥을 깔아 캐릭터가 픽셀단위로 바닥을 검증할 수 있게 구현
같은 방식으로 초록색은 옆 벽면을 검증
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | void TheOne::Update_Bottom() { if (m_v_Cur_JumpForce > .0f) { return; } // 플레이서부터 측정한다. m_Pos_Bottom_Check = {(int) m_Pos.X, (int)(m_Pos.Y + (PartYSize - m_f_Bottom_Space)) }; ; m_b_BottomCheck = false; // 바닥 타일 밟으면 서있기 if (L"Bottom" == m_Coll_Name && nullptr != p_BottomColl) { m_b_BottomCheck = true; m_Pos_Bottom_Check.Y = (int)(p_BottomColl->MotherOfTheOne_DE()->pos().Y - 15.0f); Update_BottomLine(); return; } while (true) { // 그래도 탈출은 해야지 if (m_Pos_Bottom_Check.Y > 10000) { // 아니 탈출하지말고 죽여벼리기....;; ; // 왜냐.. 10000 넘어간 객체는 보이지 않을 테니까...그게 플레이어든 뭐든 Death(true); } m_v_YColor = RESOURCEMANAGER.Get_PixelColor(m_MotherOfTheOne->acting_map(), m_Pos_Bottom_Check); // 내가 쓰는 색상 0, 255, 255 -> 16776960 if (16776960 != m_v_YColor) { m_Pos_Bottom_Check.Y++; // 필요없는 연산 없게 해 프레임 늘림 if(m_Pos_Bottom_Check.Y > m_Pos.Y + PartYSize + m_f_Bottom_Space) { return; } continue; } else if (16776960 == RESOURCEMANAGER.Get_PixelColor(m_MotherOfTheOne->acting_map(), m_Pos_Bottom_Check)) { m_b_BottomCheck = true; break; } } Update_BottomLine(); } | cs |
위 코드는 플레이어 코드 내에서 픽셀 색상을 가져오는 ResourceManager (싱글톤) 내 함수를 호출한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 검사하고 싶은 이미지를 찾고 검사하는 함수 COLORREF ResourceManager::Get_PixelColor(const WCHAR* _Name, Position _Pos) { Image* pFindImage = Find_Res_Image(_Name); KAssert(nullptr == pFindImage); if (nullptr == pFindImage) { return 0; } return pFindImage->Get_PixelColor(_Pos); } | cs |
핵심함수는 ResourceManager 내에 Get_PixelColor()이며, 내부에서는 GetPixel을 쓴다.
https://learn.microsoft.com/ko-kr/windows/win32/api/wingdi/nf-wingdi-getpixel
해당 함수로 바닥이 하늘색이면, 캐릭터가 떨어지지 않는 것이다. 단, 이러면 사용자에게 보여지는 맵이미지와 충돌처리를 검증하는 충돌이미지 총 두 장이 필요한다. 이중에 랜더링을 맵이미지만 한다.
3.2 오브젝트 설치
핀 시스템: 핀을 기준으로 게임을 진행합니다.
에디터에서는 핀을 박는다.(MFC라 에디터 창은 일반 윈도우 창이다.)
핀의 역할은... 2D 고전게임을 하다보면 알겠지만, 캐릭터가 화면 안에 갇힌 경우가 있다.
이때 특정 이벤트(적 처치 및 오브젝트 작용)를 처리해야 카메라가 이동하면서 캐릭터가 맵의 다음 부분을 활보할 수 있게 된다. 이 역할 해주는 것이 핀 시스템이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | std::vector<Pinset_Renderer::PinsetData> Pinset_Renderer::GetAllPinsetData() { std::vector<Pinset_Renderer::PinsetData> ReturnData; m_StartIter = m_PinsetMap.begin(); m_EndIter = m_PinsetMap.end(); for (; m_StartIter != m_EndIter; m_StartIter++) { PinsetData New_EnemyData; New_EnemyData.m_EPos = (*m_StartIter).m_EPos; New_EnemyData.m_DPos = (*m_StartIter).m_DPos; New_EnemyData.m_Condition = (*m_StartIter).m_Condition; ReturnData.push_back(New_EnemyData); } return ReturnData; } | cs |
내에 GetAllPinsetData() 가 핵심함수이다.
내부에서는 핀의 위치, 그리고 기준이 되는 캐릭터가 핀셋 대비 위, 아래, 왼쪽, 오른쪽에 있는지, 그리고 부가 조건을 처리하여 데이터로 들고 있다.
들고 있는 조건을 유저가 만족할 경우 화면은 다음 핀셋으로 이동한다.
3.3 충돌 - 중력
일부 투사체(폭탄) |
유저의 시체 |
총알을 제외한 포탄이나 미사일 등이 중력의 영향을 받는다.
캐릭터가 점프하거나 아웃될 경우 시체로 변하면, 중력의 영향을 받는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void TheOne::Update_Gravity() { Update_Bottom(); // 바닥 도착 - 안함 if(false == m_b_BottomCheck || true == m_b_JumpCheck) { // 아래는 프레임을 5~60 주고 중력을 적용시킨다. // 주석된 건 인위적인 중력 - 실제 중력공식을 넣었다.-> 더욱 부드러움 ㅇㅇ 그리고 간단 m_Pos.Y -= m_v_Cur_JumpForce * DELTATIME; m_v_Cur_JumpForce -= m_v_Gravity * DELTATIME * 250; if (m_v_Cur_JumpForce < .0f) { m_Status_Obj = Obj_Down; } else if (m_v_Cur_JumpForce >= .0f) { m_Status_Obj = Obj_Up; } } } | cs |
대충 보면 알겠지만 실제 중력가속도(9.80665㎨)가 적용되지는 않는다.
최대한 원작의 느낌이 날 수 있도록 마법의 숫자를 찾아 넣은 것이다.
중력 및 물리 관련해서도 필자는 여러 테스트를 해보았다.
https://www.youtube.com/watch?v=H3lFzS3PzxY
안타깝게도 위 영상의 코드는 찾아봐도 안 나온다.
여튼, 정리하자면 읽었던 물리관련 서적을 추천한다.
https://www.yes24.com/Product/Goods/1431078
게임 물리 바이블
[ CD 1 ]David H. Eberly 저 / 유채곤, 차미리 공역 | 지앤선(志&嬋)
게임 개발자를 위한 물리
차량, 스포츠, 폭발, 모바일 등 게임 물리의 모든 것-
저자데이비드 버그
-
번역Pope Kim
-
출판한빛미디어
-
발행2015.01.05.
참고해보길 바란다. 참고로 난 똥망해서(ㅠㅠ) 다음 게임 개발부턴 걍 Box2D와 같은 물리엔진을 가져다 썼을 뿐이다.
물론, 가져다 쓴다 해도 위 서적을 읽고 안 읽고에 따라 함수를 분석하는 시각이 달라지긴 했다. ㅇㅇ...
위 두 서적은 모로가도 게임 개발에 도움이 되었다는 말을 하고 싶다.
3.4 랜더링 - 투사체
권총의 일점사 |
헤비머신건의 4연발 |
폭탄의 투사체 |
총알과 폭탄은 사출 각도와 상태에 따라 보이는 모양 즉, 랜더링이 변경되어야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | void Bullet_Heavy::Init_Render() { // 절대 값이 1보다 크면 - 범위 밖이란 듯 - 종료 if (1.0f < MATHMANAGER.Absolute_Num(m_pos_Dir.X) || 1.0f < MATHMANAGER.Absolute_Num(m_pos_Dir.Y)) { return; } Trans_Renderer* p_Render = Create_Renderer<Trans_Renderer>(10); MultiSprite* p_MultiData = RESOURCEMANAGER.Find_Res_MultiSprite(L"HeavyMachine.bmp"); if (Vector2f::Right == m_pos_Dir) { p_Render->Set_Sprite(p_MultiData->Get_Sprite(0)); } else if (Vector2f::Left == m_pos_Dir) { p_Render->Set_Sprite(p_MultiData->Get_Sprite(16)); } else if (Vector2f::Up == m_pos_Dir) { p_Render->Set_Sprite(p_MultiData->Get_Sprite(48)); } else if (Vector2f::Down == m_pos_Dir) { p_Render->Set_Sprite(p_MultiData->Get_Sprite(32)); } // 1사 분면 - 랜딩을 다 다르게 해야하기 때문에 일일이 나누어야한다. ㅇ으.ㅡ;ㅓㅣ;ㅁㄴㅇㄹ // 1사 분면 else if (0.0f < m_pos_Dir.X && 0.0f > m_pos_Dir.Y) { for (size_t i = 0; i < 15; i++) { if (Render_pos_Dir_Check(Vector2f::Right, Vector2f::Up, 15, i)) { p_Render->Set_Sprite(p_MultiData->Get_Sprite(17 + i)); break; } } // 안전 코드 - 영역 안에 없을 경우 - 직선 if (nullptr == p_Render->Get_Sprte()) { p_Render->Set_Sprite(p_MultiData->Get_Sprite(16)); } } // 2사 분면 else if (0.0f > m_pos_Dir.X && 0.0f > m_pos_Dir.Y) { for (size_t i = 0; i < 15; i++) { if (Render_pos_Dir_Check(Vector2f::Left, Vector2f::Up, 15, i)) { // 리소스 역순 저장됌 p_Render->Set_Sprite(p_MultiData->Get_Sprite(48 + (15 - i))); break; } } // 안전 코드 - 영역 안에 없을 경우 - 직선 if (nullptr == p_Render->Get_Sprte()) { p_Render->Set_Sprite(p_MultiData->Get_Sprite(48)); } } // 3사 분면 else if (0.0f > m_pos_Dir.X && 0.0f < m_pos_Dir.Y) { for (size_t i = 0; i < 15; i++) { if (Render_pos_Dir_Check(Vector2f::Left, Vector2f::Down, 15, i)) { // 리소스 역순 저장됌 p_Render->Set_Sprite(p_MultiData->Get_Sprite(32 + (15 - i))); break; } } // 안전 코드 - 영역 안에 없을 경우 - 직선 if (nullptr == p_Render->Get_Sprte()) { p_Render->Set_Sprite(p_MultiData->Get_Sprite(32)); } } // 4사 분면 else if (0.0f < m_pos_Dir.X && 0.0f < m_pos_Dir.Y) { for (size_t i = 0; i < 15; i++) { if (Render_pos_Dir_Check(Vector2f::Right, Vector2f::Down, 15, i)) { // 리소스 역순 저장됌 p_Render->Set_Sprite(p_MultiData->Get_Sprite(1 + i)); break; } } // 안전 코드 - 영역 안에 없을 경우 - 직선 if (nullptr == p_Render->Get_Sprte()) { p_Render->Set_Sprite(p_MultiData->Get_Sprite(0)); } } p_Render->Set_TransColor(RGB(0, 248, 0)); p_Render->Pivot({ 40, 0 }); } | cs |
위 코드는 헤비머신건 랜더링 코드다.
대부분의 총알의 랜더러는 Initialize 단계에서만 결정되면 된다.
특히 헤비머신건의 총알은 한 번 사출된 뒤 각도만 맞춰 도트이미지를 결정지으면 된다.
단, 차후 개발이 진행될 경우를 대비해 랜더링의 업데이트가 필요한 총알이 있을 수 있기에,
총알의 구현은 거의 비슷하더라도 기본, 헤비머신건, 플레임샷 등 상속으로 처리하여 구현한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void Weapon_Bomb::Update_BottomLine() { if (true == m_b_BottomCheck) { if (0 < m_i_JumpCount) { p_Render->speed(p_Render->speed() * 2.0f); m_i_JumpCount--; m_b_JumpCheck = false; Update_Force(m_v_Jump_Force); m_v_Jump_Force *= .75f; m_Run_Speed *= .75f; } else { Activate(); } } } | cs |
위 코드는 폭탄 랜더링 코드다.
랜더링 과정을 Animation 으로 밀어버렸다.
이는, 스프라이트를 미리 폭탄이 날아가는 것처럼 준비한 것이다.
단, 바닥에 부딪혔을 때, 데굴데굴 구르는 것을 구현하기 위해 애니메이션 속도를 더 빠르게 증가시켰다.
3.5 특별능력
에리 - 폭탄 멀리 던지기 |
레오나 - 문슬레셔 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #pragma once #include "One_Player.h" // class AniName // { // std::wstring BodyAni; // std::wstring LegAni; // } class Player_Leona : public One_Player { bool MoonSlasher; virtual void Init_Collision() override {} ; virtual void Init_Render() override; void Update_Key(); void Update_Set_AniRender(); public: void Init() override; void Update() override; void MoonUpdate(); public: Player_Leona(); ~Player_Leona(); }; | cs |
위 코드는 레오나의 헤더파일이다.
Player 클래스를 상속받아 다양한 캐릭터를 구현할 수 있도록 했다.
void MoonUpdate()가 구현되어 있다.
4. 정리
c++ 로 처음 만들어본 게임이다. 그래서 좋았다. 맨날 끌어다 쓰던 Unity API에서 줄코딩으로 게임을 작성하니 새로운 경험이었다. 덕분에 왜 엔진을 개발하고 API를 개발하는지 알 것 같다 ㅎㅎ;;
WinAPI만으로는 게임을 개발하기엔 한계가 있다. 특히, 이미지 회전에 많은 자원을 소모하다 보니 똑같은 객채여도 살짝 삐뚤어진 이미지가 필요하다. 그래서 많은 그래픽 작업이 필요했다.
비록 학원에서 배운 엔진이었지만, 게임 엔진에 대한 전반적인 환상이 깨졌던 시간이었던 것 같다. 프레임워크라는 것을 배웠고 나는 그 지식을 흡수하는 과정을 거쳤다. 생각보다 구현해야할 것이 많았다. Image, Timer, Stream 시스템 등등이 필요했다.
'WINAPI MFC' 카테고리의 다른 글
크레이지아케이드 [개인 제작, 모작, 로컬 서버] (0) | 2024.05.16 |
---|