[C++] 2.5 명령 패턴 Command Pattern
카테고리: C++ games
인프런에 있는 홍정모 교수님의 홍정모의 게임 만들기 연습 문제 패키지 강의를 듣고 정리한 필기입니다.😀
🌜 공부에 사용된 홍정모 교수님의 코드들 보러가기
🌜 [홍정모의 게임 만들기 연습 문제 패키지] 강의 들으러 가기!
Chapter 2. 객체 지향으로 가는 길 : 명령 패턴
- 명령과 행위 자체를 객체로 만듬
- 추상화 해나가는 과정
- 최대한 추상화 해서 변경 사항이 생기더라도 메인 무한루프를 가지는 update()는 건드릴 곳이 없게끔 하는 것이 좋다.
🔔 전체 코드 및 구조
- class Actor ✨
- class MyTank
- class Command ✨
- class UpCommand
- class LeftCommand
- class InputHandler ✨
- class TankExample
#pragma once
#include "Game2D.h"
#include <map>
namespace jm
{
class *Actor*
{
public:
virtual void moveUp(float dt) = 0;
virtual void moveLeft(float dt) = 0;
};
class *Command*
{
public:
virtual ~Command() {}
virtual void execute(Actor& actor, float dt) = 0;
};
class UpCommand : public Command
{
public:
virtual void execute(Actor& actor, float dt) override
{
actor.moveUp(dt);
}
};
class LeftCommand : public Command
{
public:
virtual void execute(Actor& actor, float dt) override
{
actor.moveLeft(dt);
}
};
class MyTank : public Actor
{
public:
vec2 center = vec2(0.0f, 0.0f);
//vec2 direction = vec2(1.0f, 0.0f, 0.0f);
void moveUp(float dt) override
{
center.y += 0.5f * dt;
}
void moveLeft(float dt) override
{
center.x -= 0.5f * dt;
}
void draw()
{
beginTransformation();
{
translate(center);
drawFilledBox(Colors::green, 0.25f, 0.1f); // body
translate(-0.02f, 0.1f);
drawFilledBox(Colors::blue, 0.15f, 0.09f); // turret
translate(0.15f, 0.0f);
drawFilledBox(Colors::red, 0.15f, 0.03f); // barrel
}
endTransformation();
}
};
class InputHandler
{
public:
Command * button_up = nullptr;
Command * button_left = nullptr;
//std::map<int, Command *> key_command_map;
InputHandler()
{
button_up = new UpCommand;
button_left = new LeftCommand;
}
void handleInput(Game2D & game, Actor & actor, float dt)
{
if (game.isKeyPressed(GLFW_KEY_UP)) button_up->execute(actor, dt);
if (game.isKeyPressed(GLFW_KEY_LEFT)) button_left->execute(actor, dt);
/*for (auto & m : key_command_map)
{
if (game.isKeyPressed(m.first)) m.second->execute(actor, dt);
}*/
}
};
class TankExample : public Game2D
{
public:
MyTank tank;
InputHandler input_handler;
public:
TankExample()
: Game2D("This is my digital canvas!", 1024, 768, false, 2)
{
//key mapping
//input_handler.key_command_map[GLFW_KEY_UP] = new UpCommand;
}
~TankExample()
{
}
void update() override
{
// move tank
/*if (isKeyPressed(GLFW_KEY_LEFT)) tank.center.x -= 0.5f * getTimeStep(); // 원래 이렇게 구현했는데 이렇게 하면
if (isKeyPressed(GLFW_KEY_RIGHT)) tank.center.x += 0.5f * getTimeStep(); // 탱크가 아닌 전투기로 바꾸고싶다면
if (isKeyPressed(GLFW_KEY_UP)) tank.center.y += 0.5f * getTimeStep(); // 코드를 전부 교체해야하는 불편함 有
if (isKeyPressed(GLFW_KEY_DOWN)) tank.center.y -= 0.5f * getTimeStep();*/
input_handler.handleInput(*this, tank, getTimeStep());
// rendering
tank.draw();
}
};
}
🔔 Actor 클래스
class Actor
{
public:
virtual void moveUp(float dt) = 0;
virtual void moveLeft(float dt) = 0;
};
- Actor 들이 모두 기본적으로 가지는 기능 들만 묶어서
class Actor
로 만들고 이를 각각 상속받게 한다.Actor
👉 ex) 탱크, 자전거, 비행기 등등- 탱크 클래스, 자전거 클래스 등등 모두 Actor를 상속하는 자식클래스이다.
Actor
들이 기본적으로 가지는 기능- ex) moveUp(), moveDown(), stop(), moveLeft(), moveRight 등등
- 순수 가상 함수로 만들어 자식 클래스들이 각각 오버라이딩 하도록 강제한다.
- 공통적인 기능이라도 그 내용은 어떤 종류의 액터냐에 따라 다르기 때문
- 예를 들어 탱크와 비행기 둘 다 moveUp이라는 기능을 가지지만 얼만큼 y 좌표로 움직일지 등등 내용은 각자 다 다르다.
- 공통적인 기능이라도 그 내용은 어떤 종류의 액터냐에 따라 다르기 때문
🔔 MyTank 클래스
class MyTank : public Actor
{
public:
vec2 center = vec2(0.0f, 0.0f);
void moveUp(float dt) override
{
center.y += 0.5f * dt;
}
void moveLeft(float dt) override
{
center.x -= 0.5f * dt;
}
void draw() { ..... }
};
- Actor 상속
- 모든 Acotr들이 가지는 기본적인 기능인 moveUp, moveLeft를
Tank
의 개성을 살려 오버라이딩 한다.
- 모든 Acotr들이 가지는 기본적인 기능인 moveUp, moveLeft를
🔔 Command 클래스
class Command
{
public:
virtual ~Command() {} // 가상소멸자
virtual void execute(Actor& actor, float dt) = 0;
};
- 가상 소멸자를 쓴다.
- 자식 Command 들은 각자 나름의 소멸자를 호출하도록
- 무언가를 수행하는 일을 한다.
- UpCommand, DownCommand 등등 수행하는 일도 종류가 다양하다.
- 각자 어떤 일을 수행할지는 execute 함수 를 오버라이딩 하여 내용을 다르게 하면 됨
- 순수 가상함수이므로 반드시 오버라이딩 해야 함
Actor
타입의 참조 레퍼런스를 받는다.
- 순수 가상함수이므로 반드시 오버라이딩 해야 함
- 다형성 및 추상화
- virtual void execute(Actor& actor, float dt) = 0;
- 어떠한 종류의 자식 Actor 들이 들어오던지 부모인
Actor
하나로 다 참조할 수 있도록. - 어떠한 종류의 자식 Actor 들이 들어오던지 이 Command를 수행할 수 있도록.
Actor& actor
변수 하나에Tank
,AirPlane
객체 다 참조 가능.
- 어떠한 종류의 자식 Actor 들이 들어오던지 부모인
- virtual void execute(Actor& actor, float dt) = 0;
🔔 UpCommand 클래스
class UpCommand : public Command
{
public:
virtual void execute(Actor& actor, float dt) override
{
actor.moveUp(dt);
}
};
- Command 상속
- 어떤 일을 수행할 것인지 부모인 Command 클래스의 execute 함수 를 오버라이딩 한다.
- UpCommand 클래스는 Actor의 moveUp 기능을 수행하는 클래스.
- 어떤 종류의 Actor이냐에 따라 다른 moveUp이 호출될 것이다.
- Actor& actor 에 탱크가 들어오면 탱크만의 moveUp이 들어올 것.
- 탱크만의 moveUp은 Actor를 상속받는 탱크 클래스에서 오버라이딩.
- Actor& actor 에 탱크가 들어오면 탱크만의 moveUp이 들어올 것.
- 어떤 일을 수행할 것인지 부모인 Command 클래스의 execute 함수 를 오버라이딩 한다.
🔔 LeftCommand 클래스
class LeftCommand : public Command
{
public:
virtual void execute(Actor& actor, float dt) override
{
actor.moveLeft(dt);
}
};
- Command 상속
- 어떤 일을 수행할 것인지 부모인 Command 클래스의 execute 함수 를 오버라이딩 한다.
- LeftCommand 클래스는 Actor의 moveLeft 기능을 수행하는 클래스.
- 어떤 종류의 Actor이냐에 따라 다른 moveLeft이 호출될 것이다.
- Actor& actor 에 탱크가 들어오면 탱크만의 moveLeft이 들어올 것.
- 탱크만의 moveUp은 Actor를 상속받는 탱크 클래스에서 오버라이딩.
- Actor& actor 에 탱크가 들어오면 탱크만의 moveLeft이 들어올 것.
- 어떤 일을 수행할 것인지 부모인 Command 클래스의 execute 함수 를 오버라이딩 한다.
🔔 InputHandler 클래스
class InputHandler
{
public:
Command * button_up = nullptr;
Command * button_left = nullptr;
//std::map<int, Command *> key_command_map; int엔 키보드 번호가 들어간다.
InputHandler()
{
button_up = new UpCommand;
button_left = new LeftCommand;
}
void handleInput(Game2D & game, Actor & actor, float dt)
{
if (game.isKeyPressed(GLFW_KEY_UP)) button_up->execute(actor, dt);
if (game.isKeyPressed(GLFW_KEY_LEFT)) button_left->execute(actor, dt);
/*
for (auto & m : key_command_map)
{
if (game.isKeyPressed(m.first)) m.second->execute(actor, dt);
}
*/
}
};
- 키보드 입력을 감지하는 클래스
- 키보드 입력 또한 update()에서 안받게 따로 추상화하여 뺐다.
- update()에선
InputHandler
의 void handleInput(Game2D & game, Actor & actor, float dt) 함수만 실행한다.
- Game2D & game
- isKeyPressed 함수를 사용해야 하기 때문에
- Actor & actor
- 어떤 종류의 액터를 기능하게 할지
- button_up
- Command 타입인 부모형 포인터.
- Command의 자식인 UpCommand 객체를 참조한다.
- button_up에 무엇이 들어갈지는 그때 그때 바뀔 수 있다. 게임 진행 중에도.
- 예를 들어 플레이어가 술에 취해서 위 아래가 뒤집히는 연출을 하고 싶다면 button_up = new DownCommand, button_down = new UpCommand 이렇게 뒤집을 수 있겠지.
- Command 타입인 부모형 포인터.
- button_left
- Command 타입인 부모형 포인터.
- Command의 자식인 LeftCommand 객체를 참조한다.
- button_left에 무엇이 들어갈지는 그때 그때 바뀔 수 있다. 게임 진행 중에도.
- Command 타입인 부모형 포인터.
- void handleInput(Game2D & game, Actor & actor, float dt)
- 키보드 입력에 따라 어떤 Actor를 어떤 기능을 시킬지.
- button_up->execute(actor, dt);
- execute은 가상함수이므로 button_up이 참조하는 자식 객체인 UpCommand의 execute가 실행된다.
- UpCommand의 execute는 moveUp을 호출하므로
- 해당 Actor이 오버라이딩 한 moveUp이 호출된다.
미리 std::map에 키보드 번호와 Command의 자식들을 종류별로 맵핑시켜놓고 for문 돌려 실행하는 방법도 있다. if문의 나열보단 더 편리할듯.
TankExample
- input_handler.handleInput(*this, tank, getTimeStep());
- 이 함수 안에서 키보드 입력에 따른 함수 처리를 다 해줄 것.
- *this
- 부모인 Game2D 타입으로 TankExample 객체를 참조
- 키보드 입력을 받는 함수를 쓰기 위해.
- tank
- 탱크 Actor.
중요한 메인 루프 함수인 update()는 수정할게 생기더라도 건드릴 일이 없게 해야한다. 만약 자동차로 변경하고 싶다면 input_handler.handleInput(*this, car, getTimeStep()) 만 바꿔주면 됨.
class TankExample : public Game2D
{
public:
MyTank tank;
InputHandler input_handler;
public:
TankExample()
: Game2D("This is my digital canvas!", 1024, 768, false, 2)
{
//key mapping
//input_handler.key_command_map[GLFW_KEY_UP] = new UpCommand;
}
~TankExample()
{
}
void update() override
{
input_handler.handleInput(*this, tank, getTimeStep());
// rendering
tank.draw();
}
};
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기