【问题标题】:Creating a C++ class or "entity" (or something equivalent) from Lua从 Lua 创建一个 C++ 类或“实体”(或等效的东西)
【发布时间】:2015-11-07 06:06:07
【问题描述】:

我在我的 C++ 游戏中使用 Lua 编写脚本。我希望人们能够像在 Garry's Mod 中一样创建自己的“实体”。它的工作方式是创建一个新的 lua 文件,然后给实体一个名称、描述、要继承的基类/超类(例如敌人),然后给它提供 new、update、draw 等方法,你可以像这样使用它任何其他游戏实体。

所以我想要这样的东西,我该怎么做?我目前正在使用 alexames 的 LuaWrapper 将我的 C++ 类注册到 Lua。

我知道这是可能的,否则 Garry's Mod 将无法做到...

例子:

-- my_enemy.lua
ENTITY.Name = "My Entity"
ENTITY.Type = TYPE_ENEMY

function ENTITY:new(x, y)
    -- do stuff
end

function ENTITY:update()
    -- do more stuff
end

function ENTITY:draw()
    -- do even more stuff
end

并通过例如:game.newEntity(my_enemy, 0, 0) in Lua 创建它。

(使用 ENTITY 作为实体而不是 my_enemy 只是在复制 GMod 的做法。)

我并不是想用它们自己的方法来制作独特的实体,我想准确地制作 C++ 类,但基本上是从 Lua 中创建它们。

【问题讨论】:

  • @Youka 不,我不是要向 Lua 展示 C++ 类。我正在尝试创建一个实际的实体,从 Lua 到 C++,而不是相反。
  • 为什么不简单地将实体保存在容器中并在适用时使用 lua_pcall()? (即:在绘制阶段、更新阶段等)。毕竟,lua 中的方法只是常规函数。您可以使用您想要实现的代码示例进行编辑吗?请注意,我不熟悉 LuaWrapper 或 Garry's Mod。

标签: c++ class scripting lua game-engine


【解决方案1】:

免责声明:这是一个很长的答案,但它是一个非常复杂的引擎设计问题,本质上是非常开放的。我试图提供足够的细节来帮助你,假设你对 lua C api 有中等水平的知识。

所以,作为免责声明,那里有很多 lua 包装器的东西,我的偏好是不使用它们中的任何一个,而是直接使用 lua C api 做所有事情,这真的没那么糟糕......在接下来我将描述我将如何做到这一点。在您的情况下,它的某些部分可能会更好地使用您的 lua 包装器以某种方式更好地与您的引擎的其余部分保持一致,但您只需要自己弄清楚。

在我看来,基本上你需要做两件事。一个是 C++ 需要能够表示“lua 实体定义”(“类”),另一个是 C++ 需要能够跟踪这些类的实例,以便它可以调用它们的 draw 和酌情更新方法。

第一部分不太难。我首先要做的是,设置它,以便有一个特殊的表,存储在 lua 注册表中,按名称存储所有不同的 lua 定义的“类”。所以在上面的例子中,当引擎决定它需要加载“my_enemy”类型时,它会

(1) 将一个表压入堆栈(lua_newtable(L))

(2) 在栈上也复制(引用)它(lua_pushvalue(L, -1))

(3) 设置为全局值“ENTITY”(lua_setglobal(L, "ENTITY")) 这会消耗已创建的堆栈副本,但会将原始堆栈保留在堆栈上。

(4) 从注册表中获取用户定义的类表。 (将 lua_gettable 与 LUA_REGISTRYINDEX 一起使用)

(5) 将具有相同字符串值的原始数据也存储为该表的字段。现在全局表和特殊注册表都保存了这个表的副本。

(6) 将用户定义的脚本文件加载为一个chunk(lua_loadstring, lua_loadfile)

(7) 使用 lua_pcall 运行它(如果你想帮助你的用户,并配置一个适当的错误处理函数,比如 debug.backtrace)你将不传递任何参数,它不会返回任何参数,所以堆栈在此之后为空。

(8) 清除全局变量“ENTITY”(通过将 nil 赋值给它)

我并不完全熟悉 Garry 的 mod 是如何工作的,但您还需要为用户提供一种实例化此类的方法。因此,也许您以某种方式为其创建了一个工厂方法供用户使用,或者在全局空间中的某个位置为他们创建另一个实体表副本。

现在,您必须确定的是,当用户实例化一个实体时,该实体对象基本上位于何处?它本质上是一个纯 lua 对象,一个 C++ 只知道的表吗?或者它本质上是一个 C++ 对象,lua 表示为“用户数据”,但实际上它具有 C++ 风格的生命周期。您可以采用任何一种方式,但我假设您采用前者,因为它似乎更适合您发布的代码示例。

在这种情况下,帮助 C++ 跟踪纯 lua 表的标准方法是使用“luaL_Ref”和“luaL_Unref”。这个想法是,除了表示“对用户定义实体实例的引用”的“用户定义的实体类型”表之外,您应该在注册表中有第二个特殊表。基本上,在您提供给用户以实例化他们的实体的工厂方法中,您应该让它调用您编写的 C 函数,这将

(1) 从注册表中获取特殊的“实体实例”表(将其推入堆栈)

(2) 将表的副本压入堆栈,该表将表示我们提供给用户代码的实体实例(这可以在初始化此表的其他代码运行之前或之后进行,没关系)

(3) 调用 luaL_Ref -- 这会将引用存储在实体实例表中的某个特定整数索引处,并返回与该索引对应的 long long。

(4) 在您的 C++ 引擎中,您有一些绘制所有实体的图形循环。你还要加入一个类或结构体,可能称为“lua_userdefined_entity”,它包含这么长的长度,如果你的程序中有多个 lua_State,可能还有一个指向 lua_State * 的指针?这家伙应该有 C++ 方法“draw”、“update”,它们符合你其他 c++ 引擎元素的签名,但要实现这些方法,它会做的是,进入 lua 状态,进入注册表并获取实例表,使用 long long 查找对正确表的引用。然后它将酌情调用“更新”或“绘制”方法。根据您的设计方式,这可能会以几种方式起作用-也许您可以使“实体”表真正成为实例的元表,然后您要做的就是要求lua获取“更新”方法从此表中,将参数压入堆栈并使用 pcall。或者,从 lua 的角度来看,它可能不会“技术上”成为元表,而您只需自己模拟它的那一部分——在这种情况下,您将在结构中存储实体类型的名称,此外到 long long,您必须从注册表中获取两个表才能进行函数调用。 (您可能想要这样做的原因是为了防止用户篡改元表或类似的东西)

(5)在struct“lua_entity_instance”的析构函数中,使其进入lua状态,从注册表中获取instances表,调用luaL_Unref释放对users表的引用。这允许 lua 在对象消失且 C++ 不再需要找到它时释放内存。

如果您不了解 lua 注册表 / luaL_Ref 等,您绝对应该阅读这些内容,它们非常有用,并提供了将所有内容都设为用户数据的替代方法。 IMO 这有时更干净。

如果您决定全部使用 userdata,那么基本上您只需在 C++ 中实现它的所有管道,并向 lua 公开一个瘦接口。

但请注意,如果您决定将其全部作为 userdata,那么很可能您最终仍会将用户定义的函数(如更新和绘制)存储在注册表中,并使用 lua_Ref、Unref 技巧来跟踪这些相反的事情。由于您不能将 lua 函数返回给 C++ 代码。 (我猜你可以存储它的源代码,但是你必须一直重新编译它,它会慢很多,不要那样做。如果用户定义的函数实际上是一个闭包,那也会被破坏,因为当您丢弃该函数并重新编译它时,它将失去对其上值的跟踪。)

【讨论】:

  • 请注意,luaL_Ref一般会返回一个“LUA_INTEGER”,如果你使用典型配置构建lua,它只会很长。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-01-30
  • 2015-06-01
  • 1970-01-01
  • 2016-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多