从Lua创建一个C++类或"entity"(或等效的东西)

Creating a C++ class or "entity" (or something equivalent) from Lua

本文关键字:entity C++ 创建 Lua 一个 类或      更新时间:2023-10-16

我在C++游戏中使用Lua编写脚本。我希望人们能够像加里的国防部一样创建自己的"实体"。它的工作方式是创建一个新的lua文件,给实体一个名称、描述、基类/超类来继承(例如敌人),给它一些方法,比如new、update、draw等,你可以像使用任何其他游戏实体一样使用它。

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

我知道这是可能的,否则加里的国防部将无法做到…

示例:

-- 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

并通过创建,例如:Lua中的game.newEntity(my_enemy, 0, 0)

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

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

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

所以,作为免责声明,有很多lua包装器的东西,我不喜欢使用它们中的任何一个,而是直接使用lua C api做所有事情,这真的没有那么糟糕。。。在接下来的内容中,我将描述我将如何做到这一点。在您的情况下,它的某些部分可能会更好地使用lua包装器,以某种方式与引擎的其他部分保持更高的一致性,但您必须自己解决这个问题。

在我看来,从根本上讲,有两件事你需要能够做到。一件是C++需要能够表示"lua实体定义"("类"),另一件是,C++需要能够跟踪这些类的实例,以便可以适当地调用它们的draw和update方法。

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

(1) 将表格推到堆栈上(lua_newtable(L))

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

(3) 将其设置为全局值"ENTITY"(lua_stglobal(L,"ENTITY"))这会消耗已制作的堆栈副本,但会将原始副本保留在堆栈上。

(4) 从注册表中获取用户定义的类表。(使用lua_REGISTRYINDEX的lua_gettible)

(5) 将具有相同字符串值的原始值也存储为此表的字段。现在,全局表和特殊注册表表都包含该表的副本。

(6) 将用户定义的脚本文件加载为块(lua_laadstring,lua_laadfile)

(7) 使用lua_call运行它(如果你想帮助你的用户,可以配置一个合适的错误处理程序函数,比如debug.backtrace)。你不会给它传递任何参数,也不会返回任何参数,所以在这之后堆栈是空的。

(8) 清除全局变量"ENTITY"(通过为其分配零)

我不太熟悉Garry的mod是如何工作的,但你也需要为用户提供一种实例化这个类的方法。因此,也许你可以以某种方式为用户提供一个工厂方法,或者在全局空间的某个地方为他们制作另一个Entity表的副本。

现在,你必须决定,当用户实例化一个实体时,实体对象从根本上生活在哪里?从根本上说,它是一个纯粹的lua对象,一个C++只知道的表吗?或者,它从根本上是一个C++对象,lua将其表示为"用户数据",但实际上它有C++风格的生存期。无论哪种方式,你都可以这样做,但我假设你做的是前者,因为它似乎更适合你发布的代码示例。

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

(1) 从注册表中获取特殊的"实体实例"表(将其推送到堆栈上)

(2) 将表示我们提供给用户代码的实体实例的表的副本推到堆栈上(这可以在初始化该表的其他代码运行之前或之后进行,这无关紧要)

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

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

(5) 在结构体"lua\entity_instance"的析构函数中,使其进入lua State,从注册表中获取实例表,并调用luaL_Unrf以释放对users表的引用。这允许lua在对象消失并且C++不再需要找到它时释放内存。

如果你不知道lua registry/luaL_Ref等,你肯定应该读一下这些东西,它们非常有用,并且提供了一种替代方法,可以让一切都成为用户数据。IMO这有时要干净得多。

如果您决定将其全部作为用户数据来完成,那么基本上您只需在C++中实现它的所有管道,并向lua公开一个瘦接口。

但请注意,如果您决定将所有操作都作为用户数据进行,那么很可能您最终仍会将用户定义的函数(如update和draw)存储在注册表中,并使用lua_Ref、Unref技巧来跟踪这些事情。因为您不能将lua函数返回到C++代码中。(我想你可以存储它的源代码,但之后你必须一直重新编译它,它会慢得多,不要这样做。如果用户定义的函数实际上是一个闭包,这也会被破坏,因为当你放弃函数并重新编译它时,它会失去对其upvalue的跟踪。)