행동 패턴
- 장면(scene)을 시작할 차례
- 개체들의 행동이 필요
- 코드 == ‘행동’
- 수많은 행동을 유지보수하기 좋은 상태로 빠르게 정의하고 고치는데 도움이되는 패턴
- 타입 객체 패턴: 클래스를 실제로 정의하지 않아도 유연하게 행동 종류를 만들 수 있음.
- 하위 클래스 샌드박스 패턴: 다양한 행동을 정의하는 데 안전하게 사용할 수 있는 기본 기능 리스트를 제공
- 바이트 코드 패턴: 행동 구현을 코드가 아닌 데이터로 정의할 수 있는 가장 진보된 방법
바이트 코드
- 명령어를 인코딩한 데이터로 행동을 표현할 수 있는 유연함 제공
동기
- 게임 제작의 어려움
- 엄청난 양의 복잡한 소스 코드 구현
- 플랫폼의 성능도 최대한 뽑아야함.
- C++과 같은 중량 언어(heavyweight language)를 사용
- 기술을 들이는 비용 만만치 않음.
- 거기다 재미 또한 챙겨야한다.
- 반복개발 => 창조적인 몰입 상태에 빠지기 어려움
마법전투!
두 마법사가 어느 한 쪽이 이길 때까지 서로에게 마법을 쏘는 대전 게임을 만든다고 가정.
- 마법을 고칠 때 마다, 프로그래머가 코드를 고쳐야함
- 약간 바꿔서 테스트하고자 할 때 => 전체 빌드 필요
요즘 게임 == 출시한 뒤에도 업데이트를 통해서 버그를 고치거나 콘텐츠를 추가할 수 있어야한다.
- 모드 지원 == 유저 자신만의 마법
- 소스를 공개해야함.
데이터 > 코드
게임 엔진에서 사용하는 개발 언어는 마법을 구현하기에 적합하지 않다.
- 마법 기능과 핵심 게임 코드와 격리할 필요가 있다.
- 실행 파일과는 물리적으로 떼어놓도록.
- 데이터
- 행동을 데이터 파일에 따로 정의 => 게임 코드에 읽어서 ‘실행’
GOF의 인터프리터 패턴
- 실행하고 싶은 프로그래밍 언어가 있다고 가정.
- 이 언어는 아래 수식을 지원함 $(1+2) * (3-4)$
- 이런 표현식을 읽어서 언어 문법에 따라 각각 객체로 변환해야함.
숫자 리터럴을 다음과 같이 객체가 된다.
- 숫자 상수는 다순히 숫자 값을 래핑한 객체.
- 연산자도 객체롤 바뀜 => 피연산자도 같이 참조
- 괄호 + 우선순위 => 표현식이 작은 객체 트리로 바뀜.
Parsing: 파서는 문자열을 읽어서 문법 구조를 표현하는 객체 집합인 추상 구문 트리로 만든다.
- 인터프리터 패턴의 목적: 추상 구문 트리 => 실행
표현식 or 하위표현식 => 객체 트리 => 객체지향 방식으로 표현식이 자기 자시능ㄹ 평가하게함.
- 모든 표현식 객체가 상속받을 상위 인터페이스를 만든다.
1
2
3
4
5
6
class Expression
{
public:
virtual ~Expression() {}
virtual double evaluate() = 0;
};
- 언어 문법에서 지원하는 모든 표현식마다 Expression 인터페이스를 상속받는 클래스 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class NumberExpression : public Expression
{
public:
NumberExpression(double value)
: value_(value)
{}
virtual double evaluate()
{
return value_;
}
private:
double value_;
};
- 숫자 리터럴 표현식은 자기 값을 평가한다.
- 덧셈, 곱셈에는 하위표현식이 들어가 더 복잡.
- 자기를 평가하기 전, 포함된 하위 표현식을 재귀적으로 평가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AdditionExpression : public Expression
{
public:
AdditionExpression(Expression* left, Expression* right)
: left_(left),
right_(right)
{}
virtual double evaluate()
{
// Evaluate the operands.
double left = left_->evaluate();
double right = right_->evaluate();
// Add them.
return left + right;
}
private:
Expression* left_;
Expression* right_;
};
- 복잡한 수식 == 간단한 클래스로, 필요한 만큼 객체 만들어 적절히 연결
루비언어 => 인터프리터 방식으로 돌아가다, 바이트코드 방식으로 바뀜
- 단점:
- 화살표들이 많음 => 복잡한 프랙탈 트리 모양(a sprawling fractal tree)
- 코드 로딩 => 작은 객체를 많이 만들고 연결
- 객체와 객체를 잇는 포인터는 많은 메모리를 소모.
- 하위 표현식에 접근 == 데이터 캐시에 치명적
- 가상 메서드 호출 == 명령어 캐시에 치명적
- **느리다**
가상 기계어
게임 실행 => 미리 컴파일한 기계어를 실행함.
기계어 장점
- 밀도 높음: 한 비트도 낭비하지 않음.
- 선형적: 명령어가 같이 모여있음. 순서대로 실행(흐름 제어문 제외)
- 저수준: 각 명령어 == 비교적 최소한의 작업
- 빠름: 하드웨어로 직접 구현됨 구현
- 기계어를 유저한테 제공 == 해커들에게 취약
많은 게임 콘솔과 IOS에서 로딩하거나 런타임에 생성한 기계어를 애플리케이션에서 실행하지 못하게함 => JIT 컴파일러 == 코드를 즉석에서 최적화된 기계어로 바꿔줌.
기계어의 성능 <–절충–> 인터프리터 패턴의 안정성
가상 기계어와 간단한 예물레이터(가상머신, vm)
- 안전하게 격리 가능
- vm이 실행하는 가상 바이너리 기계어는 바이트코드라고 부름
- 바이트코드: 유연, 데이터로 여러가지 쉽게 정의 가능
- 인터프리터 패턴같은 고수준 표현보다 성능이 좋음
- 루아 == 바이트코드로 구현된 언어
가상 머신 == 인터프리터
The Pattern
- 명령어 집합(instruction set) == 실행할 수 있는 저수준 작업들을 정의
- 명령어 == 일련의 바이트로 인코딩
- 가상머신 == 중간 값들을 스택에 저장, 명령어 하나씩 실행
- 명령어 조합 => 복잡한 고수준 행동 정의
언제 사용?
책에 있는 패턴 중 가장 복잡하고 쉽게 적용하기 어려움.
정의할 행동은 많은데, 게임 구현에 사용한 언어로는 구현하기 어려울 때 바이트코드 패턴을 사용한다.
- 언어가 저수준이라, 만드는데 손이 많이 가거나 오류가 생기기 쉬움.
- 컴파일 시간이나 다른 빌드 환경 때문에 반복 개발하기가 너무 오래 걸림
- 보안에 취약, 정의하려는 행동이 게임을 깨먹지 않게 하고 싶다면, 나머지 코드로부터 격리해야함.
대부분의 게임이 위에 해당
빠른 반복 개발 + 안정성 => 얻기 어려움
- 바이트 코드 == 네이트 코드보다 느림. (성능 민감한 곳에 적합 x)
주의사항
- 되는 대로 만들지 말아야함
- 바이트코드가 표현할 수 있는 범위를 꼼꼼히 관리해야한다.
프론트엔드가 필요
- 저수준 바이트코드 명령어
- 성능 면에서 뛰어남.
- 사용자가 작성할 만한 게 아님.
- 컴파일러: 가상머신이 이해할 수 있는 바이트코드로 변환
- 저작 툴을 만들 여유가 없으면 바이트 코드 패턴을 쓰기 어렵다.
You’ll miss your debugger
디버거, 정적 분석기, 디컴파일러 같은 툴 == 어셈블리어와 고수준의 언어들에서만 사용할 수 있게 만들어짐
- 바이트 코드 VM에서는 이런 툴이 무용지물.
- VM 그 자체가 무엇을 하는지 알 수 있을 뿐이다.
모드로 변경할 수 있게 하고 싶다면 디버깅 기능도 같이 출시해야함
Sample Code
- 먼저 VM에 필요한 명령어 집합을 정의해야한다.
A magical API
- 마법 주문을 C++ 로 구현하기 위해 필요한 API
- 마법은 마법사의 스탯 중 하나를 바꿈.
1
2
3
void setHealth(int wizard, int amount);
void setWisdom(int wizard, int amount);
void setAgility(int wizard, int amount);
매개변수(wizard)는 마법을 적용할 대상.
- 세 가지 함수 => 다양한 마법 효과 가능
아래는 사운드 재생, 파티클을 보여주는 것
1
2
void playSound(int soundId);
void spawnParticles(int particleType);
A magical instruction set
- API가 데이터에서 제어 가능한 무언가로 어떻게 바뀌는지
- 작게 시작하기위해 우선 매개변수 전부 제거
- set___() : 마법사의 스탯 항상 최대값으로
- 이펙트효과: 하드코딩
- 마법은 이제 단순한 명령 집합.
- 명령어는 각각 어떤 작업을 하려는지 나타냄.
- 명령어들을 아래와 같이 열거형으로 표현할 수 있다.
1
2
3
4
5
6
7
8
enum Instruction
{
INST_SET_HEALTH = 0x00,
INST_SET_WISDOM = 0x01,
INST_SET_AGILITY = 0x02,
INST_PLAY_SOUND = 0x03,
INST_SPAWN_PARTICLES = 0x04
};
마법을 데이터로 인코딩하려면 이들 열거형 값을 배열에 저장하면된다.
- 원시 명령(primitive)의 개수에 따라 바이트 크기 변함.
- 마법 => 바이트들의 목록 : 바이트 코드
명령 하나 실행: 어떤 원시명령인지를 보고 이에 맞는 API메서드를 호출하면됨.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
switch (instruction)
{
case INST_SET_HEALTH:
setHealth(0, 100);
break;
case INST_SET_WISDOM:
setWisdom(0, 100);
break;
case INST_SET_AGILITY:
setAgility(0, 100);
break;
case INST_PLAY_SOUND:
playSound(SOUND_BANG);
break;
case INST_SPAWN_PARTICLES:
spawnParticles(PARTICLE_FLAME);
break;
}
인터프리터는 코드와 데이터를 연결.
- 마법 전체를 실행하는 VM에서는 이 코드를 다음과 같이 래핑한다.
첫 번째 가상머신 구현은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class VM
{
public:
void interpret(char bytecode[], int size)
{
for (int i = 0; i < size; i++)
{
char instruction = bytecode[i];
switch (instruction)
{
// Cases for each instruction...
}
}
}
};
- 이 가상 머신은 유연하지 않다.
- 상대방 마법사를 건드리거나 스탯을 낮추는 마법을 만들 수 없음.
- 사운드도 하나만 출력 가능하다.
- 매개변수를 받을 필요가 있음.
A stack machine
- 복잡한 중첩식을 실행하려면 가장 안쪽 하위 표현식부터 계산해야한다.
- 그 결과를 이를 담고 있던 표현식의 인수로 넘긴다.
- 이걸 전체 표현식이 다 계산될 때까지 반복하면 된다.
- 인터프리터 패턴 == 중첩 객체 트리 형태로 중첩식 표현
- 명령어를 1차원으로 나열해도, 하위 표현식 결과를 중첩 순서에 맞게 다음 표현식에 전달해야한다.
- 이를 위해 CPU처럼 스택을 이용해서 명령어 실행 순서를 제어한다.
이를 스택머신이라고 부름. 스택 기반 언어도 있음. C++은 컴파일러가 알아서 함수 인수를 스택에 쌓아 전달하지만, 스택기반 언어는 코드에서 직접 스택에 값을 넣어야 함수에 인수 전달 가능.
- 이를 위해 CPU처럼 스택을 이용해서 명령어 실행 순서를 제어한다.
- 명령어를 1차원으로 나열해도, 하위 표현식 결과를 중첩 순서에 맞게 다음 표현식에 전달해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class VM
{
public:
VM()
: stackSize_(0)
{}
// Other stuff...
private:
static const int MAX_STACK = 128;
int stackSize_;
int stack_[MAX_STACK];
};
- 스택을 통해 명령어들은 데이터를 주고 받는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class VM
{
private:
void push(int value)
{
// Check for stack overflow.
assert(stackSize_ < MAX_STACK);
stack_[stackSize_++] = value;
}
int pop()
{
// Make sure the stack isn't empty.
assert(stackSize_ > 0);
return stack_[--stackSize_];
}
// Other stuff...
};
- 명령어가 매개변수를 받을 때는 스택에서 꺼냄.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
switch (instruction)
{
case INST_SET_HEALTH:
{
int amount = pop();
int wizard = pop();
setHealth(wizard, amount);
break;
}
case INST_SET_WISDOM:
case INST_SET_AGILITY:
// Same as above...
case INST_PLAY_SOUND:
playSound(pop());
break;
case INST_SPAWN_PARTICLES:
spawnParticles(pop());
break;
}
- 스택에서 값을 얻어오려면, 리터럴 명령어가 필요하다.
- 리터럴 명령어는 정수 값을 나타냄
- 종료 조건없는 재귀함수 같이 무한 회귀문제를 피하면서 리터럴 명령어 자신의 값을 얻어와야함.
- 명령어 목록이 바이트의 나열이라는 점을 활용해야함.
- 숫자를 바이트 배열에 직접 집어넣으면 된다.
- 숫자 리터럴을 위한 명령어 타입은 다음과 같이 정의한다.
1
2
3
4
5
6
7
case INST_LITERAL:
{
// Read the next byte from the bytecode.
int value = bytecode[++i]; // 예제에서는 2바이트 이상의 정수를 디코딩하는 코드 대신 1바이트만 읽고 있음.
push(value);
break;
}
- 즉, 리터럴 명령어 뒤에 해당하는 정수 값을 둠.
그림으로 보는 인터프리터가 명령어 몇 개를 실행하는 과정과 스택 작동 원리 이해
- 스택이 비어 있는 상태에서 인터프리터가 첫 번째 명령을 실행한다.
리터럴 명령어부터 시작하여 바이트 값들을 읽어 스택에 넣는다.
마지막으로 INST_LITERAL을 실행한다.(10을 읽어서 스택에 넣음)
마지막으로 INST_SET_HEALTH를 실행.
- 스택에서 10 => amount 매게변수
- 스택에서 0 => wizard 매개변수
- setHealth(wizard, amount) 함수 호출
- 하지만 규칙으로 표현하는것이 좋음.
행동 = 조합
아직 몇 가지 내장 함수와 상수 매개변수만 지원할 뿐.
- 조합을 할 수 있게 해야함.
- 스탯을 지정한 값으로 바꿀 수 있는 마법 == 현재 스텟 고려
- 스탯을 얻어오는 명령을 추가해야함.
1
2
3
4
5
6
7
8
9
10
case INST_GET_HEALTH:
{
int wizard = pop();
push(getHealth(wizard));
break;
}
case INST_GET_WISDOM:
case INST_GET_AGILITY:
// You get the idea...
스탯을 복사하는 마법 또한 만들 수 있다.
다음으로는 계산 능력이 필요하다.
1
2
3
4
5
6
7
case INST_ADD:
{
int b = pop();
int a = pop();
push(a + b);
break;
}
- 마법사 체력을 민첩성과 지혜의 평균만큼 더해주는 마법
- Get the wizard’s current health and remember it.
- Get the wizard’s agility and remember it.
- Do the same for their wisdom.
- Get those last two, add them, and remember the result.
- Divide that by two and remember the result.
- Recall the wizard’s health and add it to that result.
- Take that result and set the wizard’s health to that value.
1
2
setHealth(0, getHealth(0) +
(getAgility(0) + getWisdom(0)) / 2);
- 1번은 다음과 같다.
1
2
LITERAL 0
GET_HEALTH
- 이 바이트 코드는 마법사의 체력을 스택에 넣음
아래는 스택의 상태가 변하는 것을 보여주는 예이다.
- 스탯: 체력 45, 민첩 7, 지혜 11
1 2 3 4 5 6 7 8 9 10 11 12
LITERAL 0 [0] # Wizard index LITERAL 0 [0, 0] # Wizard index GET_HEALTH [0, 45] # getHealth() LITERAL 0 [0, 45, 0] # Wizard index GET_AGILITY [0, 45, 7] # getAgility() LITERAL 0 [0, 45, 7, 0] # Wizard index GET_WISDOM [0, 45, 7, 11] # getWisdom() ADD [0, 45, 18] # Add agility and wisdom LITERAL 2 [0, 45, 18, 2] # Divisor DIVIDE [0, 45, 9] # Average agility and wisdom ADD [0, 54] # Add average to current health SET_HEALTH [] # Set health to result
가상머신
지금까지의 VM만으로 단순하면서, 깔끔한 데이터 형태로 행동을 마음껏 정의 가능.
‘바이트 코드’나 ‘가상 머신’은 스택, 반복문, 다중 선택문만으로 간단하게 만들 수 있다.
VM 구현 과정을 통해 ‘행동을 안전하게 격리‘한다는 목표를 달성함
바이트 코드 == 정의해높은 명령 몇 개를 통해서만 다른 코드에 접근할 수 있기 때문에 악의적인 코드를 실행하거나 잘못된 위치에 접근할 방법이 없음.
스택 크기를 통해 VM의 메모리 사용량을 조절할 수 있다.
- 시간또한 제어 가능
- interpret() 반복문에서 실행되는 명령어가 일정 개수 이상이면 빠져나오게 할 수 있다.
마법 제작 툴
- 고수준으로 제작할 수 있는 방법을 만드는것이 목표
- 고수준 정의 ==변환==> 저수준 스택 머신 바이트코드
- 클릭해서 작은 상자를 드래그 앤 드롭하거나 메뉴를 선택하는 식의 행동 조립
GUI 툴에서는 사용자가 ‘잘못된’ 코드를 만들 수 없음.
바이트코드 패턴의 궁극적인 목표: 사용자가 행동을 고수준 형식으로 편하게 표현할 수 있도록 하는 데 있다.
디자인 결정
바이트 코드 VM은 보통 스택기반과 레지스터 기반으로 나뉜다.
- 스택기반 VM
- 항상 스택 맨 위만 접근
- 레지스터 기반
- 스택은 있지만, 깊숙한 곳에서도 입력 값을 읽어올 수 있다.
- 바이트 코드에 인덱스가 있어, 스택 어디에서나 읽어올 수 있게
. | . |
---|---|
스택기반 VM | 명령어가 짧다: 인수를 스택 맨 위에서 얻기 때문에 명령어에 데이터를 따로 인코딩하지 않아도됨.</br>코드 생성이 간단: 순서만 잘 맞춰 명령어를 배치하면 명령어들끼리 매개변수를 주고받게한다. </br>명령어 개수가 많다: 모든 명령어가 스택 맨 위만 볼 수 있기 때문에, 결과 등을 옮기는 등의 명령문이 여러개 필요 |
레지스터 기반 VM | 명령어가 길다: 명령어가 스택 오프셋 값을 가지므로 비트가 더 필요</br>명령어 개수는 줄어든다: 한 명령어에서 더 많은 일을 할 수 있다. 성능향상 기대 |
루아는 레지스터 기반 VM을 사용. 한 명령어가 32비트, 6비트를 명령어 종류가 차지, 나머지는 인수들이 들어간다.
- 가능하면 스택기반 VM이 좋음
- 구현과 코드 생성도 간단
- 명령어를 어떻게 만드느냐, vm을 어떻게 구현하느냐에 따라 더 많이 달라짐.
어떤 명령어를 만들어야 하는가?
- 외부 원시 명령(External primitives)
- VM == 외부 게임 코드에 접근
- 바이트코드로 어떤 행동을 표현할 수 있는지를 제어
- 외부 원시 명령이 없으면, VM은 CPU 사이클 낭비.
- 내부 원시 명령(Internal primitives)
- 리터럴, 연산, 비교, 그 외 스택에 값을 주고받는 명령어들로 VM 내부 값을 다룬다.
- 흐름 제어
- 명령어를 조건에 따라 실행, 여러번 반복
- 점프 명령어: 인덱스 기록, 실행위치 옮기는것.
- 추상화
- 데이터 재사용, 프로시저 같은 것
- VM이 별도의 반환 스택을 관리.
- 호출 => 현재 실행 인덱스를 반환 스택에 넣고, 호출된 바이트코드로 점프.
- 반환 => VM은 반환 스택으로부터 실행 인덱스를 받아서 그 위치로 점프
값을 어떻게 표현?
- 문자열, 객체, 리스트 같은 여러 다른 자료형도 지원해야함.
단일 자료형
- 간단.
- 태깅이나 변환, 자료형 검사 신경 x
- 다른 자료형을 다룰 수 없음.
태그 붙은 변수
- 동적 타입 언어에서 흔한 방식.
- 모든 값은 두부분.
자료형(열거형)태그 데이터(태그에 따라 적절히 해석)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum ValueType
{
TYPE_INT,
TYPE_DOUBLE,
TYPE_STRING
};
struct Value
{
ValueType type;
union
{
int intValue;
double doubleValue;
char* stringValue;
};
};
- 값이 자신의 타입을 안다.
- 런타임에 값의 타입 확인
- 동적 디스패치에 중요(지원안하는 타입 연산을 막음)
- 메모리가 더 필요
- 모든 값에 비트를 추가해야함.
- 저수준에서는 영향이 큼
태그가 붙지 않은 공용체
- 정적 타입 언어가 이런 식으로 메모리에 표현
- 유니온을 사용하지만, 타입 태그는 없음.
- 알아서 해석
컴파일할 때 타입 시스템이 값을 제대로 해석하도록 보장 => 런타임에 따로 검증하지 않음.
- 작다
- 빠르다
- 타입 검사 오버헤드 없음.
안전하지 않음
- 포인터에 대해 잘못 해석하면 크래시 or 해킹에 취약
바이트코드 => 컴파일러를 통하지 않고 수동으로 악성 바이트코드 집어넣을 수 있음.
자바 VM이 프로그램을 로딩할 때 바이트코드 검증을 하는 이유
- 포인터에 대해 잘못 해석하면 크래시 or 해킹에 취약
인터페이스
- 객체지향: 여러 타입 중 무엇인지를 모르는 값이 있으면 다형성으로 처리.
- 인터페이스는 여러 자료형을 테스트하고 변환하는 가상 메서드를 다음과 같이 제공.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Value
{
public:
virtual ~Value() {}
virtual ValueType type() = 0;
virtual int asInt() {
// Can only call this on ints.
assert(false);
return 0;
}
// Other conversion methods...
};
- 각 타입마다 구체 클래스를 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
class IntValue : public Value
{
public:
IntValue(int value)
: value_(value)
{}
virtual ValueType type() { return TYPE_INT; }
virtual int asInt() { return value_; }
private:
int value_;
};
- 제한이 없다: 어떤 타입이든 정의 가능
- 객체지향적이다: 다형성 디스패치
- 번거롭다: 타입마다 코드 정의
비효율적: 다형성 == 포인터를 통해 동작, 힙에 할당된 객체로 래핑, 값에 접근할 때마다 가상함수 호출해야함.
- 할 수만 있다면 자료형은 하나만 사용하는것이 좋음
- 아니면 태그 붙은 변수
바이트코드는 어떻게 만들 것?
- 바이트 생성 == 보통 컴파일러
텍스트 기반 언어를 정의할 경우
- 문법을 정의해야함: 사용자가 만족하는 문법은 만들기 어려움. 문법설계 == UI설계
- 파서 구현: 파서 생성기(ANTLR, BISON), 재귀 하향 파서를 만들면됨.
- 문법 오류를 처리: 오류가 있다면, 올바른 방향으로 이끌어줘야 한다.
- 비-프로그래머들은 쓰기 어려움
UI가 있는 저작 툴을 만들 경우
- UI를 구현해야함: 이에 따라 툴은 쉽고 편해짐. 게임 콘텐츠도 개선
- 오류가 적다: 행동을 상호작용 방식으로 한 단계씩 만들기 때문.
- 이식성이 낮음: 프레임워크를 정해야함. OS에 종속되어있음.
관련 자료
- 인터프리터 패턴과 함께 데이터로 행동을 조합할 수 있는 방법 제공하는게 이 패턴
- 두 패턴을 둘다 사용하는 경우가 많다.
- 루아 == 게임 쪽에서 가장 널리 사용중인 스크립트 언어.
- 키즈멧(kismet) == 언리얼 에디터에 포함된 그래픽 스크립트 툴