마지막으로 간단하게 게임에 어떤 식으로 적용시킬 것인가에 대한 아이디어과 코드들을 정리해보겠습니다. (거의 첨부된 셈플 설명식입니다만...)

1. 자바 오브젝트 생성

클래스 오브젝트의 맴버 변수(fields)들을 할당하는 것으로 JClass 에 MakeInstance 에 해당됩니다. 생성 함수를 통해 리턴되는 값은 JObject 형을 가지고 있고, 각 맴버와 맴버에 대한 정보, 상위 오브젝트에 대한 정보들이 담겨져 있습니다. 생성한 오브젝트의 맴버에 접근하기 위해선 JObject::Find 함수를 사용합니다.

생성되는 JObject 는 자바 클래스 파일의 Fields 내용에 기반합니다. Fields 에 정의된 것들은 static 인것과 아닌 것으로 구분할 수 있는 데, static 인 것은 내부적으로 클래스 안에서 사용되며, 오브젝트 생성시에 참조되는 것은 static 이 아닌 fields 맴버입니다.
가비지 컬렉팅시 하위까지 검색되어야 하는 레퍼런스 함수의 경우 앞부분에 소팅되도록 프로그래밍 되어 있습니다. (조건의 편의를 위해서...)

생성된 오브젝트는 사용되지 않을 경우 가비지 컬렉트시 삭제되는 데, 사용중인지에 대한 것은 JVM::Activate 와 JVM::Deactivate 함수를 이용합니다.

인스턴스를 만들 때는 그 클래스의 "<init>" 생성자를 호출해줘야 합니다. (클래스 Fields 맴버 초기화 루틴등이 포함되어 있습니다.) 바이트 코드안에는 오브젝트가 생성될 때는 자동으로 생성자를 호출해주지만 jvm 을 사용하는 레벨에서는 MakeInstance 로 생성한 후에 직접 호출해주어야 합니다.

클래스 파일이 처음 읽어질때는 static 변수들 세팅과 함께 "<clinit>" 메소드를 호출합니다. (값들이 초기화되며, 정해진 작업들을 하게 됩니다.) 이러한 과정은 클래스 파일 읽어질때 딱 한번만 이루어집니다.

오브젝트가 가비지 컬렉터에 의해 삭제될 경우에는 finalize 함수가 호출됩니다. 다만 finalize 함수가 호출되는 시기는 정해져있지 않기 때문에 처리해야할 것이 아니라면 finalize 함수에 넣지 않도록 합니다. (일반적으로 꼭 finalize 에 넣어야할 일은 거의 없다죠. ^^)

2. 함수호출

함수 메소드를 호출할 때 함수형을 같이 넘기는 데, 이 형은 인자로 넘어오는 local 변수들의 타입과 개수를 알 수 있습니다. 함수형 (descriptor)의 각 아스키은 다음과 같은 의미를 가지고 있습니다.

B : signed byte
C : unicode character
F : sigle-precision floating-point value
I : interger
S : signed short
Z : true or flase
D : double-presision floating-point value *
J : long integer *
[ : array dimension (reference)
L<classname>; : a instance of class (reference)

Descripter는 위와 같으며 셈플의 jvm 에서는 *를 제외한 값들은 하나의 슬롯만 사용합니다. (*는 두개의 슬롯을 사용합니다. - 물론 32비트 시스템을 기준으로 했습니다.) 함수 호출시 사용하는 method descriptor 은

(ParameterDescripter*)ReturnDescriptor

의 구성을 하며 다음과 같이 표현됩니다.

Object mymethod(int i, double d, Thread t)
   (IDLjava/lang/Thread;)Ljava/lan/Object;

int align2grain(int i, int grain)
   (II)I

void whileInt()
   ()V

자바함수를 사용하기 위해서는 위의 형태를 정확히 사용해야 합니다.

함수형은 내부적으로는 함수 찾을 때 함수 메소드 이름과 함께 참조되기 때문에 함수 오버라이딩의 기능에도 적용된다고 할 수 있습니다. void test(int a) 와 void test(double a)는 각 "test" 라는 이름을 갖지만 함수형은 각 "(I)V", "(D)V"를 가지기 때문에, 구분 됩니다.

클래스 메소드는 기능상 static 와 static 이 아닌 함수 두가지로 구분할 수 있습니다. 첫번째 local 값이 오브젝트 인가 아닌가의 차이를 가지는 데, 어긋나게 프로그래밍을 하게 되면 스택이 엉망이 되므로 주의를 해야 합니다. (내부적으로 바이트 코드는 완벽하게 맞춰져 있으니 외부에서 함수 호출하거나 직접 native 코드로 메소드를 작성할 때 약간만 주의 하면 됩니다.)

간단하게 c 의 가변인자를 이용해서 jvm 코드에서 함수 호출이 쉽도록 구성해보면 아래와 같습니다. (원래는 local 에 해당하는 값들을 일일이 push 해주어야 합니다.)

bool Execute(JVM * jvm, const char * classname, const char * funcname, const char *descript, ...)
{
   int cnt = JVM::CountLocal4Arg(descript), i;
   va_list marker;

   va_start(marker, descript);

   for(i=0; i<cnt; i++)
       jvm->Push((jslot)va_arg( marker, int));

   va_end(marker);

   if (jvm->Execute(classname, funcname, descript) == false) {
       jvm->SkipLocal(descript, true);
       return false;
   }
   return true;
}

실제 호출하는 함수가 없을 경우에는 SkipLocal 함수를 이용해서 스택을 맞춰주어야 합니다. 위의 함수는 static 인 함수형을 위한 함수이고 오브젝트를 참조하는 메소드인 경우에는 아래처럼 구성해야 합니다.

bool Execute(JVM * jvm, JObject * obj, const char * funcname, const char *descript, ...)
{
   int cnt = JVM::CountLocal4Arg(descript), i;
   va_list marker;

   va_start(marker, descript);

   jvm->Push((jslot) obj);

   for(i=0; i<cnt; i++)
       jvm->Push((jslot)va_arg(marker, int));

   va_end(marker);

   if (obj->Execute(jvm, funcname, descript, NULL) == false) {
       jvm->SkipLocal(descript);
       return false;
   }
   return true;
}

다른 점은 오브젝트의 포인터를 제일 먼저 push 해준다는 것이고, 함수 실행시 obj의 하위 필드부터 순서대로 찾아가면서 실행한다는 점입니다. (하위에서 찾기 때문에 가상 함수의 기능을 할 수 있습니다.)
역시 함수 호출에 실패하면 SkipLocal 로 스택을 원래대로 돌려주어야 합니다. (이 경우 일반적으로 오류일 확률이 높기 때문에 assert 처리를 해서 확인해볼 필요가 있습니다.)

3. native 클래스 정의 사용

첨부된 소스의 인터페이스에서 native 클래스를 정의해서 사용하고자 한다면 JClass 형의 원형을 이름과 함께 jvm 에 추가해주면 됩니다. 이전 셈플에서처럼 자바에서

class SubSystem
{
   public native static void report(int id, String str);
} ;

선언되어 있다면 C++ 에서

class    JDumbSystem : public JClass
{
public :
   bool Execute(JVM * jvm, const char *funcname, const char * descript) {
       jslot * local;
       if (!strcmp(funcname, "report") && !strcmp(descript, "(ILjava/lang/String;)V")) {
           local = jvm->LoadLocal(descript, true);
           printf("%02d : %sn", local[0], (char*) local[1]);
           return true;
       }
       return false;
   }
} ;

jvm.AddClass(new JDumbSystem, "SubSystem");

식으로 정의해서 선언해서 사용할 수 있습니다. (SubSystem 의 경우 인스턴스를 만들지 않기 때문에 MakeInstance 는 정의하지 않았습니다.)
게임의 메인 매니저들과 통신하고자 할 때 유용하게 사용될 수 있을 것입니다. (샘플에 Textout 에 접근하는 인터페이스처럼...)

좀 더 자세히 알아보기 위해 java/lang/StringBuffer 의 일부분을 c++ 코드로 작성해서 jvm 에 등록하는 것을 살펴보겠습니다. (구현할 것은 append 와 toString 입니다.)

class    JStringBuffer : public JClass
{
public :
   JObject * MakeInstance(JVM * jvm);

   bool Execute(JVM * jvm, const char *funcname, const char * descript);

private :
   struct    Elem {
       char * ptr;
       int capacity;
       int len;
   } ;

   void Append(JVM * jvm, Elem * elm, char *text);
} ;

처럼 구성 할 수 있습니다. 먼저 MakeInstance를 살펴보면 먼저 JClassLoader의 것과 비슷함을 알 수 있습니다.

JObject * JStringBuffer :: MakeInstance(JVM * jvm) {
   JObject * obj;
   obj = (JObject*) jvm->Alloc(sizeof(JObject), 1, 2, true);
   obj->m_classname = "java/lang/StringBuffer";
   obj->m_fieldslot = (jslot *) jvm->Alloc(sizeof(Elem), 1, 1);
   return obj;
}

다만 자바 레벨에서 이 클래스의 맴버를 억세스할 일은 없다고 가정하면 m_fieldname, m_fieldn 는 무시할 수 있습니다.

void * JVM :: Alloc(int size, int count, int refcnt, bool jobject);

위의 함수가 JVM 에서 Alloc 의 기본형입니다. 이 함수를 참고하여 메모리 할당하는 부분을 살펴보겠습니다.
기본적으로 리턴되는 값은 JObject 형이고 필요한 것도 JObject 이므로 size 는 sizeof(JObject) 가 됩니다. 카운트는 length 명령이 호출되는 배열 레퍼런스가 아니면 큰 의미가 없으므로 1 로 설정합니다. 그리고 JObject 에서는 레퍼런스로 사용되는 것은 m_parent, m_fieldslot 이므로 2가 됩니다. (단, 레퍼런스로 설정되는 변수는 항만 앞부분에 선언되어야 합니다.) 그리고 할당하는 메모리가 JObject 형이므로 true 값을 넘겨줍니다. (이 프래그는 나중에 삭제시 finalize 함수가 호출되는 지 판단할 때 사용됩니다.)

이제 중요한 부분은 m_fieldslot 입니다. 이 부분에 실제 메모리가 저장되는 데, 여기서는 Elem 으로 선언한 형태로 저장할 것이기 때문에 sizeof(Elem)의 사이즈로 할당하는 것입니다. 여기서 Elem::ptr 은 레퍼런스로 사용될 것이므로 앞부분에 선언하고 할당할 때 레퍼런스 함수개수에 추가합니다. (이렇게 설정된 맴버는 JVM::Alloc 를 이용해서 할당할 경우 별도의 해제 없이 자동으로 가비지 컬렉트됩니다. 만약 가비지 컬렉팅할 필요가 없다면 따로 레퍼런스로 체크할 필요는 없습니다. - 잘 생각하셔야...)

A. JObject 메모리를 할당
B. JVM 에 등록된 함수 명을 m_classname 에 저장
C. 필드 맴버 할당 (할당시 레퍼런스 개수 잘 표시)

이제 필요한 Jobject는 구성되었으므로, 실제는 메쏘드가 호출되는 부분을 살펴보겠습니다.

bool JStringBuffer :: Execute(JVM * jvm, const char *funcname, const char * descript)
{
   jslot * local;
   JObject * obj;
   Elem * elm;

   if (!strcmp(funcname, "<init>")) {
       jvm->SkipLocal(descript);
       return true;
   }    else
   if (!strcmp(funcname, "append")) {
       char text[64];

       local = jvm->LoadLocal(descript);

       switch(descript[1]) {
           case 'F' : sprintf(text, "%f", local[1]); break;
           case 'D' : sprintf(text, "%g", *(double*)(&local[1])); break;
           case 'I' : sprintf(text, "%d", local[1]); break;
           case 'L' : sprintf(text, "%s", local[1]); break;
           default : return false;
       }

       obj = (JObject*) local[0];
       elm = (Elem*) obj->m_fieldslot;

       Append(jvm, elm, text);

       jvm->Push((jslot) obj);
       return true;
   }    else
   if (!strcmp(funcname, "toString")) {
       local = jvm->LoadLocal(descript);
       
       obj = (JObject*) local[0];
       elm = (Elem*) obj->m_fieldslot;

       jvm->Push((jslot) elm->ptr);
       return true;
   }

   return false;
}

특별한 것은 없고, 단순히 메소드 이름 (funcname) 과 함수형 (descipt) 를 이용해서 필요한 기능을 작성해주기만 하면 됩니다.

local[0] 이 JObject 임을 이용해서 obj를 얻을 수 있고, 이 값을 이용해서 실제 fieldslot 에 해당하는 elm 을 얻을 수 있습니다.
리턴되는 값은 jvm 에 Push 를 해주면 됩니다.

append 명령은 현제 오브젝트가 가지고 있는 문자열에 해당 데이타 값을 추가하라는 것으로 위에서는 추가될 내용을 text 에 출력한 후에 추가하는 식으로 구현 되어 있음을 볼 수 있습니다.
toString 의 경우 실제 내용을 String 형으로 출력해주는 함수인데 이 경우 char*의 문자열 포인트 값을 넘겨줍니다. (내부적으로 String 와 char* 는 동일하게 사용했습니다.) append 처럼 리턴 값을 push 해주면 됩니다.

메소드를 작성할 경우

A. 함수이름과 함수형을 이용해서 처리할 내용을 결정
B. LoadLocal 함수를 이용해서 인자로 넘어오는 값들을 얻어냄
C. 처리를 하기
D. 리턴 값이 필요한 경우 push 해 주기

의 과정을 통해서 처리해주면 됩니다. 만약 정의되지 않은 함수를 실행하게 되면 스택이 엉망이 될 수 있으므로 assert 등을 통해서 반드시 체크해주어야 할 것입니다.

참고 : 위의 클래스를 추가할 경우

String text = "저의 이름은 " + m_name + "이고, 나이는 " + m_age + "살이랍니다.";

같은 처리가 가능해집니다. (내부적으로 StringBuffer 를 할당해서 "저의 이름은 ", m_name, "이고, 나이는 ", m_age, "살이랍니다."를 순서대로 append 한 후에 toString 으로 리턴된 String 값을 text 함수에 저장합니다.)

실행파일의 c++ 의 native 코드로만 작성된 클래스를 추가할 경우 실제 게임루틴과 통신이 용이합니다. 만약 AI 에서 사운드를 출력한다거나 할 일이 있을 때 시스템 클래스에 "PlaySound":"(Ljava/lang/String;)V" 함수를 호출하도록 만들면 될 것입니다. (여러가지 기능은 추가해야 겠죠.)

4. 맴버 메소드로 native 함수 사용

그리고 만약 java에서 인공지능부분을 처리하되 필요한 메소드는 native 로 작성하고 싶을 때는 약간만 구성을 변형하면 됩니다.

class    JNPC : public JClass
{
public :
   JNPC(char * classname, JVM *jvm) {
       m_loader = jvm->LoadClass(classname);
   }
   bool Execute(JVM * jvm, const char *funcname, const char * descript) {
       jslot * local;
       if (!strcmp(funcname, "Scream") || !strcmp(funcname, "(Ljava/lang/String;)V"))         {
           local = jvm->LoadLocal(descript);
           Scream((char*) local[1]);
           return true;
       }    else
       if (!strcmp(funcname, "Heal") || !strcmp(funcname, "()V")) {
           local = jvm->LoadLocal(descript);
           obj = (JObject*) local[0];

           mem = obj->Find("m_energy");
           *mem = 100;
           return true;
       }    else {
           ...
       }
       return m_loader->Execute(jvm, funcname, descript);
   }
   JObject * MakeInstance(JVM * jvm) {
       return m_loader->MakeInstance(jvm);
   }

private :
   JClass * m_loader;

   void Scream(char * fname);
} ;

jvm.AddClass(new JNPC("NPC", &jvm), "NPC");

처럼 클래스를 만들어서 등록을 해두면 아래 같이 메소드를 선언해서

class NPC
{
   public native void Scream(String msg);
   public native void Heal();
   ...
} ;

native 함수에 접근할 수 있을 것입니다.

5. 응용 아이디어

- 메모리 관련 함수 작성

만약 JNI 를 활용한다면 큰 문제가 안 되겠지만 이런 식으로 메모리 클리어나 복사를 하게 되면

for(int i=0; i<1000; i++)
   data[i] = 0;

불필요한 부하로 별로 안 좋을 수 있습니다. (JNI 환경이라면 당연히 최적화되겠지만... ) 이 경우는 그냥 C 처럼 memcpy, memset 같은 native 함수를 만들어 사용하면 도움이 됩니다.

- 버추얼 함수 테이블 사용

첨부한 간이 jvm 의 단점이라면 함수나 맴버변수의 이름을 일일이 찾는 다는 점일 텐데, 소팅해서 이진 검색을 하거나 해시리스트를 쓰는 것도 방법이겠고, "test":"()V" 호출을 "test":"2번째 함수 호출" 같은 개념의 참조 테이블을 만들어서 사용하면 속도 향상이 있을 수도 있습니다. (그리고 인자 개수등도 미리 계산해두면 도움이 될 거 같습니다.)

- 기타

궁극적인 것은 아예 native 코드로 컴파일해서 그냥 코드를 실행해 버리는 것일 겁니다. 게임 프로젝트로는 부담되는 작업이겠고, 그냥 재미로 한다면... ^_^

6. 샘플코드

샘플 코드는 저번 jvm 을 기반으로 약간 변형한 셈플입니다. jvm 자체도 약간 바뀌었는 데, 개선한 거라기 보다는 이것저것 시도 해 보던 중이라고 보시면 맞을 거 같습니다. 저번 소스와는 다르게 local 변수를 스택메모리 부분을 활용 하도록 만들었습니다.

전체적으로 의도한 것은 기본적인 게임 구성에 이벤트 방식으로 스크립트를 사용하는 오브젝트 처리입니다.

내부적으로 TimerObject 란 것을 보면 폴링(Polling) 과정을 통해 이벤트가 발생할 것인지 체크하고 그 이벤트가 발생할 조건에 스크립트를 호출합니다. (샘플은 단순하게 n 프레임에 한번씩 호출하는 조건입니다. ^^)

결과는 아래와 같습니다.

frame : 3       name : dragon
frame : 6       name : dragon
frame : 7       name : cat
frame : 9       name : dragon
frame : 10      name : reddragon
frame : 12      name : dragon
frame : 14      name : cat
frame : 15      name : dragon

이 결과는 오브젝트 생성시 Dragon 은 3프레임마다, Cat 은 7 프레임마다, RedDragon 은 10프레임마다 이벤트가 발생하게 만든 결과입니다. (당연한 얘기지만 셈플처럼 String 을 매 이벤트 마다 갱신해 버리면 안 쓰이는 가비지 메모리가 많아져서 좋지 않은 영향을 줄 가능성이 높습니다. 예제니 그냥 보시고, 실제 자바로 작성할 때는 자바언어에 어울리는 자바 코드를... T_T)

7. 마치며

시도와 테스트를 반복하고 있는 중에 정리하려다보니, 많이 부족하네요. 그래도 시도 차원에서 정리를 해 봤습니다.
좀 더 공부해서 잘만 활용한다면 좀 더 효과적으로 게임을 개발할 수 있을 거 같다는 생각입니다.

끝으로 자바 관련해서 도움이 될만한 책 추천합니다. 완전 초보분에겐 마지막 책을, 자바의 아버지 고슬링이 직접 쓴 책으로 공부하고 싶은 분은 두번째 책을, 괜찮은 코드에 관심있는 분에겐 첫번째 책을 추천합니다. (사실 접해 본 책이 많지 않아서 최고의 책들이라고는 말 못하겠지만, 나름대로 이 정도면 좋다는 생각이 드네요.)

"Refactoring : Improving the Design of Existing Code"
John Brant , Kent Beck , Martin Fowler , William Opdyke
Addison-Wesley
(번역서 : 리펙토링 / 대청)

"The Java Language Specification" (2E)
Bill Joy , Guy Steele , James Gosling
Addison-Wesley

"Beginning Java 2 SDK"
Ivor Horton
Wrox Press
(번역서 : Beginning Java 2 SDK / 정보문화사)

댓글을 달아 주세요

  1. noerror 2003/03/03 13:37  댓글주소  수정/삭제  댓글쓰기

    자바 기본 클래스 파일을 구현한 GNU 자바 클래스 소스는 이 사이트 ( http://www.gnu.org/software/classpath , ftp://alpha.gnu.org/gnu/classpath )에서 구할 수 있습니다.

  2. Tmdwn 2003/03/01 06:34  댓글주소  수정/삭제  댓글쓰기

    자료 공유 고맙습니다 : )

  3. ass fuck xxx 2007/10/18 06:41  댓글주소  수정/삭제  댓글쓰기

    많은 감사 우수한 위치! 나는 너의 웹사이트를 사랑한다!