2월 7일 수업 정리
오늘은 Scene과 SceneMgr, 그리고 GameObject를 만들것이다.
셋은 서로 밀접하게 연관되어 작동하니 정리를 잘 해야한다.
가장 먼저 씬, 즉 장면을 가지고 있는 클래스를 만들자.
#pragma once
class GameObject;
class Scene
{
protected:
SceneIds id;
std::list<GameObject*> gameObjects;
public:
Scene(SceneIds id);
virtual ~Scene() = default;
virtual void Init() = 0;
virtual void Release() = 0;
virtual void Enter() = 0;
virtual void Exit() = 0;
virtual void Update(float dt);
virtual void Draw(sf::RenderWindow& window);
virtual GameObject* FindGo(const std::string& name);
virtual int FindGoAll(const std::string& name, std::list<GameObject*>& list);
virtual GameObject* AddGo(GameObject* obj);
virtual void RemoveGo(GameObject* obj);
Scene(const Scene&) = delete;
Scene(Scene&&) = delete;
Scene& operator=(const Scene&) = delete;
Scene& operator=(Scene&&) = delete;
};
먼저 게임 오브젝트 클래스를 선언해두고,
씬이 가져야 할 멤버, 몇번째 씬일지 표현하는 id와
해당 씬이 가지고 있는 게임오브젝트들을 저장하는 리스트 gameobjects 를 만든다.
생성자,소멸자는 넘어가고
Init은 해당 씬의 초기화, Release는 삭제, Enter는 해당 씬 활성화, Exit는 비활성화란 뜻이다. 전부 상속받아 실제 씬의 역할을 할 자식클래스 씬에서 구현해야 하니 순수 가상함수로 만들자.
Update는 이벤트 발생시 변경되는 점을, Draw는 모든 오브젝트들을 화면상에 출력하는 것을 말한다. 또한 FindGo는 해당 오브젝트가 씬에 있는지 검색을, FindGoAll은 새로운 리스트를 만들어 모든 오브젝트들을 검사해 집어넣고 몇개가 있는지 알려준다,
AddGo는 리스트 gameobjects에 오브젝트를 등록하고, remove는 지워주는 함수다.
그 아래는 복사,대입 생성자이며 전부 지워뒀다.
우선 씬 아이디를 열거형으로 Defines에 등록해두자.
enum class SceneIds
{
None = -1, SceneDev1, SceneDev2, Count // 0 1 2 이니까 카운트는 씬의 개수
};
없을때 -1, 이후 0 1 2 식으로 진행되며 count는 마지막 번호이니 몇개의 씬이 있는지 알려주는 용도이다.
하나하나 구현해보자.
우선 업데이트 메소드.
void Scene::Update(float dt)
{
for (auto& obj : gameObjects)
{
if (obj->GetActive())
{
obj->Update(dt);
}
}
}
씬이 가지고있는 리스트의 오브젝트들을 순회하며 모든 오브젝트들의 변경점을 업데이트해준다.
드로우
void Scene::Draw(sf::RenderWindow& window)
{
for (auto& obj : gameObjects)
{
if (obj->GetActive())
{
obj->Draw(window);
}
}
}
마찬가지로 순회하며 전부 출력해준다.
findgo
GameObject* Scene::FindGo(const std::string& name)
{
for (auto obj : gameObjects)
{
if (obj->name == name)
{
return obj;
}
}
return nullptr;
}
게임오브젝트를 이름을 붙여 저장하기 때문에, 이름을 가지고 씬에서 찾아낸다.
findgoall
int Scene::FindGoAll(const std::string& name, std::list<GameObject*>& list)
{
list.clear();
for (auto obj : gameObjects)
{
if (obj->name == name)
{
list.push_back(obj);
}
}
return list.size();
}
새로운 리스트를 만들어서, push_back 메소드를 통해 뒤에서부터 하나씩 넣어서 총 검색한 오브젝트가 몇개가 나오는지 알려준다.
addgo
GameObject* Scene::AddGo(GameObject* obj)
{
if (std::find(gameObjects.begin(), gameObjects.end(), obj) == gameObjects.end())
{
gameObjects.push_back(obj);
return obj;
}
return nullptr;
}
리스트를 순회해서 없으면 추가하고, 있으면 널포인터를 반환한다
removego
void Scene::RemoveGo(GameObject* obj)
{
gameObjects.remove(obj);
}
해당 오브젝트를 삭제한다.
씬들을 관리하는 씬 매니저이다. 주로 씬들을 벡터에 등록하고, 시작 씬과 현재 씬을 저장하며 씬을 변경하는 멤버메소드를 가지고있다.
#pragma once
#include "singleton.h"
class scene;
class SceneMgr : public Singleton<SceneMgr>
{
protected:
std::vector<Scene*> scenes;
SceneIds startScene = SceneIds::SceneDev1;
SceneIds currentScene = startScene;
public:
SceneMgr() = default;
virtual ~SceneMgr();
void Init();
void Release();
void ChangeScene(SceneIds id);
void Update(float dt);
void Draw(sf::RenderWindow& window);
SceneMgr(const SceneMgr&) = delete;
SceneMgr(SceneMgr&&) = delete;
SceneMgr& operator=(const SceneMgr&) = delete;
SceneMgr& operator=(SceneMgr&&) = delete;
};
리소스 매니저처럼 싱글톤 패턴이라 어디서든 호출해서 부를수있다.
클래스 씬을 선언해주고, 씬들을 저장할 벡터를 만든다. 포인터로 저장한다.
그후 스타트 씬을 1번 씬으로 저장하고, 현재 씬을 스타트 씬으로 한다.
다른 메소드는 비슷하다. 업데이트와 드로우는 씬과 연결되어 있어 해당 씬의 업데이트와 드로우를 호출하는 것 뿐이다. 체인지 씬은 씬 아이디를 받아 씬을 변경한다.
씬 매니저 초기화 Init
void SceneMgr::Init()
{
Release();
scenes.push_back(new SceneDev1(SceneIds::SceneDev1));
scenes.push_back(new SceneDev2(SceneIds::SceneDev2));
for (auto scene : scenes)
{
scene->Init();
}
currentScene = startScene;
scenes[(int)currentScene]->Enter();
}
push_back 을 통해 원하는 씬을 생성해 추가한다.
그후 모든 씬을 초기화 하고, 현재 씬을 스타트 씬으로 해서 현재 씬을 활성화한다.
릴리즈
void SceneMgr::Release()
{
for (auto scene : scenes)
{
scene->Release();
delete scene;
}
scenes.clear();
}
씬들을 전부 메모리 해제하고, 벡터 scenes를 비운다.
씬체인지
void SceneMgr::ChangeScene(SceneIds id)
{
// TO-DO : 모든 게임 오브젝트 업데이트 끝난 후에 씬 전환되도록
scenes[(int)currentScene]->Exit();
currentScene = id;
scenes[(int)currentScene]->Enter();
}
현재 씬을 비활성화 하고, 현재 씬 이름을 바꿔서 그 씬을 활성화한다.
다만 완벽하게 구현하지 못해서 오브젝트들의 업데이트가 끝나는것을 확인하고 씬 전환이 되도록 하면 좋다.
씬 매니저가 씬들을 관리한다면, 씬은 게임오브젝트들을 관리한다.
화면에 떠있는 배경, 텍스트, 스프라이트 전부가 오브젝트다. 이를 생성하기 위한 게임오브젝트 클래스를 만들자.
#pragma once
class GameObject
{
protected:
bool active = true;
//std::string name = "";
// int sortLayer = 0;
// int sortOrder = 0; 수업때만 퍼블릭
public:
GameObject(const std::string& name = "");
virtual ~GameObject();
virtual void Init();
virtual void Release();
virtual void Reset(); // 재사용할때 쓰는 함수
virtual void Update(float dt);
virtual void Draw(sf::RenderWindow& window);
virtual bool GetActive() const { return active; }
virtual void SetActive(bool active) { this->active = active; };
std::string name = "";
int sortLayer = 0;
int sortOrder = 0; // 수업때만 퍼블릭 layer 층, order 층 내의 우선도
};
우선 해당 오브젝트가 활성화 상태인지 표현하는 변수 active를 만들고, 오브젝트 이름 name 을 가진다.
Reset은 총알, 장애물 같은 오브젝트가 일회용은 아니니 초기화해서 재사용하려고 만든 메소드다. 구현은 아직 안했다.
update와 draw는 씬매니저->씬->게임오브젝트 순으로 불러오며 여기서 구현을 하지는 않고 이것을 상속받은 실제 오브젝트들의 클래스에서 구현하면 된다.
GetActive와 SetActive는 단순히 Active 상태를 불러오고 조정하기 위한 메소드다.
sortLayer와 sortOrder는 오브젝트의 층과 우선도를 위해 넣어놓았지만 아직 사용하지 않겠다.
상속을 위한 부모클래스이기 때문에 내용 구현은 필요가 없어 비워두었다.
이제 실제로 테스트 오브젝트를 만들어 시험해보자
구조는 전부 만들었다. 게임 오브젝트와 씬을 테스트용으로 만들어보자.
실제로 게임을 만들땐, 구름은 cloud, 유저는 player 이런식으로 전부 만들어서 gameobject를 상속받아 와야 한다. 우선 시험용으로 하나만 만들어보자.
#pragma once
#include "GameObject.h"
class testGameObject : public GameObject
{
protected:
public:
testGameObject(const std::string& name = "");
void Init() override {};
void Release() override {};
void Reset() override {};
void Update(float dt) override;
void Draw(sf::RenderWindow& window) override;
sf::Text text;
};
해당 클래스는 부모 클래스에서 name, active를 받아왔으며 본인의 데이터형 변수도 하나 가져야 한다. 여기선 메시지를 표현할것이니 Text형의 text를 변수로 가졌다.
구현부는 draw에서 window.draw(text); 하나만 해두면 된다.
이름을 대충 붙인 개발씬 1이지만, 처음 씬이니 메인 씬이다. 선언해서 씬 클래스를 상속받고, 구현해보자
#include "pch.h"
#include "SceneDev1.h"
#include "testGameObject.h"
SceneDev1::SceneDev1(SceneIds id) : Scene(id)
{
}
SceneDev1::~SceneDev1()
{
}
void SceneDev1::Init()
{
ResourceMgr<sf::Font>& fontResMgr = ResourceMgr<sf::Font>::Instance();
testGameObject* obj = new testGameObject("Message");
obj->text.setFont(fontResMgr.Get("fonts/KOMIKAP_.ttf"));
obj->text.setString("SCENE DEV 1");
AddGo(obj);
}
void SceneDev1::Release()
{
}
void SceneDev1::Enter()
{
}
void SceneDev1::Exit()
{
}
void SceneDev1::Update(float dt)
{
Scene::Update(dt);
if (InputMgr::GetKeyDown(sf::Keyboard::Space))
{
SceneMgr::Instance().ChangeScene(SceneIds::SceneDev2);
}
if (InputMgr::GetKeyDown(sf::Keyboard::Num1))
{
GameObject* findGo = FindGo("Message");
findGo->SetActive(!findGo->GetActive());
}
}
리소스 매니저를 호출해서, 테스트게임오브젝트 형을 가진 obj 하나를 만들어서 이름 Message를 붙인다.
폰트를 load를 해야할텐데, 메인 함수에서 해둘테니 괜찮다.
obj에 폰트를 Get 해주고, 내용도 SetString 해준다.
이후 AddGO 를 통해 obj를 등록해주면 된다.
업데이트 부분도 구현했는데,
인풋 매니저 만들어둔것을 활용해서 스페이스바를 누르면 씬 2가,
숫자1번을 누르면 Message라는 이름을 가진 오브젝트를 찾아서 비활성화 시키는 메소드이다.
이제 전부 만들었다.
메인 함수에서 각 매니저를 호출한다.
ResourceMgr<sf::Font>& fontResMgr = ResourceMgr<sf::Font>::Instance();
SceneMgr& scenMgr = SceneMgr::Instance();
폰트매니저로 폰트를 로드해주고,
fontResMgr.Load("fonts/KOMIKAP_.ttf");
씬매니저로 초기화를 한다.
scenMgr.Init();
이후 메인 루프에서 씬 매니저를 이용해 업데이트를 하고,
scenMgr.Update(0.f);
윈도우 clear 이후 그려주기만 하면 된다.
window.clear();
scenMgr.Draw(window);
씬 매니저에서 현재 씬을 파악하고, 그 씬을 호출해서 draw를 시키고, 그 씬은 가지고 잇는 오브젝트를 전부 파악해서 각각 draw 하기 때문에 메인 함수에선 정말 단순하게 보인다.
scenMgr.Release();
메인함수 종료 전에 릴리즈로 메모리를 해제해주면 끝이다.
간단하게 게임의 구조를 모방했는데 굉장히 어렵다. 이런 기본구조를 철저히 숙지해야 게임을 만들 수 있다는 생각이 든다.