사용자 도구

사이트 도구


kb:luacppbinding

차이

문서의 선택한 두 판 사이의 차이를 보여줍니다.

차이 보기로 링크

kb:luacppbinding [2014/11/06 16:47] (현재)
줄 1: 줄 1:
 +====== Lua C++ Binding ======
 +[[Cpp]] 프로그램에다가 루아 가상 머신을 집어넣어 보자. 목적은 간단한 함수 레벨의 튜토리얼이다. 유저 데이터니 클래스니 들어가면 상당히 복잡해지기 때문이다. 게다가 루아가 주(主)가 되지 않는 이상, 함수 정도로도 충분하다.
 +
 +루아를 C++와 연동시킬 때 루아와 C++, 둘 중에 어느 쪽을 주인으로 삼을지는 상당히 중요한 문제다. 여기서 주인이라 함은, 실제 객체가 어느 쪽에 존재하느냐다. 음. 뭔가 표현하기가 어려운데,​ 게임을 예로 들면 PC나 NPC 등이 어느 쪽에 존재하느냐 이 말이다.
 + 
 +  * **루아 쪽에 게임 객체가 존재하는 경우:** C++ 쪽에서 루아 테이블을 받아와 조작한 후, 루아에게 알려주는 형식이 된다. 상당 분량의 코딩이 루아 쪽에서 이루어지고,​ 메인 루프도 루아 쪽에 존재하게 된다. 물론 그렇지 않을 수도 있다. 짜기 나름이니깐.
 +  * **C++ 쪽에 게임 객체가 존재하는 경우:** 루아는 C++ 쪽을 바로 액세스할 수 없으므로,​ 이전에 프로그래머가 가상 머신에다 등록한 래퍼 함수를 통해, C++ 상의 객체를 조작한 후 이를 C++에게 알려주는 방식이 된다.
 +
 +결국 스크립트 언어를 얼마나 주언어로 많이 사용할 것인가에 대한 이야기라고 할 수 있는데, 위에서도 이야기했듯이 여기에서는 C++ 쪽을 주(主)로 한다. 또한 LuaPlus 같은 애드인 라이브러리는 사용하지 않는다. 함수로만 끝낼 것이기 때문에 필요가 없다. :)
 +
 +
 +====== 루아 설치 ======
 +일단 루아 라이브러리가 있어야 한다. [[http://​www.lua.org | 루아 공식 사이트]]에서 배포본을 다운로드받는다.
 +
 +배포본의 압축을 풀면 include 디렉토리와 src 디렉토리가 있을 것이다. 다른 디렉토리도 많다만 다 필요없다.
 +
 +  * include - 라이브러리 헤더가 들어있다.
 +  * src - 라이브러리 소스가 들어있다.
 +    * lib - 루아 표준 라이브러리가 들어있다.
 +    * lua - 루아 인터프리터가 들어있다.
 +    * luac - 루아 컴파일러가 들어있다.
 +
 +이중에 실제로 필요한 것은 include, src, src/lib 디렉토리에 들어있는 파일 뿐이다. 따로 빌드해도 되지만 영 귀찮다면,​ lib 디렉토리에 들어있는 소스 파일도 프로젝트에 같이 추가해서 빌드해버려도 된다. 어쨌든 이렇게 빌드하면 헤더 파일과 라이브러리 파일을 가지게 된다!
 +
 +
 +====== 구현 목표 ======
 +플레이어가 NPC를 클릭하면 플레이어의 명성에 따라 NPC가 다른 말을 출력하도록 만들어보자! GUI까지 다 집어넣어서 마우스 클릭 처리까지 하는 것은 아무리 생각해도 오바~이므로 콘솔 프로그램에서 간단히 시뮬레이션하는 정도로만 하자.
 +
 +
 +====== 구현 순서 ======
 +==== C++ 쪽에서 게임 객체를 구현 ====
 +<code cpp>
 +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) {}
 +};
 +</​code>​ 일단 간단히 그냥 로컬에서 돌아가는 1인용 게임이라고 간주하고,​ 플레이어는 전역 변수로 둔다.
 +
 +==== 루아에서 쓰일 래퍼 함수 구현 ====
 +<code cpp>
 +// 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;
 +}
 +</​code>​ NPC의 대사를 출력하는 함수와 플레이어의 명성치를 루아 쪽으로 넘기는 함수다. C++ 쪽이 메인이 된다고 했으므로,​ 래퍼 함수는 그냥 약간의 파라미터를 주고받으며 C++ 쪽의 함수를 호출하는 함수가 대부분이 된다.
 +
 +==== 루아 함수 구현 ====
 +clicked.lua
 +<code lua>
 +player_fame = GET_PLAYER_FAME()
 +if player_fame > 500 then
 +    NPC_SAY("​Oh,​ I know you!")
 +else
 +    NPC_SAY("​Who are you?")
 +end
 +</​code>​ 플레이어의 명성치가 500 이상일 때만 아는 체를 하는 NPC 스크립트이다.
 +
 +==== 루아 호출 부분 구현 ====
 +<code cpp>
 +// 플레이어가 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;
 +}
 +</​code>​ NPC를 클릭한 경우, 그 NPC의 click_script_name 파일 이름을 읽어와 해당 파일을 실행하게 된다. 디버깅을 좀 더 용이하게 하기 위해서 _TRACEBACK 함수를 스택의 맨 아래에다 집어넣고,​ lua_pcall을 호출하는 부분이 약간 복잡하긴 하다.
 +
 +==== 통합 ====
 +<code cpp>
 +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;
 +}
 +</​code>​
 +에러 처리는 대부분 생략했다. 컴파일은 안해봤는데,​ 루아 디렉토리만 include 패스에 잘 추가했다면,​ 아마 컴파일될 것이다. -_-;;;
 +
 +
 +====== 요약 ======
 +결국 필수적으로 처리해야하는 일들을 요약해보자면...
 +
 +  * 플레이어의 입력에 따라 실행할 스크립트를 판단할 수 있는 시스템이 필요하다. 위의 예에서는 NPC의 인덱스를 플레이어로부터 입력받아,​ 해당 NPC 객체의 멤버 변수로 들어가있는 스크립트 파일 이름을 이용했다.
 +  * 루아 쪽에서 C++ 쪽에 있는 객체의 상태를 쿼리할 수 있는 래퍼 함수가 필요하다. 위의 예에서는 플레이어의 명성치를 쿼리하는 GET_PLAYER_FAME 함수가 있다.
 +  * 루아 쪽에서 C++ 쪽에 있는 기능을 호출할 수 있는 래퍼 함수가 필요하다. 위의 예에서는 NPC의 대사를 화면에 출력하는 NPC_SAY 함수가 있다.
 +
 +
 +====== 주의사항 ======
 +루아 관련 헤더를 C++ 프로젝트에다가 포함시킬 때에는 include 문을 아래와 같이 <​nowiki>​extern "​C"</​nowiki>​ 문으로 감싸줘야한다. 그렇지 않으면 링크 에러가 발생한다.
 +<code cpp>
 +extern "​C" ​
 +{
 +    #include <​lua.h>​
 +    #include <​lualib.h>​
 +    #include <​lauxlib.h>​
 +}
 +</​code>​
 +----
 +  * see also [[Lua]], [[LuaBinaryExtension]]
  
kb/luacppbinding.txt · 마지막으로 수정됨: 2014/11/06 16:47 (바깥 편집)