1. 오브젝트 (언리얼의 액터)
플레이어, 몬스터, 투사체 등 Scene 상에 배치되는 모든 물체들.
2. Object.h (부모 클래스)
#pragma once
enum class ObjectType
{
None,
Player,
Monster,
Projectile,
};
class Object
{
public:
Object();
Object(ObjectType type);
virtual ~Object();
virtual void Init() abstract;
virtual void Update() abstract;
virtual void Render(HDC hdc) abstract;
public:
ObjectType GetObjectType() { return _type; }
Pos GetPos() { return _pos; }
void SetPos(Pos pos) { _pos = pos; }
protected:
ObjectType _type = ObjectType::None;
Stat _stat = {};
Pos _pos = {};
};
플레이어, 몬스터, 투사체의 세가지 enum 값을 활용한다.
물체의 위치를 관리하기 위한 Get, SetPos 함수와 어떤 Object인지 판단할 수 있는 GetObjectType 함수를 구현한다.
1) Types.h - Stat, Pos
객체의 체력, 최대 체력, 이동속도 등 능력치가 들어 있는 Stat 구조체를 Types.h에 구현한다.
struct Stat
{
int32 hp = 0;
int32 maxHp = 0;
float speed = 0;
};
struct Pos
{
float x = 0;
float y = 0;
};
3. Player.h, Player.cpp
#pragma once
#include "Object.h"
class Player : public Object
{
public:
Player();
virtual ~Player() override;
virtual void Init() override;
virtual void Update() override;
virtual void Render(HDC hdc) override;
};
#include "pch.h"
#include "Player.h"
#include "InputManager.h"
#include "TimeManager.h"
#include "Utils.h"
#include "Missile.h"
Player::Player() : Object(ObjectType::Player)
{
}
Player::~Player()
{
}
void Player::Init()
{
// 데이터 시트를 참고하여 대입
_stat.hp = 100;
_stat.maxHp = 100;
_stat.speed = 500;
_pos.x = 400;
_pos.y = 500;
}
void Player::Update()
{
float deltaTime = GET_SINGLE(TimeManager)->GetDeltaTime();
//거리 = 시간 * 속도
if (GET_SINGLE(InputManager)->GetButton(KeyType::A))
{
_pos.x -= _stat.speed * deltaTime;
}
if (GET_SINGLE(InputManager)->GetButton(KeyType::D))
{
_pos.x += _stat.speed * deltaTime;
}
if (GET_SINGLE(InputManager)->GetButton(KeyType::W))
{
_pos.y -= _stat.speed * deltaTime;
}
if (GET_SINGLE(InputManager)->GetButton(KeyType::S))
{
_pos.y += _stat.speed * deltaTime;
}
if (GET_SINGLE(InputManager)->GetButtonDown(KeyType::SpaceBar))
{
// TODO : 미사일 발사
// 투사체를 발생시켜야 함
}
}
void Player::Render(HDC hdc)
{
Utils::DrawCircle(hdc, _pos, 50);
}
이전에 DevScene에서 들고 있던 플레이어의 이동 조작 코드를 Player.cpp로 옮겨온다.
위에 설정된 스텟은 임의의 값으로 저장해준다.
4. Missile.h, Missile.cpp
#pragma once
#include "Object.h"
class Missile : public Object
{
public:
Missile();
virtual ~Missile() override;
virtual void Init() override;
virtual void Update() override;
virtual void Render(HDC hdc) override;
};
#include "pch.h"
#include "Missile.h"
#include "Utils.h"
#include "InputManager.h"
#include "TimeManager.h"
Missile::Missile() : Object(ObjectType::Projectile)
{
}
Missile::~Missile()
{
}
void Missile::Init()
{
_stat.hp = 1;
_stat.maxHp = 1;
_stat.speed = 600;
}
void Missile::Update()
{
float deltaTime = GET_SINGLE(TimeManager)->GetDeltaTime();
_pos.y -= deltaTime * _stat.speed;
//충돌
// TODO: 공격 판정
}
void Missile::Render(HDC hdc)
{
Utils::DrawCircle(hdc, _pos, 25);
}
Player와 Monster가 발사하는 Missile 클래스이다.
그렇다면 이 미사일은 어디에서 관리되어야 할까?
1) Object 들을 어떤 타입으로 들고 있을 것인가? - 컨테이너(벡터, 리스트, 맵, 해쉬맵 등) 선택
각 컨테이너의 장단점을 비교하여 선택한다.
간단한 싱글 게임이고 Object가 많이 등장하지 않으므로 임의 접근이 가능한 벡터를 선택하겠다.
2) Object를 어디에서 들고 있을 것인가? - 오브젝트 매니저 or Scene
언리얼, 유니티에서는 Scene 에 물체를 배치하기 때문에 보통 Scene에 저장한다.
특정 물체를 빠르게 찾아서 상호작용하게 해야하는 온라인 게임이면 오브젝트 매니저로 관리하는 것이 더 편할 수도 있다.
여기에서는 오브젝트 매니저 방법으로 연습한다.
5. 미사일 object 생성
1) ObjectManager
Player.cpp에서 스페이스 바를 누르면 미사일이 발사되는 구조이다.
이 때 모든 Object를 매니저로 관리하기로 했기 때문에 ObjectManager를 생성한다.
#pragma once
#include "Object.h"
// Game 소멸자에서 소멸
class ObjectManager
{
DECLARE_SINGLE(ObjectManager);
~ObjectManager();
template<typename T>
T* CreateObject()
{
T* object = new T();
object->Init();
return object;
}
private:
// 인벤토리와 유사하게 Object를 들고 있을 것임
vector<Object*> _objects;
};
그리고 발사하는 미사일은 Player.cpp에서 생성시키게 하겠다.
if (GET_SINGLE(InputManager)->GetButtonDown(KeyType::SpaceBar))
{
// TODO : 미사일 발사
// 투사체를 발생시켜야 함
Missile* missile = GET_SINGLE(ObjectManager)->CreateObject<Missile>();
}
}
* 맨 위에 "ObjectManager.h" 헤더를 추가하는 것을 잊지 않도록 한다.
Missile* missile = GET_SINGLE(ObjectManager)->CreateObject<???>();
위의 CreateObject에서 알맞은 타입으로 형변환이 가능한지 체크하기 위해 추가 코드를 작성하겠다.
(???에 들어가는 것은 Init 함수를 가진 Object여야 한다는 조건을 추가)
template<typename T>
T* CreateObject()
{
// Type Trait
static_assert(std::is_convertible_v<T*, Object*>);
//~~
???에 입력된 클래스 / 구조체 등이 Object 포인터 타입인지 혹은 Object를 상속하고 있는지 확인하는 함수를 사용하였다.
Missile* missile = GET_SINGLE(ObjectManager)->CreateObject<Missile>();
2) ObjectManager.h, ObjectManager.cpp
Object들을 벡터로 관리하기로 했기 때문에, Add, Remove, Clear 함수를 만들어 관리하도록 하겠다.
void Add(Object* object);
void Remove(Object* object);
void Clear();
const vector<Object*>& GetObjects() { return _objects; }
#include "pch.h"
#include "ObjectManager.h"
#include "Object.h"
ObjectManager::~ObjectManager()
{
Clear();
}
void ObjectManager::Add(Object* object)
{
if (object == nullptr)
return;
// 기존에 이미 추가되어 있는지 검색
auto findIt = std::find(_objects.begin(), _objects.end(), object);
if (findIt != _objects.end())
return;
_objects.push_back(object);
}
void ObjectManager::Remove(Object* object)
{
if (object == nullptr)
return;
auto removeIt = std::remove(_objects.begin(), _objects.end(), object);
_objects.erase(removeIt, _objects.end());
// 여기에서 delete를 할 것인가?
delete object;
}
void ObjectManager::Clear()
{
std::for_each(_objects.begin(), _objects.end(), [=](Object* ob) { delete ob; });
_objects.clear();
}
std::for_each 함수를 사용하여 벡터의 처음부터 끝까지 순회하여 각 object를 제거한다.
_objects 벡터는 각 object들의 포인터를 요소로 들고 있다.
이 요소들을 제거하기 위해서 값에 직접 접근할 수 있도록 [=](복사 캡처 블록)을 사용한 람다 문법을 사용한다.
** 벡터 내의 요소를 제거할 때 주의사항
1. std::remove는 지우고자 하는 데이터를 직접 제거하지 않는다.
실제로는 제거하려는 요소를 뒤로 옮겨 빈 공간을 채운다.
즉, 가짜로 제거된 상태이다.
2. std::remove는 반복자(Iterator)를 반환한다.
지우고자 한 데이터의 위치를 가리키는 반복자를 리턴한다.
3. 데이터를 확실하게 제거하려면 std::remove 이후에 vector::erase를 사용한다.
vector::erase를 사용하면 데이터를 제거한 이후에 벡터의 size를 조절한다.
반복자를 매개변수로 받는다.
3) GameScene.cpp
#include "pch.h"
#include "GameScene.h"
#include "Monster.h"
#include "Player.h"
#include "ObjectManager.h"
GameScene::GameScene()
{
}
GameScene::~GameScene()
{
}
void GameScene::Init()
{
//_player = new Player();
//_player->Init();
Player* player = GET_SINGLE(ObjectManager)->CreateObject<Player>();
player->SetPos({400, 400});
GET_SINGLE(ObjectManager)->Add(player);
}
void GameScene::Update()
{
//if (_player)
// _player->Update();
const vector<Object*>& objects = GET_SINGLE(ObjectManager)->GetObjects();
for (Object* object : objects)
object->Update();
}
void GameScene::Render(HDC hdc)
{
//if (_player)
// _player->Render(hdc);
const vector<Object*>& objects = GET_SINGLE(ObjectManager)->GetObjects();
for (Object* object : objects)
object->Render(hdc);
}
player를 하드 코딩 하는 것이 아니라 ObjectManager로 관리하게했다.
이제 Player.cpp에서 미사일을 생성하는 것 또한 ObjectManager의 인스턴스로 가능하다.
4) Player.cpp
if (GET_SINGLE(InputManager)->GetButtonDown(KeyType::SpaceBar))
{
// TODO : 미사일 발사
// 투사체를 발생시켜야 함
Missile* missile = GET_SINGLE(ObjectManager)->CreateObject<Missile>();
missile->SetPos(_pos);
GET_SINGLE(ObjectManager)->Add(missile);
}
투사체를 Player의 위치에서 나가는 것으로 설정한다.
_objects 벡터에 추가하는 코드도 잊지 않도록 한다.
마지막으로 Game.cpp에서 SceneType을 GameScene으로 변경한다.
GET_SINGLE(SceneManager)->ChangeScene(SceneType::GameScene);
프로그램을 실행하고 미사일을 발사하면 이어서 크래쉬가 발생한다.
'Windows API & 게임 수학' 카테고리의 다른 글
1-8. 리소스 제작 툴 만들기 (Resources, EditScene) (0) | 2023.10.17 |
---|---|
1-7. 오브젝트(Object) 설계 (2) - 디버깅, 몬스터, 피격 판정 (0) | 2023.10.17 |
1-5. 더블 버퍼링 (Double Buffering) (0) | 2023.10.17 |
1-4. 게임을 위한 프레임워크 제작하기(3) - Scene, SceneManager (1) | 2023.10.16 |
1-3. 게임을 위한 프레임워크 제작하기(2) - TimeManager, InputManager (1) | 2023.10.16 |