Lua ♡ C++

1 개요

    Cpp 프로그램에다가 루아 가상 머신을 집어넣어 보자. 목적은 간단한 함수 레벨의 튜토리얼이다. 유저 데이터니 클래스니 들어가면 상당히 복잡해지기 때문이다. 게다가 루아가 주(主)가 되지 않는 이상, 함수 정도로도 충분하다.

    루아를 C++와 연동시킬 때 루아와 C++, 둘 중에 어느 쪽을 주인으로 삼을지는 상당히 중요한 문제다. 여기서 주인이라 함은, 실제 객체가 어느 쪽에 존재하느냐다. 음. 뭔가 표현하기가 어려운데, 게임을 예로 들면 PC나 NPC 등이 어느 쪽에 존재하느냐 이 말이다.

    • 루아 쪽에 게임 객체가 존재하는 경우: C++ 쪽에서 루아 테이블을 받아와 조작한 후, 루아에게 알려주는 형식이 된다. 상당 분량의 코딩이 루아 쪽에서 이루어지고, 메인 루프도 루아 쪽에 존재하게 된다. 물론 그렇지 않을 수도 있다. 짜기 나름이니깐.
    • C++ 쪽에 게임 객체가 존재하는 경우: 루아는 C++ 쪽을 바로 액세스할 수 없으므로, 이전에 프로그래머가 가상 머신에다 등록한 래퍼 함수를 통해, C++ 상의 객체를 조작한 후 이를 C++에게 알려주는 방식이 된다.

    결국 스크립트 언어를 얼마나 주언어로 많이 사용할 것인가에 대한 이야기라고 할 수 있는데, 위에서도 이야기했듯이 여기에서는 C++ 쪽을 주(主)로 한다. 또한 LuaPlus 같은 애드인 라이브러리는 사용하지 않는다. 함수로만 끝낼 것이기 때문에 필요가 없다. :)


2 루아 설치

    일단 루아 라이브러리가 있어야 한다. [WWW]루아 공식 사이트에서 배포본을 다운로드받는다.

    배포본의 압축을 풀면 include 디렉토리와 src 디렉토리가 있을 것이다. 다른 디렉토리도 많다만 다 필요없다.

    • include - 라이브러리 헤더가 들어있다.
    • src - 라이브러리 소스가 들어있다.
      • lib - 루아 표준 라이브러리가 들어있다. 표준 라이브러리에 관한 내용은 LuaStandardLibrary 페이지를 참고하시라.
      • lua - 루아 인터프리터가 들어있다.
      • luac - 루아 컴파일러가 들어있다.

    이중에 실제로 필요한 것은 include, src, src/lib 디렉토리에 들어있는 파일 뿐이다. 따로 빌드해도 되지만 영 귀찮다면, lib 디렉토리에 들어있는 소스 파일도 프로젝트에 같이 추가해서 빌드해버려도 된다. 어쨌든 이렇게 빌드하면 헤더 파일과 라이브러리 파일을 가지게 된다!


3 구현 목표

    플레이어가 NPC를 클릭하면 플레이어의 명성에 따라 NPC가 다른 말을 출력하도록 만들어보자! GUI까지 다 집어넣어서 마우스 클릭 처리까지 하는 것은 아무리 생각해도 오바~이므로 콘솔 프로그램에서 간단히 시뮬레이션하는 정도로만 하자.


4 구현 순서

    4.1 C++ 쪽에서 게임 객체를 구현

      class Player
      {
      public: string name;// 플레이어의 이름
      int fame; // 플레이어의 명성
      };


      extern Player g_player;

      class NPC
      {
      public:
      string name; // NPC의 이름
      string click_script_name; // 플레이어가 클릭했을 때 실행할 스크립트의 이름
      NPC(const string& n = "", const string& clicked = "")
      : name(n), click_script_name(clicked) {}
      };
      일단 간단히 그냥 로컬에서 돌아가는 1인용 게임이라고 간주하고, 플레이어는 전역 변수로 둔다.

    4.2 루아에서 쓰일 래퍼 함수 구현

      // NPC의 대사를 화면에다 출력한다.
      int NPC_SAY(lua_State* L)
      {
      // 스택에서 메시지를 뽑아낸다.
      luaL_checkstring(pLuaState, -1);
      string msg(lua_tostring(pLuaState, -1));
      lua_pop(pLuaState, 1); cout << msg << endl;

      // 스택에 푸쉬한 리턴값의 갯수를 반환해야한다.

      // 루아 쪽으로 넘겨야할 반환값이 없으므로 0을 반환한다.
      return 0;
      }

      // 플레이어의 명성치를 루아 쪽으로 넘긴다.
      int GET_PLAYER_FAME(lua_State* L)
      {
      // 플레이어의 명성치를 루아 스택에다가 푸쉬한다.
      lua_pushnumber(L, g_player.fame);

      // 스택에 푸쉬한 리턴값의 갯수를 반환해야한다.
      // 플레이어의 명성을 푸쉬했으니, 1을 반환한다.
      return 1;
      }
      NPC의 대사를 출력하는 함수와 플레이어의 명성치를 루아 쪽으로 넘기는 함수다. C++ 쪽이 메인이 된다고 했으므로, 래퍼 함수는 그냥 약간의 파라미터를 주고받으며 C++ 쪽의 함수를 호출하는 함수가 대부분이 된다.

    4.3 루아 함수 구현

      clicked.lua
      player_fame = GET_PLAYER_FAME()
      if player_fame > 500 then
      NPC_SAY("Oh, I know you!")
      else
      NPC_SAY("Who are you?")
      end
      플레이어의 명성치가 500 이상일 때만 아는 체를 하는 NPC 스크립트이다.

    4.4 루아 호출 부분 구현

      // 플레이어가 NPC를 클릭한 경우에 호출한다.
      // 스크립트를 무사히 실행한 경우에는 false를 반환하고,
      // 무언가 에러가 생긴 경우에는 true를 반환한다.
      bool OnNpcClick(lua_State* pLuaState, NPC& npc)
      {
      const string& filename = npc.click_script_name;

      // 먼저 스크립트를 파싱해서 청크를 만든 다음, 스택에 푸쉬
      if (luaL_loadfile(pLuaState, filename.c_str()))
      {
      // 파싱 실패!
      return true;
      }


      // 트레이스 함수를 위에서 파싱한 청크 밑에다 끼워넣는다.
      // 트레이스 함수는 전역 테이블에 "_TRACEBACK"이란 이름으로 들어가있다.
      int base = lua_gettop(pLuaState);
      lua_getglobal(pLuaState, "_TRACEBACK");
      lua_insert(pLuaState, base);


      // 실행한다.
      return lua_pcall(pLuaState, 0, 0, base) != 0;
      }
      NPC를 클릭한 경우, 그 NPC의 click_script_name 파일 이름을 읽어와 해당 파일을 실행하게 된다. 디버깅을 좀 더 용이하게 하기 위해서 _TRACEBACK 함수를 스택의 맨 아래에다 집어넣고, lua_pcall을 호출하는 부분이 약간 복잡하긴 하다.

    4.5 통합

      extern "C" {
      #include <lua.h>
      #include <lualib.h>
      #include <lauxlib.h>
      }
      #include <iostream>
      #include <string>
      #include <vector>
      using namespace std;

      //////////////////////////////////////////////////////////////////////////////
      // class Player
      //////////////////////////////////////////////////////////////////////////////

      class Player
      {
      public:
      string name; // 플레이어의 이름
      int fame; // 플레이어의 명성
      };
      Player g_player;





      //////////////////////////////////////////////////////////////////////////////




      // class NPC

      //////////////////////////////////////////////////////////////////////////////


      class NPC
      {
      public:
      string name; // NPC의 이름
      string click_script_name; // 플레이어가 클릭했을 때 실행할 스크립트의 이름
      NPC(const string& n = "", const string& clicked = "")
      : name(n), click_script_name(clicked) {}};
      //////////////////////////////////////////////////////////////////////////////

      // Function Prototypes
      //////////////////////////////////////////////////////////////////////////////

      bool OnNpcClick(lua_State* pLuaState, NPC& npc);
      int NPC_SAY(lua_State* L);
      int GET_PLAYER_FAME(lua_State* L);
      //////////////////////////////////////////////////////////////////////////////
      void main()
      { // 루아 상태 객체를 생성한다.
      lua_State* pLuaState = lua_open();
      // 필요한 라이브러리들을 연다.


      lua_baselibopen(pLuaState);
      lua_tablibopen(pLuaState);
      //lua_iolibopen(pLuaState);
      //lua_strlibopen(pLuaState);
      lua_mathlibopen(pLuaState);
      lua_dblibopen(pLuaState);
      // 래퍼 함수들을 등록한다. 두번째 인자는 루아에서 쓰일 함수의
      // 이름인데, 혼란을 방지하기 위해서 실제 래퍼 함수의 이름과 똑같게
      // 하는 것이 좋다.

      lua_register(pLuaState, "NPC_SAY", NPC_SAY);
      lua_register(pLuaState, "GET_PLAYER_FAME", GET_PLAYER_FAME);
      // NPC들을 생성한다.
      typedef vector<NPC> NPC_VECTOR;
      NPC_VECTOR npcs;
      npclist.push_back(NPC("NPC1", "clicked.lua"));
      npclist.push_back(NPC("NPC2", "clicked.lua"));
      npclist.push_back(NPC("NPC3", "clicked.lua"));
      npclist.push_back(NPC("NPC4", "clicked.lua"));
      // 플레이어의 상태를 입력받는다.
      cout << "플레이어의 명성을 입력하시오 >> ";
      cin >> g_player.fame;
      // 선택을 입력받아, 클릭 함수를 호출한다.
      int index = 0; cout << "클릭할 NPC의 인덱스를 입력하시오 (0~3) >> ";
      cin >> index;
      OnNpcClick(pLuaState, npcs[index]);
      // 다 끝났으니 루아 상태 객체를 삭제한다.
      lua_close(pLuaState); }

      /////////////////////////////////////////////////////////////////////////////
      //
      // 플레이어가 NPC를 클릭한 경우에 호출한다.
      // 스크립트를 무사히 실행한 경우에는 false를 반환하고,
      // 무언가 에러가 생긴 경우에는 true를 반환한다.
      //////////////////////////////////////////////////////////////////////////////

      bool
      OnNpcClick(lua_State* L, NPC& npc)
      {
      const string& filename = npc.click_script_name;
      //먼저 스크립트를 파싱해서 청크를 만든 다음, 스택에 푸쉬
      if (luaL_loadfile(L, filename.c_str()))
      {
      // 파싱 실패!
      return true;
      }


      // 트레이스 함수를 위에서 파싱한 청크 밑에다 끼워넣는다.
      // 트레이스 함수는 전역 테이블에 "_TRACEBACK"이란 이름으로 들어가있다.
      int base = lua_gettop(L);
      lua_getglobal(L, "_TRACEBACK");
      lua_insert(L, base);

      // 실행한다.
      return lua_pcall(L, 0, 0, base) != 0;
      }

      //////////////////////////////////////////////////////////////////////////////
      // NPC의 대사를 화면에다 출력한다.
      //////////////////////////////////////////////////////////////////////////////
      int NPC_SAY(lua_State* L)
      {
      // 스택에서 메시지를 뽑아낸다.
      luaL_checkstring(L, -1);
      string msg(lua_tostring(L, -1));
      lua_pop(pLuaState, 1); cout << msg << endl;

      // 스택에 푸쉬한 리턴값의 갯수를 반환해야한다.
      // 루아 쪽으로 넘겨야할 반환값이 없으므로 0을 반환한다.
      return 0;
      }

      //////////////////////////////////////////////////////////////////////////////
      // 플레이어의 명성치를 루아 쪽으로 넘긴다.
      //////////////////////////////////////////////////////////////////////////////
      int GET_PLAYER_FAME(lua_State* L)
      {
      // 플레이어의 명성치를 루아 스택에다가 푸쉬한다.
      lua_pushnumber(L, g_player.fame);

      // 스택에 푸쉬한 리턴값의 갯수를 반환해야한다.
      // 플레이어의 명성을 푸쉬했으니, 1을 반환한다.
      return 1;
      }
      에러 처리는 대부분 생략했다. 컴파일은 안해봤는데, 루아 디렉토리만 include 패스에 잘 추가했다면, 아마 컴파일될 것이다. -_-;;;


5 요약

    결국 필수적으로 처리해야하는 일들을 요약해보자면...
    • 플레이어의 입력에 따라 실행할 스크립트를 판단할 수 있는 시스템이 필요하다. 위의 예에서는 NPC의 인덱스를 플레이어로부터 입력받아, 해당 NPC 객체의 멤버 변수로 들어가있는 스크립트 파일 이름을 이용했다.
    • 루아 쪽에서 C++ 쪽에 있는 객체의 상태를 쿼리할 수 있는 래퍼 함수가 필요하다. 위의 예에서는 플레이어의 명성치를 쿼리하는 GET_PLAYER_FAME 함수가 있다.
    • 루아 쪽에서 C++ 쪽에 있는 기능을 호출할 수 있는 래퍼 함수가 필요하다. 위의 예에서는 NPC의 대사를 화면에 출력하는 NPC_SAY 함수가 있다.


6 주의사항

  • 루아 관련 헤더를 C++ 프로젝트에다가 포함시킬 때에는 include 문을 아래와 같이 extern "C" 문으로 감싸줘야한다. 그렇지 않으면 링크 에러가 발생한다.
    extern "C" {     #include <lua.h>     #include <lualib.h>     #include <lauxlib.h> } 

이 글과 관련있는 글을 자동검색한 결과입니다 [?]

by 사랑합니다 | 2009/03/20 15:51 | 0xffffDEV | 트랙백 | 덧글(0)
트랙백 주소 : http://ojb1112.egloos.com/tb/2325239
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]

:         :

:

비공개 덧글



< 이전페이지 다음페이지 >