[C++] 2.5 명령 패턴 Command Pattern

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 게임 만들기 연습 문제 패키지 강의를 듣고 정리한 필기입니다.😀
🌜 공부에 사용된 홍정모 교수님의 코드들 보러가기
🌜 [홍정모의 게임 만들기 연습 문제 패키지] 강의 들으러 가기!


Chapter 2. 객체 지향으로 가는 길 : 명령 패턴

  • 명령과 행위 자체를 객체로 만듬
    • 추상화 해나가는 과정
  • 최대한 추상화 해서 변경 사항이 생기더라도 메인 무한루프를 가지는 update()는 건드릴 곳이 없게끔 하는 것이 좋다.


🔔 전체 코드 및 구조

  1. class Actor ✨
    • class MyTank
  2. class Command ✨
    • class UpCommand
    • class LeftCommand
  3. class InputHandler ✨
  4. 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의 개성을 살려 오버라이딩 한다.


🔔 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 객체 다 참조 가능.


🔔 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를 상속받는 탱크 클래스에서 오버라이딩.


🔔 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를 상속받는 탱크 클래스에서 오버라이딩.


🔔 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()에선 InputHandlervoid 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 이렇게 뒤집을 수 있겠지.
  • button_left
    • Command 타입인 부모형 포인터.
      • Command의 자식인 LeftCommand 객체를 참조한다.
    • button_left에 무엇이 들어갈지는 그때 그때 바뀔 수 있다. 게임 진행 중에도.
  • 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();
		}
	};


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

C++ games 카테고리 내 다른 글 보러가기

댓글 남기기