Unity 프로젝트에서 일반적인 게임 프로그래밍 디자인 패턴을 구현하면 깔끔하고 체계적이며 가독성 높은 코드베이스를 효율적으로 빌드하고 유지 관리할 수 있습니다. 디자인 패턴은 리팩터링과 테스트 시간을 줄여 개발 프로세스의 속도를 높이고 게임, 팀, 비즈니스의 성장을 위한 탄탄한 기반을 마련합니다.
디자인 패턴은 코드에 복사하여 붙여넣을 수 있는 완성된 솔루션이 아니라 더 크고 확장 가능한 애플리케이션을 구축하는 데 도움이 되는 추가 도구라고 생각하세요.
이 페이지에서는 명령 디자인 패턴에 대해 설명합니다.
이 콘텐츠는 무료 전자책을 기반으로 합니다, 게임 프로그래밍 패턴으로 코드 레벨 업하기.
Unity 게임 프로그래밍 디자인 패턴 시리즈에 대한 자세한 내용은 Unity 베스트 프랙티스 허브 또는 다음 링크를 통해 확인할 수 있습니다:
명령 프로그래밍 디자인 패턴은 원래 4인방 중 하나로, 특정 일련의 작업을 추적하고자 할 때 유용합니다. 실행 취소/다시 실행 기능을 사용하거나 입력 내역을 목록으로 보관하는 게임을 플레이해 본 적이 있다면 이러한 명령 패턴을 보셨을 것입니다. 사용자가 실제로 턴을 실행하기 전에 여러 차례의 턴을 계획할 수 있는 전략 게임을 상상해 보세요. 이것이 바로 명령 패턴입니다.
명령 패턴을 사용하면 작업을 개체로 표현할 수 있습니다. 액션을 오브젝트로 캡슐화하면 사용자 입력에 따라 게임 오브젝트의 동작을 제어할 수 있는 유연하고 확장 가능한 시스템을 만들 수 있습니다. 이는 메서드를 직접 호출하는 대신 하나 이상의 메서드 호출을 '명령 객체'로 캡슐화하여 작동합니다. 그런 다음 이러한 명령 개체를 대기열이나 스택과 같은 컬렉션에 저장하여 작은 버퍼로 사용할 수 있습니다.
이러한 방식으로 명령 개체를 저장하면 나중에 재생할 수 있도록 일련의 작업을 잠재적으로 지연시켜 실행 타이밍을 제어할 수 있습니다. 마찬가지로 다시 실행하거나 실행 취소할 수 있으며 각 명령 개체의 실행을 제어할 수 있는 유연성을 추가할 수 있습니다.
다음은 다양한 게임 장르에서 이 패턴이 일반적으로 적용되는 몇 가지 예시입니다:
- 실시간 전략 게임에서는 명령 패턴을 사용하여 유닛과 건물 행동을 대기열에 넣을 수 있습니다. 그런 다음 리소스를 사용할 수 있게 되면 게임에서 각 명령을 실행합니다.
- 턴제 전략 게임에서 플레이어는 유닛을 선택한 다음 그 유닛의 이동이나 행동을 대기열이나 다른 컬렉션에 저장할 수 있습니다. 턴이 끝나면 게임은 플레이어의 대기열에 있는 모든 명령을 실행합니다.
- 퍼즐 게임에서는 명령 패턴을 통해 플레이어가 동작을 실행 취소하고 다시 실행할 수 있습니다.
- 격투 게임에서는 특정 명령 목록에서 버튼을 누르거나 게임패드의 동작을 읽으면 콤보와 필살기를 사용할 수 있습니다.
명령 패턴을 포함하여 게임 개발의 맥락에서 다양한 프로그래밍 디자인 패턴을 보여주는 샘플 프로젝트를 GitHub에서 확인해 보세요.
이 샘플에서 플레이어는 왼쪽에 있는 버튼을 클릭하여 미로를 이동할 수 있습니다. 플레이어가 움직이면 이동 흔적을 볼 수 있습니다. 하지만 더 중요한 것은 이전 작업을 실행 취소하고 다시 실행할 수 있다는 점입니다.
프로젝트에서 해당 장면을 찾으려면 "9 Command"라는 폴더로 이동합니다.
명령 패턴을 구현하려면 작업을 포함할 일반 개체가 필요합니다. 이 명령 객체에는 수행할 로직과 실행 취소 방법이 저장됩니다.
이를 구현하는 방법에는 여러 가지가 있지만 다음은 ICommand라는 인터페이스를 사용하는 간단한 버전입니다:
공용 인터페이스 ICommand
{
void Execute();
void Undo();
}
이 경우 모든 게임플레이 액션은 ICommand 인터페이스를 적용합니다(추상 클래스로 구현할 수도 있습니다).
각 명령 객체는 자체 실행 및 실행 취소 메서드를 담당합니다. 따라서 게임에 명령을 더 추가해도 기존 명령에는 영향을 미치지 않습니다.
그런 다음 CommandInvoker 클래스는 명령 실행 및 취소를 담당합니다. 실행 명령 및 실행 취소 메서드 외에도 명령 객체 시퀀스를 보관하는 실행 취소 스택이 있습니다.
샘플 프로젝트에서는 작은 미로에서 플레이어를 움직일 수 있습니다. 플레이어의 위치를 이동하는 간단한 옵션은 플레이어무버를 생성하는 것입니다.
이렇게 하려면 이동 메서드에 벡터3를 전달하여 플레이어가 네 개의 나침반 방향을 따라 이동하도록 안내해야 합니다. 레이캐스트를 사용하여 적절한 레이어마스크에서 벽을 감지할 수도 있습니다. 물론 명령 패턴에 적용하려는 내용을 구현하는 것은 패턴 자체와는 별개의 문제입니다.
명령 패턴을 따르려면 PlayerMover의 Move 메서드를 오브젝트로 캡처합니다. Move를 직접 호출하는 대신 ICommand 인터페이스를 구현하는 MoveCommand라는 새 클래스를 만듭니다.
public class MoveCommand : ICommand
{
PlayerMover playerMover;
Vector3 이동;
public MoveCommand(PlayerMover player, Vector3 moveVector)
{
this.playerMover = player;
this.movement = moveVector;
}
public void Execute()
{
playerMover.Move(movement);
}
public void Undo()
{
playerMover.Move(-movement);
}
}
수행하려는 로직이 무엇이든 여기에 들어가므로 이동 벡터로 이동을 호출합니다.
또한 장면을 이전 상태로 복원하려면 실행 취소 메서드가 필요합니다. 이 경우 실행 취소 로직은 이동 벡터를 빼서 플레이어를 반대 방향으로 밀어냅니다.
MoveCommand는 실행에 필요한 모든 매개변수를 저장합니다. 생성자를 사용하여 설정합니다. 이 경우 적절한 플레이어무버 컴포넌트와 이동 벡터를 저장합니다.
명령 객체를 생성하고 필요한 매개변수를 저장한 후에는 CommandInvoker의 정적 ExecuteCommand 및 UndoCommand 메서드를 사용하여 MoveCommand를 전달합니다. 그러면 MoveCommand의 실행 또는 실행 취소가 실행되고 실행 취소 스택에서 명령 개체를 추적합니다.
InputManager는 PlayerMover의 Move 메서드를 직접 호출하지 않습니다. 대신 RunMoveCommand라는 메서드를 추가하여 새 MoveCommand를 생성하고 CommandInvoker로 전송합니다.
그런 다음 UI 버튼의 다양한 onClick 이벤트를 설정하여 네 가지 이동 벡터로 RunPlayerCommand를 호출합니다.
InputManager의 구현 세부 사항은 샘플 프로젝트를 확인하세요. 키보드나 게임패드를 사용하여 직접 입력을 설정할 수도 있습니다. 이제 플레이어가 미로를 탐색할 수 있습니다. 실행 취소 버튼을 클릭하면 시작 사각형으로 되돌릴 수 있습니다.
재생 기능 또는 실행 취소 기능을 구현하는 것은 명령 오브젝트 컬렉션을 생성하는 것만큼이나 간단합니다. 명령 버퍼를 사용하여 특정 컨트롤을 사용하여 동작을 순서대로 재생할 수도 있습니다.
예를 들어, 특정 버튼을 연달아 클릭하면 콤보 동작이나 공격이 발동되는 격투 게임을 생각해 보세요. 명령 패턴으로 플레이어 동작을 저장하면 이러한 콤보를 훨씬 더 간단하게 설정할 수 있습니다.
반대로 명령 패턴은 다른 디자인 패턴과 마찬가지로 더 많은 구조를 도입합니다. 이러한 추가 클래스와 인터페이스가 애플리케이션에서 명령 객체를 배포하는 데 충분한 이점을 제공하는 위치를 결정해야 합니다.
기본 사항을 익힌 후에는 상황에 따라 명령의 타이밍을 변경하고 연속 또는 역순으로 재생할 수 있습니다.
명령 패턴을 통합할 때 다음 사항을 고려하세요:
- 더 많은 명령을 생성하세요: 샘플 프로젝트에는 한 가지 유형의 명령 오브젝트인 MoveCommand만 포함되어 있습니다. ICommand를 구현하는 명령 개체를 얼마든지 생성하고 CommandInvoker를 사용하여 추적할 수 있습니다.
- 재실행 기능을 추가하려면 스택을 하나 더 추가하면 됩니다: 명령 개체를 실행 취소할 때는 다시 실행 작업을 추적하는 별도의 스택에 밀어 넣습니다. 이렇게 하면 실행 취소 기록을 빠르게 순환하거나 해당 작업을 다시 실행할 수 있습니다. 사용자가 완전히 새로운 동작을 호출할 때 다시 실행 스택을 지웁니다(샘플 프로젝트에서 구현을 확인할 수 있습니다).
- 명령 개체 버퍼에 다른 컬렉션을 사용하세요: 선입선출(FIFO) 동작을 원한다면 대기열을 사용하는 것이 더 편리할 수 있습니다. 목록을 사용하는 경우 현재 활성 인덱스를 추적하며, 활성 인덱스 이전의 명령은 실행 취소할 수 없습니다. 인덱스 이후의 명령은 다시 실행할 수 있습니다.
- 스택의 크기를 제한합니다: 실행 취소 및 재실행 작업은 금방 통제 불능 상태가 될 수 있습니다. 스택을 최소 명령 수로 제한하세요.
- 생성자에 필요한 매개변수를 전달합니다: 이렇게 하면 MoveCommand 예제에서 볼 수 있듯이 로직을 캡슐화하는 데 도움이 됩니다.
- CommandInvoker는 다른 외부 객체와 마찬가지로 명령 객체의 내부 동작을 볼 수 없으며 실행 또는 실행 취소만 호출합니다. 생성자를 호출할 때 명령 객체에 작업에 필요한 데이터를 제공하세요.