手把手教你架构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/

发布了327 篇原创文章 · 获赞 588 · 访问量 90万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览