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);

 

프로그램을 실행하고 미사일을 발사하면 이어서 크래쉬가 발생한다.