UDN
Search public documentation:

UnrealScriptStatesKR
English Translation
日本語訳
中国翻译

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 홈 > 언리얼스크립트 > 언리얼스크립트 언어 참고서 > 언리얼스크립트 스테이트(State)

언리얼스크립트 스테이트(State)


문서 변경내역: Tim Sweeney 원저. 홍성진 번역.

개요


역사적으로 게임 프로그래머는 게임이 "pong" 단계를 지나 진화를 거듭한 이래로 스테이트라는 개념을 사용해 왔습니다. 스테이트(와 소위 "스테이트 머신 프로그래밍")란 복잡한 오브젝트 작동방식을 관리하기 좋도록 자연스럽게 만드는 것입니다. 그러나 UnrealScript 이전에는 스테이트가 언어 레벨에서 지원되지 않아, 개발자들은 오브젝트의 상태에 따라 C/C++ "switch" 문으로 만들어야 했습니다. 그런 코드는 작성하기도 수정하기도 어려웠죠.

UnrealScript 는 스테이트를 언어 레벨에서 지원합니다.

UnrealScript 에서 월드에 있는 각 액터는 항상 하나, 딱 하나의 스테이트에만 있습니다. 그 스테이트는 액터가 하려는 액션을 반영합니다. 예를 들어 움직이는 브러시에는 "StandOpenTimed" 나 "BumpOpenTimed" 같은 스테이트(상태)가 있습니다. Pawn 에는 "Dying", "Attacking", "Wandering" 같은 스테이트가 있습니다.

UnrealScript 에서는 특정 상태에 존재하는 함수와 코드를 작성할 수 있습니다. 그 함수는 액터가 그런 상태에 있을 때만 호출되는 것입니다. 예를 들어 몬스터 스크립트를 작성하는데, "SeePlayer" 함수를 어떻게 처리할지 궁리중입니다. 돌아다니면서 보이는 플레이어를 공격하게 하고 싶습니다. 이미 플레이어를 공격하고 있다면, 중단하지 않고 계속 공격하게 해야 합니다.

이런 작업을 가장 쉽게 하려면 여러가지 (Wandering, Attacking) 스테이트를 정의하고, 각 스테이트의 "Touch" 를 각기 다른 버전으로 작성하면 됩니다. UnrealScript 에서는 이 기능을 지원합니다.

스테이트에 대해 파고 들어가기 전, 스테이트에는 두 가지 큰 장점과 한 가지 복합증이 있다는 것을 알아둘 필요가 있습니다:

  • 장점: 스테이트를 사용하면 액터가 하고 있는 작업에 따라 같은 함수를 여러가지 방식으로 처리할 수 있는 상태 관련 함수를 쉽게 작성할 수 있습니다.
  • 장점: 스테이트를 가지고 특수한 "스테이트 코드"를 작성할 수 있는데, 보통의 UnrealScript 명령에 "잠복성(latent) 함수"라는 특수 함수를 같이 사용하는 것입니다. 잠복성 함수는 "천천히" (막지 않은 상태로) 실행되다가, 일정한 "게임 시간"이 흐른 뒤 반환하는 함수를 말합니다. 이를 통해 시간 기반 프로그래밍이 가능하며, 이는 C/C++/Java 에는 없는 큰 장점입니다. 다시 말해, 개념을 잡은 대로 코드를 작성할 수 있다는 뜻입니다. 예를 들면 "이 문을 열고, 2 초간 멈춘 뒤, 이 사운드 효과를 내고, 저 문을 연 뒤, 저 몬스터를 풀어 플레이어를 공격하게 해라" 는 식으로 스크립트를 작성할 수 있습니다. 이런 작업을 간단한 직선형 코드로 작성하면, 코드의 시간 기반 실행 관리에 대한 세부적인 것은 언리얼 엔진이 알아서 해 주는 것입니다.
  • 복합증: (Touch 같은) 함수는 여러 스테이트는 물론 자손 클래스에서도 덮어쓸 수 있기 때문에, 특정 상황에서 어떤 "Touch" 함수를 호출해야 할 지 정확히 알아야 한다는 부담이 있습니다. UnrealScript 에는 이 프로세스를 명확히 그려내는 규칙이 있긴 하지만, 클래스와 스테이트 계층구조를 복잡하게 만든다면 주의해야 할 부분입니다.

TriggerLight 스크립트의 스테이트 예제입니다:

// 트리거가 라이트를 켭니다.
state() TriggerTurnsOn
{
   function Trigger( actor Other, pawn EventInstigator )
   {
      Trigger = None;
      Direction = 1.0;
      Enable( 'Tick' );
   }
}

// 트리거가 라이트를 끕니다.
state() TriggerTurnsOff
{
   function Trigger( actor Other, pawn EventInstigator )
   {
      Trigger = None;
      Direction = -1.0;
      Enable( 'Tick' );
   }
}

여기서는 두 가지 스테이트(TriggerTurnsOn 과 TriggerTurnsOff)를 선언하고 있으며, 각 스테이트에서 Trigger 함수의 각기 다른 버전을 작성하고 있습니다. 이런 구현은 스테이트 없이도 뽑아낼 수 있기는 하지만, 스테이트를 사용하면 모듈성과 확장성이 훨씬 좋아집니다. UnrealScrip 에서는 기존 클래스를 서브클래싱하여, 새로운 스테이트를 추가하고, 새로운 함수를 추가하는 작업을 쉽게 할 수 있는 것입니다. 이런 것을 스테이트 없이 작성한 코드는 나중에 확장하기가 훨씬 힘들어 집니다.

스테이트는 편집가능 상태로 선언할 수 있는데, 사용자가 언리얼 에디터에서 액터의 스테이트를 설정할 수 있는지 없는지를 나타냅니다. 편집가능 스테이트를 선언하려면:

state() MyState
{
   ...
}

편집불가능 스테이트를 선언하려면:

state MyState
{
   ...
}

"auto" 키워드를 사용하여 액터의 자동, 혹은 초기 스테이트를 지정할 수도 있습니다. 모든 새 액터가 처음 활성화되면 그 스테이트에 들어가도록 합니다:

auto state MyState
{
   ...
}

스테이트 라벨과 잠복성 함수


함수는 물론 스테이트도 하나 이상의 라벨에 UnrealScript 코드를 붙일 수 있습니다. 예:

auto state MyState
{
Begin:
   `log( "MyState has just begun!" );
   Sleep( 2.0 );
   `log( "MyState has finished sleeping" );
   goto('Begin');
}

위의 스테이트 코드는 "MyState has just begun!" 메시지를 출력한 다음 2 초간 멈춘 후 "MyState has finished sleeping" 메시지를 출력합니다. 이 예제에서 재밌는 것은, 잠복성 함수 "Sleep" 을 호출한 것입니다. 이 함수는 즉시 반환되지 않고, 일정한 게임 시간이 지난 후 반환됩니다. 잠복성 함수는 스테이트 코드 안에서만 호출 가능하며, 함수 안에서는 안됩니다. 잠복성 함수로 시간의 흐름이 관여된 복잡한 이벤트를 쉽게 관리할 수 있습니다.

모든 스테이트 코드는 라벨 정의로 시작됩니다. 위의 예제에서 라벨의 이름은 "Begin" 입니다. 이 라벨은 편리한 스테이트 코드 진입점이 됩니다. 스테이트 코드의 라벨 이름은 아무렇게나 지어도 되지만, "Begin" 라벨은 특별합니다. 그 스테이트의 디폴트 시작 지점이거든요.

모든 액터에 사용가능한 세 가지 주요 잠복성 함수는 이렇습니다:

  • Sleep( float Seconds ) 는 실행을 일정 기간 멈춘 다음 다시 시작합니다.
  • FinishAnim() 는 현재 재생중인 애니메이션 시퀸스가 완료될 때까지 기다린 다음 다시 시작합니다. 이 함수로 애니메이션 주도형 스크립트, 즉 실행이 메시 애니메이션에 의해 좌우되는 스크립트를 쉽게 작성할 수 있습니다. 예를 들어 대부분의 AI 스크립트는 (시간 주도형에 반대되는) 애니메이션 주도형입니다. 부드러운 애니메이션이 AI 시스템의 주 목표이기 때문입니다.
  • FinishInterpolation() 는 현재 InterpolationPoint 이동이 끝날 때까지 기다린 다음 다시 시작합니다.

Pawn 클래스는 월드를 탐험하거나 단기간 이동같은 동작에 대한 여러가지 중요한 잠복성 함수를 정의합니다. 그 사용법에 대한 설명은 별도의 AI 문서를 참고하시기 바랍니다.

스테이트 코드 작성시에는 이 세 가지 네이티브 UnrealScript 함수가 특히 유용합니다:

  • 스테이트 내 (C/C++/Basic goto 와 비슷한) "Goto('LabelName')" 함수는 스테이트 코드가 지정된 라벨부터 실행되도록 합니다.
  • 스테이트 내 특수 "Stop" 명령은 스테이트 코드 실행을 멈춥니다. 스테이트 코드 실행은 새로운 스테이트에 들어가기 전, 또는 현재 스테이트 내 새로운 라벨로 이동하기 전까지 계속되지 않습니다.
  • "GotoState" 함수는 액터가 다른 스테이트에 들어가도록 하며, 선택적으로 지정된 라벨(이 없으면, 디폴트는 "Begin" 라벨)에서 계속되도록 할 수도 있습니다. 스테이트 코드 안에서 GotoState 를 호출하면 목적지로 즉시 이동합니다. 액터의 어느 함수에서도 GotoState 를 호출할 수도 있는데, 이 경우는 즉시 효과가 발휘되지 않습니다. 우선 스테이트 코드로 실행이 돌아와야 합니다.

여지껏 이야기한 스테이트 개념에 대한 예제는 이렇습니다:

// 자동 실행되는 스테이트입니다.
auto state Idle
{
	// 다른 액터가 터치했을 때...
	function Touch( actor Other )
	{
		`log( "I was touched, so I'm going to Attacking" );
		GotoState( 'Attacking' );
		`log( "I have gone to the Attacking state" );
	}
Begin:
	`log( "I am idle..." );
	sleep( 10 );
	goto 'Begin';
}

// 공격 상태.
state Attacking
{
Begin:
	`log( "I am executing the attacking state code" );
	...
}

이 프로그램을 실행시키고 가서 액터를 건드리면, 이렇게 나옵니다:

I am idle...
I am idle...
I am idle...
I was touched, so I'm going to Attacking
I have gone to the Attacking state
I am executing the attacking state code

GotoState 중 이 부분은 중요하니 확실히 이해해야 합니다. 함수 안에서 GotoState 를 호출하면, 목적지로 즉시 이동하지 않고, 스테이트 코드로 실행이 돌아가고 나서야 이동된다는 점입니다.

스테이트 상속성 및 영역 규칙


UnrealScript 에서 기존 클래스를 서브클래싱할 때, 새로운 클래스는 모든 변수, 함수, 스테이트를 부모 클래스로부터 상속받습니다. 확실히 이해되지요.

그러나 UnrealScript 프로그래밍 모델에 스테이트 추상화 개념을 추가하고 나니 상속성과 영역 규칙이 조금 꼬였습니다. 자세한 상속 규칙은:

  • 새로운 클래스는 부모 클래스에서 모든 변수를 상속받습니다.
  • 새로운 클래스는 부모 클래스에서 모든 비-스테이트 함수를 상속받습니다. 상속받은 비-스테이트 함수는 어느 것이든 덮어쓸 수 있습니다. 완전 새로운 비-스테이트 함수를 추가할 수도 있습니다.
  • 새로운 클래스는 부모 클래스의 모든 스테이트를 상속하며, 그 스테이트 내 함수와 라벨도 포함됩니다. 상속받은 스테이트 함수나 스테이트 라벨은 어느 것이든 덮어쓸 수 있으며, 새로운 스테이트 함수나 스테이트 라벨을 추가할 수도 있습니다.

모든 덮어쓰기 규칙 예제는 이렇습니다:

// 부모 클래스 예제입니다.
class MyParentClass extends Actor;

// 비-스테이트 함수입니다.
function MyInstanceFunction()
{
	`log( "Executing MyInstanceFunction" );
}

// 스테이트 입니다.
state MyState
{
	// 스테이트 함수입니다.
	function MyStateFunction()
	{
		`log( "Executing MyStateFunction" );
	}
// "Begin" 라벨입니다.
Begin:
	`log("Beginning MyState");
}

// 자손 클래스 예제입니다.
class MyChildClass extends MyParentClass;

// 비-스테이트 함수를 덮어씁니다.
function MyInstanceFunction()
{
	`log( "Executing MyInstanceFunction in child class" );
}

// MyStateFunction 을 덮어쓸 수 있도록 MyState 를 다시 선언합니다.
state MyState
{
	// MyStateFunction 을 덮어씁니다.
	function MyStateFunction()
	{
		`log( "Executing MyStateFunction" );
	}
// "Begin" 라벨을 덮어씁니다.
Begin:
	`log( "Beginning MyState in MyChildClass" );
}

하나 이상의 스테이트, 그리고 하나 이상의 부모 클래스에 전역적으로 구현된 함수가 있는 경우, 주어진 맥락에서 어떤 버전의 함수를 호출할지 이해해야 합니다. 이렇게 복잡한 상황을 해결하는 영역 규칙은:

  • 오브젝트가 어떤 스테이트에 있고, 함수 구현부가 (액터의 클래스든 어느 부모 클래스든) 그 스테이트 어딘가에 있는 경우, 가장 많이 파생된(most-derived) 스테이트 버전 함수가 호출됩니다.
  • 그렇지 않으면 가장 많이 파생된 비-스테이트 버전 함수가 호출됩니다.

고급 스테이트 프로그래밍


한 스테이트가 부모 클래스에 이름이 같은 스테이트를 덮어쓰지 않는다면, 선택적으로 "extends" 키워드를 사용하여 현재 클래스에 있는 스테이트에서 그 스테이트를 확장시킬 수 있습니다. 이 기능은 예를 들면, (MeleeAttacking, RangeAttacking 처럼) 공통된 함수성이 많이 들어있는 유사 스테이트 그룹에 쓰기 좋습니다. 이 경우 기본 Attacking 스테이트를 다음과 같이 선언할 수 있습니다:

// 기본 공격 상태입니다.
state Attacking
{
	// 기본 함수를 여기 붙입니다...
}

// 근접 공격입니다.
state MeleeAttacking extends Attacking
{
	// 전용 함수를 여기 붙입니다...
}

// 원거리 공격입니다.
state RangeAttacking extends Attacking
{
	// 전용 함수를 여기 붙입니다...
}

스테이트는 옵션으로 ignores 지정자를 붙여 한 스테이트에 있을 때 무시할 함수를 지정할 수 있습니다. 문법은 이렇습니다:

// 스테이트를 선언합니다.
state Retreating
{
	// 다음 메시지를 무시합니다...
	ignores Touch, UnTouch, MyFunction;

	// 여기에 함수를 붙입니다...
}

액터의 "state" 변수, "name" 형 변수를 통해 액터가 어느 상태에 있는지 알 수 있습니다.

GotoState('') 를 사용하면 액터가 어느 상태에도 있지 않게 할 수 있습니다. 액터가 어느 상태에도 있지 않으면, 전역(비-스테이트) 함수만 호출됩니다.

액터의 스테이트 설정을 위해 GotoState 명령을 사용할 때마다, 엔진은 두 가지 특수한 통지 함수를 (정의해 둔 경우) 호출합니다. EndState()BeginState() 입니다. EndState 는 새로운 스테이트 시작 직전 현재 스테이트에서 호출되며, BeginState 는 새로운 스테이트 시작 직후 호출됩니다. 이 두 함수에는 각 스테이트에 필요할 수 있는 스테이트 전용 초기화(initialize)나 정리(cleanup) 작업을 넣기 좋습니다.

스테이트 스태킹

보통 한 스테이트에서 다른 스테이트로 변경하면, 방금 전 스테이트로 돌아갈 수가 없습니다. 그러나 스테이트 스태킹으(쌓기)로는 가능합니다. PushState 함수를 호출하면 새로운 스테이트로 바꾸면서 스택 맨 위에 넣고, 현재 스테이트는 얼립니다. PopState 가 호출되면 예전 스테이트로 복원시키고, PushState 가 호출된 지점부터 실행을 계속합니다. PushState 는 (스테이트 코드 안에서만) 가능하면 잠복성 함수처럼 작동하므로, 함수 안에서 PushState 를 호출하면 코드 실행 방식이 다릅니다. 함수에서 호출하면 (함수 안에서 GotoState 를 호출할 때와 비슷하게) 코드 실행이 중지되지 않는 반면, 스테이트 코드 안에서 호출하면 (마찬가지로 스테이트 코드 안에서 GotoState 를 호출할 때와 비슷하게) 자손 스테이트에서 pop 하기 전까지 실행이 중지됩니다.

스테이트는 스택에 딱 한 번만 쌓을 수 있으며, 스택에 같은 스테이트를 두 번 push 하려 하면 실패합니다. PushState 는 딱 GotoState 처럼 작동하며, 스테이트 이름과 스테이트의 시작점으로 쓸 옵션 라벨을 받습니다. 새로운 스테이트는 PushedState 이벤트를 받으며, 현재 스테이트는 PausedState 이벤트를 받습니다. PopState 를 호출한 이후 현재 스테이트는 PoppedState 이벤트를 받으며, (스택에서 다음에 있던) 새로운 스테이트는 ContinuedState 를 받습니다.

state FirstState
{
	function Myfunction()
	{
		doSomething();
		PushState('SecondState');
		// 함수 안에 있기에 바로 실행됩니다. (잠복성 함수 아님)
		JustPushedSecondState();
   }

Begin:
	doSomething();
	PushState('SecondState');
	// 스테이트 코드 블럭 안에 있기에 SecondState 가 pop 되면 실행됩니다. (잠복성 함수)
	JustPoppedSecondState();
}

state SecondState
{
	event PushState()
	{
		// push 받았으니, push 해 돌려 줍니다.
		PopState();
	}
}

IsInState 함수를 사용하면 스택에 특정 스테이트가 있는지 검사할 수 있습니다. 이 함수는 스테이트의 이름만 검사하므로, 부모 스테이트를 검사하는데는 사용할 수 없습니다. 예:

state BaseState
{
   ...
}

state ExtendedState extends BaseState
{
   ...
}

활성 스테이트가 ExtendedStateIsInState('BaseState') 는 거짓을 반환합니다. 물론 IsInState('BaseState', true) 를 호출하면 BaseState 가 스택에 있을 때 참을 반환합니다.