从 Lua 创建 C++ class 或 "entity" (或等效的东西)
Creating a C++ class or "entity" (or something equivalent) from Lua
我正在使用 Lua 在我的 C++ 游戏中编写脚本。我希望人们能够像 Garry 的 Mod 那样创建自己的 'entities'。它的工作方式是你创建一个新的 lua 文件,然后给实体一个名称、描述、baseclass/superclass 来继承(例如敌人),然后给它一些方法,比如 new、update、draw 等。您可以像使用任何其他游戏实体一样使用它。
所以我想要这样的东西,我该怎么做?我目前正在使用 alexames 的 LuaWrapper 将我的 C++ 类 注册到 Lua.
我知道这是可能的,否则 Garry 的 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 创建它们。
免责声明:这是一个很长的答案,但它是一个非常复杂的引擎设计问题,本质上是非常开放的。我试图提供足够的细节来帮助您,假设您对 lua C api.
有中等水平的知识
因此,作为免责声明,那里有很多 lua 包装器,我的偏好是不使用它们中的任何一个,而是直接使用 lua C api,它真的没有那么糟糕......接下来我将描述我将如何做到这一点。在你的情况下,它的某些部分可能更好地用你的 lua 包装器以某种方式完成,以便与你的引擎的其余部分更加一致,但你只需要自己弄清楚。
据我所知,从根本上说,您需要能够做两件事。一个是 C++ 需要能够表示 "lua entity definition"('class'),另一个是 C++ 需要能够跟踪这些 class 的实例,所以它可以根据需要调用它们的绘制和更新方法。
第一部分并不难。我首先要做的是,设置它以便有一个特殊的 table,存储在 lua 注册表中,它存储所有不同的 lua 定义的 "classes" .所以在上面的例子中,当引擎决定它需要加载"my_enemy"类型时,它会
(1) 将 table 压入堆栈 (lua_newtable(L))
(2) 在堆栈上复制(引用)它 (lua_pushvalue(L, -1))
(3) 设置为全局值"ENTITY" (lua_setglobal(L, "ENTITY"))
这会消耗已创建的堆栈副本,但会将原始堆栈留在堆栈中。
(4) 从注册表中获取用户定义的classes table。 (使用 lua_gettable 和 LUA_REGISTRYINDEX)
(5) 将具有相同字符串值的原始值存储为此 table 的字段。现在全局 table 和特殊注册表 table 都持有此 table.
的副本
(6) 将用户定义的脚本文件加载为一个块 (lua_loadstring, lua_loadfile)
(7) 运行 它使用 lua_pcall (并配置一个适当的错误处理函数,如 debug.backtrace 如果你想帮助你的用户)你将通过它 no参数,它将 return 没有参数,所以堆栈在此之后为空。
(8) 清除全局变量"ENTITY"(通过给它赋值nil)
我不完全了解 Garry 的 mod 是如何工作的,但您还需要为用户提供一种实例化此 class 的方法。因此,也许您以某种方式为用户提供了一个工厂方法,或者为他们在全局 space 的某个地方制作了另一个实体 table 的副本。
现在,您必须决定,当用户实例化实体时,实体对象从根本上存在于何处?它基本上是一个纯粹的 lua 对象,一个 C++ 只知道的 table 对象吗?或者它本质上是一个 C++ 对象,lua 表示为 "userdata",但实际上它具有 C++ 样式的生命周期。您可以采用任何一种方式执行此操作,但我假设您采用前者,因为它似乎更适合您发布的代码示例。
在这种情况下,帮助 C++ 跟踪纯 lua table 的标准方法是使用 "luaL_Ref" 和 "luaL_Unref"。这个想法是,除了代表 "references to user defined entity instances" 的 "user defined entity types" table 之外,您应该在注册表中有第二个特殊的 table。基本上,在您提供给用户以实例化他们的实体的工厂方法中,您应该让它调用您编写的 C 函数,这将
(1) 从注册表中获取特殊的 "entity instances" table(将其压入堆栈)
(2) 将 table 的副本压入堆栈,该副本将代表我们提供给用户代码的实体实例(这可以在初始化此 [ 的其他代码运行之前或之后进行) =118=],没关系)
(3) 调用 luaL_Ref -- 这将引用存储在实体实例 table 中,位于某个特定的整数索引处,并且 returns 到 C 的 long long 对应于该指数。
(4) 在您的 C++ 引擎中,您有一些绘制所有实体的图形循环。您还将加入一个 class 或结构,可能称为 "lua_userdefined_entity" ,其中将包含那个 long long,并且可能还包含一个指向 lua_State * 的指针,如果您有多个lua_State在你的程序中进行?这个家伙应该有 C++ 方法 "draw"、"update",它们符合你的其他 C++ 引擎元素的签名,但是要实现这些方法,它会做的是,转到 lua 状态,转到注册表并获取实例 table,使用那个 long long 查找对正确 table 的引用。然后它将根据需要调用 "update" 或 "draw" 方法。取决于你如何设计它,这可能会以几种方式起作用——也许你会让 "Entity" 的 table 真正成为实例的元 table,然后你要做的是让 lua 从这个 table 中获取 "update" 方法,将参数压入堆栈并使用 pcall。或者,从 lua 的角度来看,它可能不会 "technically" 成为 metatable,您将自己模拟它的那部分——在这种情况下,您会在你的结构中存储实体类型的名称,除了 long long,你必须从注册表中获取两个 tables 才能进行函数调用。 (你可能想这样做的原因是为了防止用户篡改 metatable 或类似的东西)
(5) 在结构"lua_entity_instance"的析构函数中,使其进入lua状态,从注册表中获取实例table,并调用luaL_Unref 发布对用户 table 的引用。这允许 lua 在对象消失并且 C++ 不再需要能够找到它时释放内存。
如果您不了解 lua 注册表/luaL_Ref 等,您绝对应该阅读这些内容,它们非常有用,并且提供了一种替代方法,可以让所有内容都成为用户数据。在我看来,这有时会干净很多。
如果您决定全部作为用户数据来完成,那么基本上您只需在 C++ 中实现它的所有管道,并向 lua 公开一个瘦接口。
但请注意,如果您决定将所有操作都作为用户数据来完成,那么很可能您最终仍会存储用户定义的函数,例如更新和在注册表中绘制,并使用 lua_Ref、Unref跟踪那些东西的技巧。因为你不能对 C++ 代码 return 一个 lua 函数。 (我猜你可以存储它的源代码,但你将不得不一直重新编译它,它会慢很多,不要那样做。如果用户定义的函数实际上是一个闭包,那也会被破坏,因为当您丢弃该函数并重新编译它时,它会丢失它的上值。)
我正在使用 Lua 在我的 C++ 游戏中编写脚本。我希望人们能够像 Garry 的 Mod 那样创建自己的 'entities'。它的工作方式是你创建一个新的 lua 文件,然后给实体一个名称、描述、baseclass/superclass 来继承(例如敌人),然后给它一些方法,比如 new、update、draw 等。您可以像使用任何其他游戏实体一样使用它。
所以我想要这样的东西,我该怎么做?我目前正在使用 alexames 的 LuaWrapper 将我的 C++ 类 注册到 Lua.
我知道这是可能的,否则 Garry 的 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 创建它们。
免责声明:这是一个很长的答案,但它是一个非常复杂的引擎设计问题,本质上是非常开放的。我试图提供足够的细节来帮助您,假设您对 lua C api.
有中等水平的知识因此,作为免责声明,那里有很多 lua 包装器,我的偏好是不使用它们中的任何一个,而是直接使用 lua C api,它真的没有那么糟糕......接下来我将描述我将如何做到这一点。在你的情况下,它的某些部分可能更好地用你的 lua 包装器以某种方式完成,以便与你的引擎的其余部分更加一致,但你只需要自己弄清楚。
据我所知,从根本上说,您需要能够做两件事。一个是 C++ 需要能够表示 "lua entity definition"('class'),另一个是 C++ 需要能够跟踪这些 class 的实例,所以它可以根据需要调用它们的绘制和更新方法。
第一部分并不难。我首先要做的是,设置它以便有一个特殊的 table,存储在 lua 注册表中,它存储所有不同的 lua 定义的 "classes" .所以在上面的例子中,当引擎决定它需要加载"my_enemy"类型时,它会
(1) 将 table 压入堆栈 (lua_newtable(L))
(2) 在堆栈上复制(引用)它 (lua_pushvalue(L, -1))
(3) 设置为全局值"ENTITY" (lua_setglobal(L, "ENTITY")) 这会消耗已创建的堆栈副本,但会将原始堆栈留在堆栈中。
(4) 从注册表中获取用户定义的classes table。 (使用 lua_gettable 和 LUA_REGISTRYINDEX)
(5) 将具有相同字符串值的原始值存储为此 table 的字段。现在全局 table 和特殊注册表 table 都持有此 table.
的副本(6) 将用户定义的脚本文件加载为一个块 (lua_loadstring, lua_loadfile)
(7) 运行 它使用 lua_pcall (并配置一个适当的错误处理函数,如 debug.backtrace 如果你想帮助你的用户)你将通过它 no参数,它将 return 没有参数,所以堆栈在此之后为空。
(8) 清除全局变量"ENTITY"(通过给它赋值nil)
我不完全了解 Garry 的 mod 是如何工作的,但您还需要为用户提供一种实例化此 class 的方法。因此,也许您以某种方式为用户提供了一个工厂方法,或者为他们在全局 space 的某个地方制作了另一个实体 table 的副本。
现在,您必须决定,当用户实例化实体时,实体对象从根本上存在于何处?它基本上是一个纯粹的 lua 对象,一个 C++ 只知道的 table 对象吗?或者它本质上是一个 C++ 对象,lua 表示为 "userdata",但实际上它具有 C++ 样式的生命周期。您可以采用任何一种方式执行此操作,但我假设您采用前者,因为它似乎更适合您发布的代码示例。
在这种情况下,帮助 C++ 跟踪纯 lua table 的标准方法是使用 "luaL_Ref" 和 "luaL_Unref"。这个想法是,除了代表 "references to user defined entity instances" 的 "user defined entity types" table 之外,您应该在注册表中有第二个特殊的 table。基本上,在您提供给用户以实例化他们的实体的工厂方法中,您应该让它调用您编写的 C 函数,这将
(1) 从注册表中获取特殊的 "entity instances" table(将其压入堆栈)
(2) 将 table 的副本压入堆栈,该副本将代表我们提供给用户代码的实体实例(这可以在初始化此 [ 的其他代码运行之前或之后进行) =118=],没关系)
(3) 调用 luaL_Ref -- 这将引用存储在实体实例 table 中,位于某个特定的整数索引处,并且 returns 到 C 的 long long 对应于该指数。
(4) 在您的 C++ 引擎中,您有一些绘制所有实体的图形循环。您还将加入一个 class 或结构,可能称为 "lua_userdefined_entity" ,其中将包含那个 long long,并且可能还包含一个指向 lua_State * 的指针,如果您有多个lua_State在你的程序中进行?这个家伙应该有 C++ 方法 "draw"、"update",它们符合你的其他 C++ 引擎元素的签名,但是要实现这些方法,它会做的是,转到 lua 状态,转到注册表并获取实例 table,使用那个 long long 查找对正确 table 的引用。然后它将根据需要调用 "update" 或 "draw" 方法。取决于你如何设计它,这可能会以几种方式起作用——也许你会让 "Entity" 的 table 真正成为实例的元 table,然后你要做的是让 lua 从这个 table 中获取 "update" 方法,将参数压入堆栈并使用 pcall。或者,从 lua 的角度来看,它可能不会 "technically" 成为 metatable,您将自己模拟它的那部分——在这种情况下,您会在你的结构中存储实体类型的名称,除了 long long,你必须从注册表中获取两个 tables 才能进行函数调用。 (你可能想这样做的原因是为了防止用户篡改 metatable 或类似的东西)
(5) 在结构"lua_entity_instance"的析构函数中,使其进入lua状态,从注册表中获取实例table,并调用luaL_Unref 发布对用户 table 的引用。这允许 lua 在对象消失并且 C++ 不再需要能够找到它时释放内存。
如果您不了解 lua 注册表/luaL_Ref 等,您绝对应该阅读这些内容,它们非常有用,并且提供了一种替代方法,可以让所有内容都成为用户数据。在我看来,这有时会干净很多。
如果您决定全部作为用户数据来完成,那么基本上您只需在 C++ 中实现它的所有管道,并向 lua 公开一个瘦接口。
但请注意,如果您决定将所有操作都作为用户数据来完成,那么很可能您最终仍会存储用户定义的函数,例如更新和在注册表中绘制,并使用 lua_Ref、Unref跟踪那些东西的技巧。因为你不能对 C++ 代码 return 一个 lua 函数。 (我猜你可以存储它的源代码,但你将不得不一直重新编译它,它会慢很多,不要那样做。如果用户定义的函数实际上是一个闭包,那也会被破坏,因为当您丢弃该函数并重新编译它时,它会丢失它的上值。)