Unity 프로젝트에서 일반적인 게임 프로그래밍 디자인 패턴을 구현하면 깔끔하고 체계적이며 가독성 높은 코드베이스를 효율적으로 빌드하고 유지 관리할 수 있습니다. 디자인 패턴은 리팩터링과 테스트 시간을 줄여줄 뿐만 아니라 온보딩 및 개발 프로세스의 속도를 높여 게임, 개발팀, 비즈니스 성장을 위한 탄탄한 기반이 됩니다.
디자인 패턴은 코드에 복사하여 붙여넣을 수 있는 완성된 솔루션이 아니라 올바르게 사용하면 더 크고 확장 가능한 애플리케이션을 구축하는 데 도움이 되는 추가 도구라고 생각하세요.
이 페이지에서는 상태 패턴과 이를 통해 코드베이스를 더 쉽게 관리할 수 있는 방법을 살펴봅니다.
이 콘텐츠는 무료 전자책을 기반으로 합니다, 게임 프로그래밍 패턴으로 코드 수준 높이기에서 잘 알려진 디자인 패턴을 설명하고 Unity 프로젝트에 사용할 수 있는 실용적인 예제를 공유합니다.
Unity 게임 프로그래밍 디자인 패턴 시리즈의 다른 글은 Unity 베스트 프랙티스 허브에서 확인하거나 다음 링크를 클릭하세요:
플레이 가능한 캐릭터를 만든다고 상상해 보세요. 어느 순간 캐릭터가 바닥에 서 있을 수도 있습니다. 컨트롤러를 움직이면 달리거나 걷는 것처럼 보입니다. 점프 버튼을 누르면 캐릭터가 공중으로 도약합니다. 몇 프레임 후, 착륙하여 유휴 상태의 서 있는 자세로 다시 들어갑니다.
컴퓨터 게임의 상호작용성 때문에 런타임에 변경되는 많은 시스템을 추적하고 관리해야 합니다. 캐릭터의 다양한 상태를 나타내는 다이어그램을 그리면 위 그림과 같은 그림이 나올 수 있습니다:
몇 가지 차이점이 있지만 순서도와 비슷합니다:
- 여러 상태(유휴/대기, 걷기, 달리기, 점프 등)로 구성되며, 주어진 시간에 하나의 현재 상태만 활성화됩니다.
- 각 상태는 런타임의 조건에 따라 다른 상태로 전환할 수 있습니다.
- 전환이 발생하면 출력 상태가 새로운 활성 상태가 됩니다.
이 다이어그램은 유한 상태 머신 (FSM)이라는 것을 설명합니다. 게임 개발에서 FSM의 일반적인 사용 사례 중 하나는 소품이나 플레이 가능한 캐릭터와 같은 '게임 액터'의 내부 상태를 추적하는 것입니다. 게임 개발에서 FSM의 사용 사례는 매우 다양하며, Unity에서 프로젝트를 개발한 경험이 있다면 이미 Unity의 애니메이션 스테이트 머신 컨텍스트에서 FSM을 사용해 본 적이 있을 것입니다.
FSM은 상태 목록으로 정의됩니다. 각 전환에 대한 조건이 있는 초기 상태가 있습니다. FSM은 주어진 시간에 유한한 수의 상태 중 정확히 하나에 있을 수 있으며, 외부 입력에 따라 한 상태에서 다른 상태로 변경되어 전환이 발생할 수 있습니다.
반면 State 디자인 패턴은 상태를 나타내는 인터페이스와 각 상태에 대해 이 인터페이스를 구현하는 클래스를 정의합니다. 상태에 따라 동작을 변경해야 하는 컨텍스트 또는 클래스는 현재 상태 객체에 대한 참조를 보유합니다. 컨텍스트의 내부 상태가 변경되면 상태 객체에 대한 참조를 다른 객체를 가리키도록 업데이트하기만 하면 컨텍스트의 동작이 변경됩니다.
상태 패턴은 다양한 상태를 관리하고 상태 간에 전환할 수 있다는 점에서 FSM과 유사합니다. 그러나 FSM은 일반적으로 스위치 문을 사용하여 구현되는 반면, 상태 디자인 패턴은 상태를 나타내는 인터페이스와 각 상태에 대해 이 인터페이스를 구현하는 클래스를 정의합니다.
상태 패턴은 게임 개발에서 널리 사용되며 메인 메뉴, 게임플레이 상태, 게임 오버 상태 등 게임의 다양한 상태를 효과적으로 관리할 수 있는 방법입니다.
다음 섹션의 예시를 통해 상태 패턴이 실제로 어떻게 작동하는지 살펴보겠습니다.
이 섹션의 예제 코드를 제공하는 데모 프로젝트는 Github에서 확인할 수 있습니다.
기본 FSM을 코드에서 설명하는 간단한 방법은 열거형과 switch 문을 사용하는 아래 예제와 같습니다.
먼저 세 가지 상태로 구성된 열거형 PlayerControllerState를 정의합니다: 유휴, 걷기, 점프.
그런 다음 업데이트 루프에서 switch를 조건문으로 사용하여 현재 어떤 상태에 있는지 테스트합니다. 상태에 따라 적절한 함수를 호출하여 적용되는 특정 동작을 수행할 수 있습니다.
이 방법은 작동할 수 있지만, 특히 상태 간 전환 조건을 공식화해야 하는 경우 PlayerController 스크립트가 빠르게 지저분해질 수 있습니다. 스위치 문을 사용하여 하나의 스크립트로 게임 상태를 관리하는 것은 복잡하고 유지 관리하기 어려운 코드를 만들 수 있으므로 모범 사례로 간주되지 않습니다. 상태 및 전환의 수가 증가함에 따라 스위치 문이 커지고 이해하기 어려워질 수 있습니다.
또한 스위치 문을 변경해야 하므로 새 상태나 전환을 추가하기가 더 어려워집니다. 반면에 상태 패턴은 보다 모듈적이고 확장 가능한 디자인을 허용하므로 새로운 상태나 전환을 더 쉽게 추가할 수 있습니다.
상태 패턴을 다시 구현하여 PlayerController의 로직을 재구성해 보겠습니다. 이 코드 예제는 깃허브에서 호스팅되는 데모 프로젝트에서도 확인할 수 있습니다.
원래 4인방에 따르면 상태 디자인 패턴은 두 가지 문제를 해결합니다:
- 객체는 내부 상태가 변경되면 동작이 변경되어야 합니다.
- 상태별 동작은 독립적으로 정의됩니다. 새 상태를 추가해도 기존 상태의 동작에는 영향을 미치지 않습니다.
이전 코드 예제에서 리팩터링되지 않은 플레이어 컨트롤러 클래스는 상태 변경을 추적할 수 있지만 두 번째 문제를 만족시키지 못합니다. 새 상태를 추가할 때 기존 상태에 미치는 영향을 최소화하고 싶을 것입니다. 대신 상태를 객체로 캡슐화할 수 있습니다.
위의 다이어그램과 같이 예제에서 각 상태를 구조화한다고 상상해 보세요. 여기에서 적절한 상태를 입력하고 조건에 따라 제어 흐름이 종료될 때까지 각 프레임을 반복합니다. 즉, 특정 상태를 항목, 업데이트 및 종료로 캡슐화합니다.
위의 패턴을 구현하려면 IState라는 인터페이스를 만듭니다. 그런 다음 게임의 각 구체적인 상태는 이 규칙에 따라 인터페이스를 구현합니다:
- 항목: 이 로직은 상태를 처음 입력할 때 실행됩니다.
- 업데이트 이 로직은 매 프레임마다 실행됩니다(실행 또는 틱이라고도 함). 물리용 고정 업데이트, 후기 업데이트 등을 사용하여 모노비헤이비어처럼 업데이트 메서드를 더욱 세분화할 수 있습니다. 업데이트의 모든 기능은 상태 변경을 트리거하는 조건이 감지될 때까지 매 프레임마다 실행됩니다.
- 출구: 여기의 코드는 상태를 떠나 새 상태로 전환하기 전에 실행됩니다.
각 상태에 대해 IState를 구현하는 클래스를 만들어야 합니다. 샘플 프로젝트에서는 워크스테이트, 아이들스테이트, 점프스테이트에 대해 별도의 클래스를 설정했습니다.
그러면 또 다른 클래스인 StateMachine.cs가 제어 흐름이 상태에 들어오고 나가는 방식을 관리합니다. 세 가지 예제 상태를 사용하면 상태 머신은 아래 코드 샘플과 같이 보일 수 있습니다.
이 패턴을 따르기 위해 상태 머신은 관리 중인 각 상태(이 경우 walkState, jumpState, idleState)에 대해 퍼블릭 오브젝트를 참조합니다. 상태 머신은 모노비헤이비어에서 상속되지 않으므로 생성자를 사용하여 각 인스턴스를 설정하세요.
생성자에 필요한 모든 매개변수를 전달할 수 있습니다. 샘플 프로젝트에서는 각 상태에서 플레이어 컨트롤러를 참조합니다. 그런 다음 이를 사용하여 프레임당 각 상태를 업데이트합니다(아래 IdleState 예제 참조).
상태 머신 개념에 대해 다음 사항에 유의하세요:
- Serializable 속성을 사용하면 인스펙터에서 StateMachine.cs(및 해당 공개 필드)를 표시할 수 있습니다. 그러면 다른 모노비헤이비어(예: 플레이어 컨트롤러 또는 적 컨트롤러)가 상태 머신을 필드로 사용할 수 있습니다.
- CurrentState 속성은 읽기 전용입니다. StateMachine.cs 자체는 이 필드를 명시적으로 설정하지 않습니다. 그러면 PlayerController와 같은 외부 오브젝트가 Initialize 메서드를 호출하여 기본 State를 설정할 수 있습니다.
- 각 상태 객체는 현재 활성 상태를 변경하기 위해 TransitionTo 메서드를 호출하기 위한 자체 조건을 결정합니다. 스테이트머신 인스턴스를 설정할 때 각 스테이트에 필요한 종속성(스테이트머신 자체 포함)을 전달할 수 있습니다.
예제 프로젝트에서는 PlayerController에 이미 StateMachine에 대한 참조가 포함되어 있으므로 플레이어 파라미터를 하나만 전달하면 됩니다.
각 상태 오브젝트는 자체 내부 로직을 관리하며, 게임 오브젝트나 컴포넌트를 설명하는 데 필요한 만큼의 상태를 만들 수 있습니다. 각각은 IState를 구현하는 자체 클래스를 갖습니다. SOLID 원칙에 따라 상태를 더 추가해도 이전에 생성된 상태에 미치는 영향은 최소화됩니다.
다음은 유휴 상태의 예시입니다.
StateMachine.cs 스크립트와 마찬가지로 생성자는 PlayerController 객체를 전달하는 데 사용됩니다. 이 플레이어에는 스테이트 머신에 대한 레퍼런스와 업데이트 로직에 필요한 모든 것이 포함되어 있습니다. IdleState는 캐릭터 컨트롤러의 속도 또는 점프 상태를 모니터링한 다음 스테이트 머신의 TransitionTo 메서드를 적절히 호출합니다.
워크스테이트 및 점프스테이트 구현을 위한 샘플 프로젝트도 살펴보세요. 동작을 전환하는 하나의 큰 클래스가 있는 대신 각 상태에는 자체 업데이트 로직이 있어 서로 독립적으로 작동할 수 있습니다.
상태 패턴은 객체에 대한 내부 로직을 설정할 때 SOLID 원칙을 준수하는 데 도움이 될 수 있습니다. 각 상태는 상대적으로 작으며 다른 상태로 전환하기 위한 조건만 추적합니다. 개방형-폐쇄형 원칙에 따라 기존 상태에 영향을 주지 않고 더 많은 상태를 추가할 수 있으며, 하나의 모놀리식 스크립트에서 번거롭게 전환하거나 문장을 변경하지 않아도 됩니다.
또한 기능을 확장하여 상태 변경 사항을 외부 객체에 전달할 수도 있습니다. 이벤트를 추가할 수 있습니다( 관찰자 패턴 참조). 상태 진입 또는 종료 시 이벤트가 발생하면 관련 리스너에 알림을 보내고 런타임에 응답하도록 할 수 있습니다.
반면에 추적할 상태가 몇 개만 있는 경우에는 추가 구조가 지나치게 많을 수 있습니다. 이 패턴은 상태가 어느 정도 복잡해질 것으로 예상되는 경우에만 의미가 있을 수 있습니다. 다른 모든 디자인 패턴과 마찬가지로 특정 게임의 요구 사항에 따라 장단점을 평가해야 합니다.
Unity 프로그래밍을 위한 고급 리소스
전자책 게임 프로그래밍 패턴으로 코드 레벨 업에서는 Unity에서 디자인 패턴을 사용하는 방법에 대한 더 많은 예제를 제공합니다.
모든 고급 Unity 기술 전자책과 문서는 베스트 프랙티스 허브에서 확인할 수 있습니다. 이 전자책은 문서 내 고급 모범 사례 페이지에서도 확인할 수 있습니다.