1. 程式人生 > >手把手教你架構3D引擎高階篇系列八

手把手教你架構3D引擎高階篇系列八

本篇部落格是給讀者介紹引擎底層如何與Lua進行結合,方便開發者直接使用指令碼程式設計,給讀者介紹的是最基本的C++與Lua的互動,引擎的封裝會在下篇部落格中具體講解。
為什麼選擇Lua,通常開發者會使用Json,XML,txt等等。相比Lua有哪些優點呢?
a、除了Lua庫,在沒有使用其他庫可以使用。
b、可以在檔案中使用不同的公式,例如:some_variable = math.sqrt(2)* 2
c、它非常輕巧,速度快
d、它是在MIT許可下換句話說程式碼是開源的,因此我們可以以任何想要的方式使用它
e、它是用C語言編寫的,幾乎可以編譯任何C編譯器
f、可以使用表對資料進行分類,易於編輯和閱讀
既然這麼多優點,我們就採用Lua作為指令碼使用,Lua與引擎的結合還是非常重要的。我們下面先從基礎的講起,慢慢給讀者深入。先看看Lua語言編寫的指令碼:

player = {    pos = {         X = 20,         Y = 30,    },    filename = "res/images/player.png",    HP = 20,-- you can also have comments}

我們要使用Lua指令碼中的內容,需要執行下面的語句:

LuaScript script("player.lua");
std::string filename = script.get("player.filename");
int posX = script.get("player.pos.X");

如何使用Lua與C++繫結,讀者可以參考網址:

http://lua-users.org/wiki/BindingCodeToLua
接下來我們分析一下上面的Lua指令碼,Player表是全域性的,因此需要通過lua_getglobal方法獲取它, 現在Player表將位於堆疊頂部,使用lua_getfield函式獲取pos表,然後使用變數x,如下圖所示:
在這裡插入圖片描述
對應的程式碼下載地址如下所示:
https://github.com/EliasD/unnamed_lua_binder
使用上面的程式碼時,不要忘記把Lua的庫加進工程裡面。我們可以使用Lua做很多事情,如下所示:

-- somefile.luasome_array = { 1, 2, 3, 4, 5, 6}
std::vector v = script.getIntVector("some_array")

如何清理Lua堆疊?我們可以使用lua_gettop函式返回陣列中元素的數量,從而弄清楚我們必須彈出多少項。

void clean()
 {   
  int n = lua_gettop(L);  
    lua_pop(L, n);
 }

下面實現的介面:

std::vector LuaScript::getIntVector(const std::string& name)
 {    
 std::vector<int> v;  
   lua_getglobal(L, name.c_str());    
   if(lua_isnil(L, -1))
    {       
     return std::vector();   
      }  
        lua_pushnil(L);   
         while(lua_next(L, -2))
          {       
         	      v.push_back((int)lua_tonumber(L, -1));    
               lua_pop(L, 1);  
                 }  
                   clean();   
                    return v;
           }

它們是如何工作的?首先,我們獲取全域性表並檢查是否找到它。 如果它是nil(尚未定義,或者…,nil),我們只返回一個空向量。
然後我們將nil值推到Lua堆疊的頂部, 這是因為lua_next的作用,它從堆疊中彈出鍵值,然後將鍵值對推送到堆疊。 如果陣列中沒有更多元素,我們清理堆疊並返回結果向量。
還可以建立一個函式來獲取字串或浮點陣列, 這需要更改的是向量型別和一些強制轉換(請不要忘記將lua_tonumber更改為lua_tostring)
使用Lua指令碼程式設計,因為它是指令碼,對效能要求比較高的程式碼建議不要使用Lua指令碼,直接使用C或者C++程式設計。
通過案例的方式給讀者介紹,比如下面程式碼:

function sum(x, y)
return x + y
end

對應的C++程式碼如下所示:

int sum(int x, int y)
 {   
  lua_State* L = luaL_newstate();   
   if (luaL_loadfile(L, "sum.lua") || lua_pcall(L, 0, 0, 0))
    {       
     std::cout<<"Error: failed to load sum.lua"<<std::endl;     
        return 0;    
        }    
         lua_getglobal(L, "sum");  
           lua_pushnumber(L, x); 
              lua_pushnumber(L, y);    
               std::cout<<"loaded"<<std::endl;  
                lua_pcall(L, 2, 1, 0);   
                  int result = (int)lua_tonumber(L, -1);    
                  lua_pop(L, 1);   
                   return result;
   }
                   

接下來給讀者分析一下,首先,我們建立新的Lua狀態並載入檔案。注意:這只是一個示例,我們應該將狀態與載入的檔案保持在某個位置,以防止每次使用函式時重新載入,因為這樣做效率不高。
然後我們在Lua堆疊的頂部得到名為sum的全域性函式。 使用lua_pushnumber函式然後我們推送2個變數,現在我們的堆疊看起來像這樣:
在這裡插入圖片描述
第一個是lua_State,第二個是你要呼叫的函式中的引數個數, 第三是你希望返回的功能。 第四是錯誤程式碼(應該在Lua參考手冊中閱讀)
在我們呼叫一個函式之後,它會從它的引數中彈出堆疊。 堆疊中剩下的唯一東西是值sum函式返回,所以現在我們可以用lua_tonumber獲取它的值並彈出它。
說了這麼多,現在給讀者介紹如何使用它們?
假設我們在遊戲中實現NPC, 當玩家靠近NPC時,NPC會做不同的事情。
我們經常會安排玩家與NPC的一些對話,比如說“讓我幫助你”,而另一個NPC只是說“你好”並且什麼都不做。我們的互動程式碼可能如下所示:

if(isPressed(ACTIVATION_BUTTON)) 
{   
 Character* character = find_nearby_character(player);   
  if(character) 
  {      
    character->interact(player);   
     }
     }

如何實現這個互動方法?我們很容易想到使用列舉,如下所示:

enum CharacterType { Player, Talker, Healer };
CharacterType type;

函式如下所示:

void Character::interact(Character* secondCharacter)
 {   
  switch(type) 
  {        
  case Character::Player:            
  break;        
  case Character::Talker:          
    say("Hello");          
      break;       
       case Character::Healer:        
           say("Let me help you");      
                 heal(secondCharacter);       
                      break;  
                        }
                        }

通過程式碼我們可以看出,這麼設計非常不利於擴充套件,我們還可以想到使用另一種解決方案是使互動虛擬功能和使用繼承, 但是我們會去實現每種型別的NPC,這種方案也是不可取的。
或者有讀者可以使用更好的策略模式,用C ++編碼的,必須重新編譯程式碼,也是不可取的。
最終的解決方案就想到了Lua的編寫,在使用Lua之前,我們首先要建立一個Character類,如下所示:

class Character 
{
public:   
 Character(const char* name, int hp);    
void say(const char* text);  
  void heal(Character* character);    
      const char* getName() { return name; }    
      int getHealth() { return health; }   
       void setHealth(int hp) { health = hp; }    
           // will be implemented later       
            void interact(Character* character);
            private:  
              const char* name;   
               int health;
               }; 
               Character::Character(const char* name, int hp) 
               {    this->name = name;    health = hp;}
                void Character::say(const char* text) 
                {    std::cout << name << ":" << text << std::endl;} 
                void Character::heal(Character* character) 
                {    character->setHealth(100);}

我們遇到了一個問題, 如果Lua現在沒有關於這種型別,我們如何將Character *作為引數傳遞? 我們如何在Lua中註冊非靜態成員函式並呼叫它們?Lua包裝器可以參考網址:http://lua-users.org/wiki/BindingCodeToLua
決定使用LuaWrapper,它沒有額外的依賴關係而且不需要構建, 只需將一個頭檔案複製到專案中即可開始使用。
使用LuaWrapper,函式的編寫如下所示:

int Character_getName(lua_State* L)
 {   
 	 Character* character = luaW_check<Character>(L, 1);    	lua_pushstring(L, character->getName());  
    	return 1;
    } 
    int Character_getHealth(lua_State* L)
     {  
       Character* character = luaW_check<Character>(L, 1);    	lua_pushnumber(L, character->getHealth());    
       return 1;
       } 
       int Character_setHealth(lua_State* L)
        {   
         Character* character = luaW_check<Character>(L, 1);    	int hp = luaL_checknumber(L, 2);    
        character->setHealth(hp);  
          return 0;
          }

從現在開始,我們將使用checknumber而不是tonumber。 它基本相同,但如果出現問題,它會丟擲錯誤資訊。
LuaWrapper提供了相同的方法,可以用它來獲取C ++物件,還可以建立物件並呼叫它們的方法,如下所示:

player = Character.new(“Hero”, 100)

player:getHealth()

使用luaW_check(L,1)可以獲得玩家物件在C ++中使用它,餘下的程式碼如下所示:

static luaL_Reg Character_table[] = {    { NULL, NULL }}; 
static luaL_Reg Character_metatable[] = {   
 { "getName", Character_getName },  
   { "getHealth", Character_getHealth },    
   { "setHealth", Character_setHealth }, 
      { NULL, NULL }
 };
 static int luaopen_Character(lua_State* L) 
 {   
  luaW_register<Character>(L, "Character", Character_table, Character_metatable, Character_new); 
    return 1;
    }

Character_table用於靜態函式, 我們在Character類中沒有它們,所以這個結構是空的。
Character_metatable用於設定將在Lua中使用的函式名稱。
luaopen_Character註冊一個類, 第一個引數是lua_State *,第二個引數是如何在Lua指令碼中命名我們的類。 其他引數是靜態表,元表和建構函式。
我們的測試指令碼如下所示:

player = Character.new("Hero", 100)
player:setHealth(80)
hp = player:getHealth()
name = player:getName()
print("Character name: "..name..". HP = "..hp)

最後程式碼下載地址如下所示:
連結:https://pan.baidu.com/s/1Rn3WwXYVLA-t79s0MFFarQ
提取碼:mtgr

參考網址:https://eliasdaler.wordpress.com/2013/10/11/lua_cpp_binder/