사용자 도구

사이트 도구


kb:luacppbinding

Lua C++ Binding

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

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

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

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

루아 설치

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

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

  • include - 라이브러리 헤더가 들어있다.
  • src - 라이브러리 소스가 들어있다.
    • lib - 루아 표준 라이브러리가 들어있다.
    • lua - 루아 인터프리터가 들어있다.
    • luac - 루아 컴파일러가 들어있다.

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

구현 목표

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

구현 순서

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인용 게임이라고 간주하고, 플레이어는 전역 변수로 둔다.

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

// 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++ 쪽의 함수를 호출하는 함수가 대부분이 된다.

루아 함수 구현

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 스크립트이다.

루아 호출 부분 구현

// 플레이어가 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을 호출하는 부분이 약간 복잡하긴 하다.

통합

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 패스에 잘 추가했다면, 아마 컴파일될 것이다. -_-;;;

요약

결국 필수적으로 처리해야하는 일들을 요약해보자면…

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

주의사항

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

extern "C" 
{
    #include <lua.h>
    #include <lualib.h>
    #include <lauxlib.h>
}

kb/luacppbinding.txt · 마지막으로 수정됨: 2014/11/06 16:47 (바깥 편집)