Home [게임 프로그래밍 패턴] Behavioral Patterns: Bytecode
Post
Cancel

[게임 프로그래밍 패턴] Behavioral Patterns: Bytecode

행동 패턴

  • 장면(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++은 컴파일러가 알아서 함수 인수를 스택에 쌓아 전달하지만, 스택기반 언어는 코드에서 직접 스택에 값을 넣어야 함수에 인수 전달 가능.

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;
}
  • 마법사 체력을 민첩성과 지혜의 평균만큼 더해주는 마법
    1. Get the wizard’s current health and remember it.
    2. Get the wizard’s agility and remember it.
    3. Do the same for their wisdom.
    4. Get those last two, add them, and remember the result.
    5. Divide that by two and remember the result.
    6. Recall the wizard’s health and add it to that result.
    7. 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이 프로그램을 로딩할 때 바이트코드 검증을 하는 이유

인터페이스

  • 객체지향: 여러 타입 중 무엇인지를 모르는 값이 있으면 다형성으로 처리.
  • 인터페이스는 여러 자료형을 테스트하고 변환하는 가상 메서드를 다음과 같이 제공.
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) == 언리얼 에디터에 포함된 그래픽 스크립트 툴

출처

bytecode

This post is licensed under CC BY 4.0 by the author.

[게임 프로그래밍 패턴] Squencing Patterns: Update Method

[게임 프로그래밍 패턴] Behavioral Patterns: Subclass Sandbox