가상 머신 명령어를 인코딩한 데이터로 행동을 표현할 수 있는 유연함을 제공한다.

 

큰 규모의 프로젝트는 빌드에 시간이 오래 걸릴 수 있기 때문에, 자주 고칠 수 있어야 하는 코드는 데이터 파일로 빼서 쉽게 수정할 수 있도록 해야 한다.

 

기계어의 성능과 인터프리터 패턴의 안정성 사이에서 절충해야 한다.

 

가상 기계어(바이트 코드)와 가상 머신(가상 기계어를 실행하는 에뮬레이터 = 인터프리터)을 사용

 

11.3 패턴

명령어 집합은 실행할 수 있는 저수준 작업들을 정의한다.

명령어는 일련의 바이트로 인코딩된다.

가상 머신은 중간 값들을 스택에 저장해가면서 명령어를 하나씩 실행한다.

 

11.4 언제 쓸 것인가?

1. 게임 구현에 사용된 언어가 너무 저수준이라 만드는 데 손이 많이 가거나 오류가 생기기 쉬운 경우 고수준으로 표현하기 위해 사용한다.

2. 컴파일 시간이나 다른 빌드 환경 때문에 반복 개발하기가 너무 오래 걸리는 경우

3. 정의하려는 행동이 보안에 취약한 경우

 

바이트코드는 네이티브 코드보다는 느리므로 성능이 민감한 곳에는 적합하지 않다.

enum Instruction {
  INST_SET_HEALTH = 0x00,
  INST_SET_WISDOM = 0x01,
  INST_PLAY_AGILITY = 0x02,
  INST_PLAY_SOUND = 0x03,
  INST_SPAWN_PARTICLES = 0x04
};

class VM {
public: 
  void interpret(char bytecode[], int size) {
    for (int i = 0; i < size; i++) {
      char instruction = bytecode[i];
      
      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;
  ...
      }
    }
  }
}

대부분의 바이트코드 VM은 1바이트로 명령어를 표현한다. (256개의 명령어)

 

스택 머신

class VM {
  public:
    void interpret(char bytecode[], int size) {
    
    .. 위 예제와 동일 ..
    
    switch (instruction) {
    
      case INST_SET_HEALTH:
        int amount = pop();
        int wizard = pop();
        setHealth(wizard, amount);
        break;
    
    ....
    
      case INST_LITERAL: 
        int value = bytecode[++i];
        push(value);
        break;
    }
    
    
  }
  
  private:
    static const int MAX_STACK = 128;
    int stackSize_;
    int stack_[MAX_STACK];
  
    void push(int value){
      assert(stackSize_ < MAX_STACK);
      stack_[stackSize_++] = value;
    }
    
    int pop(){
      assert(stackSize_ > 0);
      return stack_[--stackSize_];
    }

명령어가 매개변수를 받을 때는 스택을 이용해서 전달할 수 있다.

리터럴 명령어는 바이트 코드에서 바이트 배열에서 다음 인덱스의 바이트 값을 읽는다.

위에서 구현한 숫자 리터럴의 경우 바이트 값을 그대로 int 변수에 저장한다.

 

GET_HEALTH 명령어와 ADD 명령어같은 명령어도 추가할 수 있다.

 

ADD 명령어는 스택에서 두 값을 pop하여 결과를 push하는 방식으로 구현할 수 있다.

 

고수준 행동을 바이트 코드로 만들어 주는 툴이 필요하다

GUI 툴, 텍스트 기반 언어 컴파일러를 사용할 수 있다.

 

11.7 디자인 결정

두가지 종류의 바이트 코드 VM

1. 스택 기반 VM

ㄴ 명령어가 짧다.

ㄴ 코드 생성이 간단하다.

ㄴ 하나의 행동을 수행하는데 필요한 명령어 개수가 많다.

 

2. 레지스터 기반 VM

ㄴ 명령어가 길다.

ㄴ 명령어의 개수는 줄어든다.

 

어떤 명령어를 만들어야 하는가?

1. 외부 원시명령

ㄴ 게임 코드에 접근하고 유저가 볼 수 있는 일들을 처리한다. (위 예제에서는 INST_SET_HEALTH 와 같은 것들)

 

2. 내부 원시명령

ㄴ 리터럴, 연산, 비교, PUSH, POP 같은 명령어들

 

3. 흐름 제어

ㄴ 반복문을 구현하기 위해 jump가 필요하다. (바이트 코드의 실행 위치를 옮기는 명령)

 

4. 추상화

ㄴ 고수준 언어에서의 함수 역할을 하기 위해 call(호출) 과 return(반환) 명령이 필요하다.물론 jump도 필요하다.

 

값을 어떻게 표현할 것인가?

1. 단일 자료형

ㄴ 위의 예제처럼 하나의 자료형을 사용하는 방법

 

2. 태그 붙은 변수

ㄴ 8비트를 사용한다고 가정하면 앞의 2비트는 자료형(int인지 double인지 string인지)를 구분하는데 사용, 나머지 6비트는 데이터로 사용(공용체)

ㄴ 런타임에서 값의 자료형을 확인할 수 있다는 장점이 있다.

ㄴ 타입을 검사하느라 클럭을 낭비한다.

enum ValueType {
  TYPE_INT,
  TYPE_DOUBLE,
  TYPE_STRING
};

struct Value {
  ValueType type;
  union {
    int intValue;
    double doubleValue;
    char* stringValue;
  };
};

 

3. 태그가 붙지 않은 공용체

ㄴ 공용체를 사용하되 자료형 태그는 따로 없어서 사용자가 알아서 해석해야 한다.

ㄴ 안전하지 않다.

 

4. 인터페이스

ㄴ Value 인터페이스를 만들고 IntValue FloatValue 등의 구현 클래스가 상속받게 하는 방법이다.

ㄴ 자료형마다 클래스를 만들어주는게 번거롭고 비효율적(다형성은 포인터를 통해서 동작하기 때문에)이다.

 

결론 : 되도록이면 자료형은 하나만 쓰거나 안되면 태그 붙은 변수를 쓰자.

바이트코드는 어떻게 만들 것인가?

1. 텍스트 기반 언어를 정의할 경우

ㄴ 문법을 정의해야 한다.

ㄴ 파서를 구현해야 한다.

ㄴ 문법 오류를 처리해야 한다.

 

2. UI가 있는 저작 툴을 만들 경우

ㄴ UI를 구현해야 한다.

ㄴ 오류가 적다.

ㄴ 이식성이 낮다. (UI 프레임워크가 OS에 종속적이기 때문에)

+ Recent posts