2월 15일 수업 정리
이제 본격적으로 어려운 부분을 객체로 만들어보자.
일단 Tree를 GamaObject를 상속받아서 단순히 서있기만 하게 만든다.
그후 Tree를 상속받은 Branch를 만들어서 만일 포지션이 변경되어도 나뭇가지는 나무와 함께 움직이도록 만든다.
#pragma once
#include "GameObject.h"
#include "Branch.h"
class Branch;
class Tree : public GameObject
{
protected:
sf::Sprite tree;
std::list<Branch*> branches;
bool isOver = false;
public:
Tree(const std::string& name = "");
virtual ~Tree();
int branchCount = 6;
std::string treeTexureId = "graphics/tree.png";
std::string branchTextureId = "graphics/branch.png";
float branchOffsetY = 150.f;
static Sides RandomSide(int range = 4);
void SetBranchCount(int count) { branchCount = count; }
Sides Chop(Sides side);
void UpdateBranchPos();
void Init();
void Release();
void Reset();
void Update(float dt);
void Draw(sf::RenderWindow& window);
void SetPosition(const sf::Vector2f pos) override;
void SetOrigin(const sf::Vector2f newOrigin) override;
void SetOrigin(Origins preset) override;
float const GetTreeWidth() { return tree.getLocalBounds().width; }
Sides const GetFirstBranch() { return branches.front()->GetSide(); }
void SetOver(bool isover);
};
일단 나뭇가지 클래스 Branch를 전방선언 해준다, 그후 컨테이너 리스트 branches를 나무가 가지고 있게 한다.
int branchCount = 6;는 나뭇가지의 숫자,
float branchOffsetY = 150.f;는 나뭇가지들 끼리 떨어져있는 거리 오프셋이다.
이후 구현부에 메소드를 만들자.
#include "pch.h"
#include "Tree.h"
#include "Branch.h"
Tree::Tree(const std::string& name) : GameObject(name)
{
}
Tree::~Tree()
{
Release();
}
Sides Tree::RandomSide(int range)
{
Sides side = Sides::NONE;
int rand = Utils::RandomRange(0, range);
if (rand < 2)
{
side = (Sides)rand;
}
return side;
}
Sides Tree::Chop(Sides side)
{
// push의 반대는 pop, push_back 과 pop_front 쓸것
Branch* temp = branches.front();
branches.pop_front();
int rand = Utils::RandomRange(0, 4);
if (rand < 2)
{
temp->SetSide((Sides)rand);
}
else
{
temp->SetSide(Sides::NONE);
}
branches.push_back(temp);
UpdateBranchPos();
return branches.front()->GetSide();
}
void Tree::UpdateBranchPos()
{
sf::Vector2f pos = position;
for (Branch* branch : branches)
{
pos.y -= branchOffsetY;
branch->SetPosition(pos);
}
}
void Tree::Init()
{
Release();
tree.setTexture(RES_MGR_TEXTURE.Get(treeTexureId));
Utils::SetOrigin(tree, Origins::BC);
float originBranchX;
originBranchX = tree.getLocalBounds().width * -0.5f;
sf::Texture& branchTexture = RES_MGR_TEXTURE.Get(branchTextureId);
float originBranchY = branchTexture.getSize().y;
for (int i = 0; i < branchCount; ++i)
{
Branch* branch = new Branch();
branch->SetTexture(branchTextureId);
branch->SetOrigin({ originBranchX, originBranchY });
branches.push_back(branch);
}
}
void Tree::Release()
{
for (Branch* branch : branches)
{
delete branch;
}
branches.clear();
}
void Tree::Reset()
{
for (Branch* branch : branches)
{
branch->Reset();
}
int noneCount = 2;
int count = 0;
for (Branch* branch : branches)
{
if (count < noneCount)
{
branch->SetSide(Sides::NONE);
}
else
{
branch->SetSide(RandomSide());
}
++count;
}
UpdateBranchPos();
}
void Tree::Update(float dt)
{
if (isOver != true)
{
if (InputMgr::GetKeyDown(sf::Keyboard::Left))
{
Chop(Sides::LEFT);
}
if (InputMgr::GetKeyDown(sf::Keyboard::Right))
{
Chop(Sides::RIGHT);
}
}
for (Branch* branch : branches)
{
if (branch->GetActive())
{
branch->Update(dt);
}
}
}
void Tree::Draw(sf::RenderWindow& window)
{
window.draw(tree);
for (Branch* branch : branches)
{
if (branch->GetActive())
{
branch->Draw(window);
}
}
}
void Tree::SetPosition(const sf::Vector2f pos)
{
GameObject::SetPosition(pos);
tree.setPosition(pos);
UpdateBranchPos();
}
void Tree::SetOrigin(const sf::Vector2f newOrigin)
{
}
void Tree::SetOrigin(Origins preset)
{
}
void Tree::SetOver(bool isover)
{
isOver = isover;
}
많이 복잡하다.
그전에 우선 열거형 sides를 enum class Sides
{
NONE = -1, LEFT, RIGHT, COUNT
};로 바꿔서 0, 1 가 왼쪽 오른쪽이 되게 한다.
우선 RandomSide 메소드는 이전에 만들어뒀던 랜덤 함수를 사용해서 0부터 3까지중에 하나가 나오도록 했다. 그래서 3 ,4가 나오면 그냥 NONE 이고, 0 1이 나오면 왼쪽과 오른쪽중 하나가 되게 해서 랜덤한 나뭇가지 방향을 구현했다.
Chop 메소드는 나뭇가지가 하나씩 내려오는것을 구현한 메소드인데,
Branch 포인터를 하나 만들어서 branches 리스트의 첫번째 나뭇가지를 복사한다.
그후 pop을 사용해 첫번째 리스트를 삭제하고, 랜덤한 나뭇가지 방향을 만들어 push_back으로 맨 뒤쪽에 붙여넣었다. 이후 나뭇가지들의 위치를 업데이트하는 메소드이다.
updateBranchPos는 말그대로 첫 포지션을 잡은 후, 오프셋만큼 y의 값을 내려서 나뭇가지들이 그 간격만큼 올라가며 위치하도록 한다(y가 높을수록 아래로 온다)
초기화 메소드 Init에서 텍스처 set과 나무 자체의 오리진을 잡아주고,
나무의 너비를 구해서 그 절반만큼의 x와 나뭇가지 높이만큼의 y로 오리진을 지정해준다.
이렇게 하면 나무 정중앙에 오리진이 잡혀서 포지션을 이동하지 않고 x축을 뒤집기만 해도 나뭇가지가 왔다갔다 하는것으로 보인다.
Reset에서 0~1번 나뭇가지는 NONE으로 해주고 이후는 랜덤하게 배치했다. 그후 위치를 또 업데이트 해준다.
마지막으로 업데이트 메소드에서 키 입력시 Chop 을 하게 설정한다. 나무통이 잘려나가야 하니 방향도 꼭 넣어야 한다.
대부분 나무 쪽에서 구현했지만 나무를 상속받는 나뭇가지 클래스도 만들어둔다.
#pragma once
#include "SpriteGo.h"
class Branch : public SpriteGo
{
protected:
Sides originalDir = Sides::RIGHT;
Sides side = Sides::NONE;
public:
Branch(const std::string name = "");
virtual ~Branch() = default;
void Reset() override;
void SetSide(Sides side);
Sides GetSide() const { return side; }
};
원래 위치와 새 위치를 받는 변수가 있다.
#include "pch.h"
#include "Branch.h"
Branch::Branch(const std::string name) : SpriteGo(name)
{
}
void Branch::Reset()
{
SetSide(Sides::NONE);
}
void Branch::SetSide(Sides side)
{
this->side = side;
switch (this->side)
{
case Sides::LEFT:
case Sides::RIGHT:
SetActive(true);
SetFlipX(originalDir != side);
break;
default:
SetActive(false);
break;
}
}
SetSide 메소드는 방향을 받아서 acitve를 활성해주고, 방향이 원래랑 반대면 x축을 플립해준다.
이제 나무 밑단을 치면 날아가는것을 구현할 차례다.
날아가는 나무통 하나하나가 전부 객체이기 때문에, 한번만 사용하면 메모리 낭비가 심해서
사용 후 3초가 지나면 비활성화 해서 저장해뒀다가, 재사용할것이다.
#pragma once
#include "SpriteGo.h"
class EffectLog : public SpriteGo
{
protected:
sf::Vector2f gravity = { 0.f, 400.f };
sf::Vector2f velocity;
public:
EffectLog(const std::string& name = "");
virtual ~EffectLog();
float duration = 3.f;
float timer = 0.f;
void Fire(const sf::Vector2f v);
void Update(float dt) override;
};
호출마다 적용될 중력 gravity, 그리고 객체 자체의 속도 velocity 를 만들었다.
#include "pch.h"
#include "EffectLog.h"
EffectLog::EffectLog(const std::string& name) : SpriteGo(name)
{
}
EffectLog::~EffectLog()
{
}
void EffectLog::Fire(const sf::Vector2f v)
{
velocity = v;
timer = 0.f;
}
void EffectLog::Update(float dt)
{
timer += dt;
if (timer > duration)
{
SetActive(false);
return;
}
velocity += gravity * dt;
SetPosition(position + velocity * dt);
}
격발 시키는 메소드 Fire 에서 객체마다의 타이머를 0으로 설정하고, (3초를 세기위해)
속도를 받아서 velocity에 저장한다.
그후 업데이트로 타이머에 델타타임을 받아서 시간을 재고, 3초로 설정한 duration이 지나면 액티브를 비활성화한다.
속도에는 매 호출마다 중력 가속도를 감해주고, 그 속도만큼을 또 매 호출마다 받아 position을 변경해준다.
이제 메인 씬에서 적용해줄 차례이다.
SceneGame//
std::list<EffectLog*> useEffectList;
std::list<EffectLog*> unuseEffectList;
float timer = 0.f;
float duration = 3.f;
타이머와 듀레이션을 여기에 넣는다.
또한 사용되는 나무통과, 비활성화된 나무통을 저장할 컨테이너 리스트를 각각 만든다.
void SceneGame::playEffectLog(Sides side)
{
EffectLog* effectLog = nullptr;
if (unuseEffectList.empty())
{
effectLog = new EffectLog();
effectLog->SetTexture("graphics/log.png");
effectLog->SetOrigin(Origins::BC);
effectLog->Init();
}
else
{
effectLog = unuseEffectList.front();
unuseEffectList.pop_front();
}
effectLog->SetActive(true);
effectLog->Reset();
effectLog->SetPosition(tree->GetPosition());
sf::Vector2f velocity(1000.f, -1000.f);
if (side == Sides::RIGHT)
{
velocity.x *= -1.f;
}
effectLog->Fire(velocity);
useEffectList.push_back(effectLog);
AddGo(effectLog);
}
메인 씬 밑쪽에 구현해줬다.
각 나무통이 될 객체 effectLog를 만들고, 만약 사용되지 않은 나무통 리스트가 비어있다면 새 객체를 생성해서 초기화 까지 해주는 조건문을 만들었다.
그후 비어있지 않다면, 사용되지 않는 나무통의 맨 처음것을 대입하고, 리스트의 첫번째 객체를 삭제한다.
이후 이 객체들을 리셋, 활성화 , 포지셔닝까지 해준다.
그후 속도 velocity를 설정해서 Fire 메소드에 넣으면 속도를 입력받은 객체가 포지션이 변경되며 날아가는것처럼 보일것이다.
사용한 객체는 사용한 객체 리스트에 넣고, gameobjects 에 addgo로 추가해준다.
이렇게 되면 첫 객체가 만들어지고, 3초가 지나면 비활성화 되어 리스트에 넣어져 만일 다른 호출이 발생하면 이걸 사용한다. 만약 3초 내에 다른 호출이 있다면 새로운 객체를 만든다. 따라서 3초내에 연타한 만큼은 객체가 생성되는 것이다.
오늘 배운것을 응용해서 플레이어와 도끼, 비석까지 만들어보자.
플레이어는 나무처럼 SpriteGo를 상속받아서, 나뭇가지처럼 입력에 따라 x축이 플립되게 만든다. 위치는 적당히 나무의 너비를 참고했고, y축은 본인의 크기만큼을 입력해서 오리진을 최하단으로 설정했다.
#pragma once
#include "SpriteGo.h"
class Axe;
class Player : public SpriteGo
{
protected:
std::string playerId = "graphics/player.png";
Sides side = Sides::RIGHT;
bool isOver = false;
public:
Player(const std::string name = "");
virtual ~Player() = default;
void SetSide(Sides side);
Sides const GetSide() {return side;}
void Init() override;
void Reset() override;
void Update(float dt) override;
float const GetPlayerHeight() { return sprite.getLocalBounds().height; }
void SetOver(bool isover);
};
#include "pch.h"
#include "Player.h"
#include "Tree.h"
Player::Player(const std::string name) : SpriteGo(name)
{
}
void Player::SetSide(Sides side)
{
this->side = side;
SetFlipX(this->side == Sides::LEFT);
}
void Player::Init()
{
}
void Player::Reset()
{
SetActive(true);
SetSide(Sides::RIGHT);
}
void Player::Update(float dt)
{
if (isOver != true)
{
if (InputMgr::GetKeyDown(sf::Keyboard::Left))
{
if (this->side == Sides::RIGHT)
{
SetSide(Sides::LEFT);
}
}
if (InputMgr::GetKeyDown(sf::Keyboard::Right))
{
if (this->side == Sides::LEFT)
{
SetSide(Sides::RIGHT);
}
}
}
}
void Player::SetOver(bool isover)
{
isOver = isover;
}
Setside 메소드는 단순히 방향 변수와 FLip만을 담당하고,
업데이트에서 키 입력에 따라 방향이 변경되게 했다.
게임 오버 상태에서 입력되는것을 방지하려면 키 입력 부분을 메인 씬에 넣어야 했는데,
너무나도 코드가 길어져서 플레이어 클래스에 넣고 isover라는 스위치만 받아서 이게 꺼져있는 동안에만 입력을 받도록 했다.
그래도 메인 씬에서 setover, reset 등등은 키 입력에 따라 변경해줘야 한다.
반드시 텍스처는 오리진 설정 전에 set 해줘야 한다. 생성자에 set를 넣었더니 메인 씬에서 오리진이 먼저 설정되고 set이 되어서 적용이 안됐었다. origin을 하드코딩하기 싫어 나무 위치를 참조하려고 메인 씬으로 빼냈더니 생긴 문제라, 다른 방식으로 나무 크기를 받고 origin 메소드는 player 안으로 넣거나 , set과 origin을 둘다 밖으로 빼내는 방법 둘중 하나밖에 없었다.
도끼는 player와 비슷하게 하나 키 입력시만 active 활성화되게 했다.
active가 비활성화 되면 업데이트도 안되어서 active 설정을 메인 씬에서 해줘야 했다.
비석은 단순히 게임오버 status일 때 플레이어의 텍스처를 비석으로 set 해줬다.