본문 바로가기

게임프로그래밍패턴 2장 - 명령

https://learn.unity.com/tutorial/command-pattern#


명령 패턴

GOF
요청 자체를 캡슐화하는 것입니다. 이를 통해 요청이 서로 다른 사용자(Client)를 매개변수로 만들고, 요청을 대기시키거나 로깅하며, 되돌릴 수 있는 연산을 지원합니다.

박일
명령 패턴은 메서드 호출을 실체화(reify) 한 것이다.
= 명령 패턴은 함수 호출을 객체로 감쌌다는 의미이다.
= '콜백', '일급함수', '함수포인터', '클로저', '부분 적용 함수',
= 명령 패턴은 콜백을 객체지향적으로 표현한 것.

*실체화 : 어떤 개념을 변수에 저장하거나 함수에 전달할 수 있도록 객체(데이터)로 바꿀 수 있다.
ㄴ 리플렉션 시스템 : 런타임에 자료형을 가져와 다룰 수 있게 한다.

void InputHandler::handleInput()
{
	if(IsPressed(ButtonX)) jump();
    else if(IsPressed(ButtonY)) fireGun();
}

명령패턴으로 바꾸면

// 공통 상위 클래스
class Command
{
	public:
    	virtual ~Command() {};
        virtual void execute() = 0;
}

//하위클래스
class JumpCommand : public Command
{
	public:
    	virtual execute() {jump();}
}


class FireCommand : public Command
{
	public:
    	virtual execute() {Fire();}
}

//입력핸들러 코드
// 각 버튼별로 Command포인터를 저장한다.


class InputHandler
{
	private:
    	Command* buttonX_;
        Command* buttonY_;
        
	public:
    	handleInput()
        {
        	if(isPressed(BUTTON_X)) buttonX_->execute();
            else if(isPressed(BUTTON_Y)) buttonY_->execute();
        }
}

코드를 직접 호출하지 않고 한 겹 우회한다.





#2.2 액터에게 지시하기
jump()같은것은 전역함수와 캐릭터 객체와의 커플링이 깔려있다.

제어하려는 객체를 함수에서 직접 찾게 하지 말고 밖에서 전달해주자!
ㄴ 천재인가봐

class JumpCommand : public Command
{
public:
	virtual void execute(GameActor& InActor)
    {
    	actor.jump();
    }
}

입력핸들러에서 입력을 받아 적당한 객체의 메서드를 호출하는 명령 객체를 연결해줘야한다.

Command* InputHandler::HandleInput()
{
	if(isPressed(BUTTON_X)) return buttonX_;
    else if(isPressed(BUTTON_Y)) return buttonY_;
    
    //참고 : 기존코드는 buttonX_->execute();
}

handleInput()에서 명령을 직접 실행하는 대신에
함수 호출 시점을 지연시켜서, 실행 액터와의 디커플링을 이룬다.

Command* command = inputHandler.handleInput();
if(command != nullptr)
{
	command->execute(actor);
}

명령과 액터 사이 추상 계층을 하나 더 이루어서
명령을 실행할 때 액터만 바꾸면 플레이어가 게임에 있는 어던 액터라도 제어할 수 있게 되었다.

더보기

지금 회사 코드.

이벤트상황에서 이벤트리스너 통해서 Command 쌓아두고, 틱에서 매번 컨테이너에 보관중인 명령 리스트 execute 하여 디커플링을 이뤘다.





#2.3 실행취소와 재실행

class MoveUnitCommand : public Command
{
public:
	MoveUnitCommand(Unit* unit, int x, int y)
    : unit_(unit)
    , x_(x)
    , y_(y)
    {}
    
    virtual void execute() 
    {
    	unit_->moveTo(x_, y_);
    }
    
    virtual void Undo()
    {
    	unit_->moveTO(xBefore_, yBefore_);
    }
    
private:
	Unit* unit;
    int x_, y_;
 	int xBefore, yBefore_;
}

이제 특정 시점에 발생될 일을 표현할 것이다.
입력 핸들러 코드에서는 플레이어가 이동을 선택할 때마다 명령 인스턴스를 생성해야 한다.

Command* handleInput()
{
	Unit* unit = getSelectedUnit();
    if(IsPressed(BUTTON_UP))
    {
    	int destY = unit->y() - 1;
        return new MoveUnitCommand(unit, unit->x(), destY);
    }
    
    if(IsPressed(BUTTON_DOWN))
    {
    	int destY = unit->y() + 1;
        return new MoveUnitCommand(unit, unit->x(), destY);
    }
    
    return nullptr;
}


유닛이 이동한 뒤에는 이전 위치를 알 수 없기 때문에, 이동을 취소할 수 있도록 이전 위치를 xBefore_, yBefore_멤버변수에 따로 저장한다.
플레이어가 이동을 취소할 수 있게 하려면 이전에 실행했던 명령을 저장해야 한다. 계속 이전 명령의 undo()가 실행되는 것이다.

여러단계의 실행 취소 또한 마찬가지다. 가장 최근 명령만 기억하는게 아니라, 명령 목록을 유지하고 현재 명령만 알고 있으면 된다. 실행취소를 선택하면 현재 명령을 실행취소하고 현재명령 포인터를 뒤로 이동한다.

'' 카테고리의 다른 글

-  (0) 2023.07.18
좋아하는 블로그 모음  (0) 2022.11.06