在每次游戏循环迭代时调用 main.lua 脚本——这是好的还是坏的设计?它如何影响性能(相对)?
从.维护游戏状态。C++ 主机程序或b。来自 Lua 脚本或c。从两者同步它们?
(关于该主题的上一个问题:Lua 和 C++:职责分离)
(我投票给每个答案。最好的答案将被接受。)
在每次游戏循环迭代时调用 main.lua 脚本——这是好的还是坏的设计?它如何影响性能(相对)?
从.维护游戏状态。C++ 主机程序或b。来自 Lua 脚本或c。从两者同步它们?
(关于该主题的上一个问题:Lua 和 C++:职责分离)
(我投票给每个答案。最好的答案将被接受。)
我对 lua 的基本规则是——或者游戏中的任何脚本语言——
基本上,任何以 >33-100Hz(取决于帧速率)调用的代码都是 C++ 我尝试调用 <10Hz 的脚本引擎。
基于任何一种实际指标?并不真地。但它确实在设计中划出了一条分界线,清楚地描绘了 c++ 和 lua 任务 - 如果没有预先划分,每帧 lua 任务将增长,直到它们每帧处理都陷入困境 - 然后就没有明确的指导方针来修剪什么。
lua 最好的一点是它有一个轻量级的虚拟机,在块被预编译后在虚拟机中运行它们实际上是相当快的,但仍然没有 C++ 代码那么快,而且我不认为每次都调用 lua渲染帧将是一个好主意。
我会将游戏状态放在 C++ 中,并在 lua 中添加可以访问和修改状态的函数。基于事件的方法几乎更好,其中事件注册应该在 lua 中完成(最好只在游戏开始或特定游戏事件,但每分钟不超过几次),但实际事件应该由C++ 代码。用户输入也是事件,它们通常不会在每一帧都发生(也许 MouseMove 除外,但因此应该谨慎使用)。您在 lua 中处理用户输入事件的方式(是否处理所有事情(例如按下哪个键等),或者是否有例如键盘上每个键的单独事件(在极端情况下)取决于您的游戏重新尝试制作(基于回合的游戏可能只有一个事件处理程序用于所有事件,RTS 应该有更多事件,并且应该小心处理FPS(主要是因为每帧都会移动鼠标)。通常,您拥有的事件类型越多,您在 lua 中编写的代码就越少(这将提高性能),但是如果您需要处理的“真实事件”实际上是由更独立的“编程级别”触发的,则变得越困难events”(这实际上可能会降低性能,因为 lua 代码需要更复杂)。
或者,如果性能真的很重要,您实际上可以通过向其添加新的操作码来改进 lua VM(我见过一些公司这样做,但主要是为了使编译的 lua 块的反编译更加困难),这实际上不是很难做的事。如果你有一些 lua 代码需要做很多次的事情(比如事件注册、事件运行或更改游戏状态),你可能希望在 lua VM 中实现它们,而不是多个getglobal
和setglobal
他们只需要一个或两个操作码(例如,您可以使用 0-255 和 0-65535 参数创建一个 SETSTATE 操作码,其中第一个参数描述要修改的状态,第二个参数描述状态的新值。当然,这只适用于最多 255 个事件,最多 2^16 个值,但在某些情况下可能就足够了。而且这只需要一个操作码的事实意味着代码运行得更快)。如果您打算隐藏您的 lua 代码,这也会使反编译更加困难(尽管对于了解 lua 内部工作的人来说并不多)。每帧运行几个操作码(大约 30-40 个顶部)不会对您的性能造成那么严重的影响。但是 lua VM 中的 30-40 个操作码不会
我不喜欢 C++。但我确实喜欢游戏。
我的方法可能有点不典型:我在 Lua 中尽我所能,在 C++ 中只做绝对最低限度的工作。游戏循环、实体等都在 Lua 中完成。我什至在 Lua 中完成了 QuadTree 实现。C++ 处理图形和文件系统的东西,以及与外部库的接口。
这不是基于机器的决策,而是基于程序员的决策;我在 Lua 中输出代码的速度比在 C++ 中快得多。所以我把我的程序员周期花在新功能上,而不是节省计算机周期。我的目标机器(过去 3 年的任何笔记本电脑)都能够非常轻松地处理这么多的 Lua。
Lua 的占用空间小得惊人(如果您不知道,请查看luaJIT )。
这就是说,如果我找到了瓶颈(我还没有),我会分析游戏以找到慢速部分,然后我会将那部分翻译成 C++ ......只有当我找不到使用 Lua 解决它。
我第一次在我一直在开发的游戏中使用 Lua。我的应用程序的 C++ 端实际上保存了指向每个游戏状态实例的指针。有些游戏状态是用 C++ 实现的,有些是用 Lua 实现的(例如“游戏”状态)。
更新和主应用程序循环存在于 C++ 方面。我已经公开了允许 Lua VM 在运行时向应用程序添加新游戏状态的函数。
即使在资源有限的硬件上运行(带有集成视频的 Atom 处理器),我还没有遇到任何缓慢的问题。每一帧都会调用 Lua 函数。我的应用程序中最昂贵(就时间而言)的操作是渲染。
在 Lua 中完全创建新状态的能力是我在项目中做出的最佳决定之一,因为它允许我自由地添加游戏的各个部分而无需重新编译整个游戏。
编辑:我正在使用Luabind,我读过它的性能通常比其他绑定框架慢,当然还有 Lua C API。
恕我直言 Lua 脚本是针对特定行为的,如果您每秒调用 Lua 脚本 60 次,肯定会损害性能。
Lua 脚本通常用于将行为和特定事件与游戏引擎逻辑(GUI、项目、对话框、游戏引擎事件等)分开。例如,Lua 的一个很好的用法是在触发爆炸(粒子 FX)时,如果游戏角色走到某个地方,在引擎中硬编码该事件的输出将是一个非常丑陋的选择。不过,让引擎触发正确的脚本将是一个更好的选择,将特定行为与引擎分离。
我建议,尝试将您的游戏状态保持在一个部分中,而不是提高在两个地方(Lua 和引擎)保持状态同步的复杂程度,向其中添加线程,您最终会遇到非常丑陋的混乱. 把事情简单化。(在我的设计中,我主要将游戏状态保留在 C++ 中)
祝你的游戏好运!
您可能不想在每一帧迭代时都执行整个 Lua 脚本,因为任何足够复杂的游戏都会有多个游戏对象,它们有自己的行为。换句话说,除非你有多个小脚本来处理大型游戏行为的特定部分,否则 Lua 的优势就会丧失。您可以使用 lua_call 函数在脚本中调用任何适当的 lua 例程,而不仅仅是整个文件。
这里没有理想的答案,但是你的绝大多数游戏状态传统上存储在游戏引擎中(即 C++)。您向 Lua 透露的信息刚好足以让 Lua 执行您分配给 Lua 的决策。
您需要考虑哪种语言适合哪种行为。Lua 对于高级控制和决策很有用,而 C++ 对于面向性能的代码很有用。Lua 对于您需要调整而无需重新编译的游戏部分特别有用。例如,所有魔法常量和变量都可以进入 Lua。不要试图在 Lua 不属于它的地方硬塞,即图形或音频渲染。
关于1的性能:如果main.lua
没有变化,用lua_loadfile
or加载一次loadfile
,保存对返回函数的引用,然后在需要的时候调用。
我想投入两分钱,因为我坚信这里给出了一些不正确的建议。就上下文而言,我在一个涉及密集 3D 渲染和密集游戏逻辑模拟的大型游戏中使用 Lua。我对 Lua 和性能的熟悉程度超出了我的预期……
请注意,我将专门讨论 LuaJIT,因为您将要使用 LuaJIT。它是即插即用的,真的,所以如果你可以嵌入 Lua,你就可以嵌入 LuaJIT。如果不是为了额外的速度,你会想要它,然后是自动外部函数接口模块(需要'ffi'),它允许你直接从 Lua 调用你的本机代码,而无需接触 Lua C API (95 %+ 的案例)。
以 60hz 调用 Lua完全没问题(我在 VR 中以 90hz 调用它......)。问题是你必须小心正确地做到这一点。正如其他人提到的,仅加载一次脚本至关重要。然后,您可以使用 C API 访问您在该脚本中定义的函数,或将脚本本身作为函数运行。我推荐前者:对于一个相对简单的游戏,你可以通过定义onUpdate(dt)、onRender()、onKeyPressed(key)、onMouseMoved(dx,dy)等函数来解决。你可以在适当的时候调用它们从 C++ 中的主循环。或者,您实际上可以将整个主循环放在 Lua 中,而是调用 C++ 代码来执行性能关键的例程(我这样做)。使用 LuaJIT FFI 尤其容易做到这一点。
这是一个非常难的问题。这将取决于您的需求。你能很容易地敲定游戏状态吗?太好了,把它放在 C++ 端并从 LuaJIT FFI 访问。不确定游戏状态中的所有内容/希望能够快速制作原型?将它保存在 Lua 中没有错。也就是说,直到你开始谈论一个包含 1000 个对象的复杂游戏,每个对象都包含非平凡的状态。在这种情况下,混合是要走的路,但是准确地弄清楚如何在 C++ 和 Lua 之间分割状态,以及如何在两者之间进行混合状态(尤其是在性能关键的例程中)是一门艺术。如果您想出一种防弹技术,请告诉我 :) 与其他一切一样,一般的经验法则是:通过性能关键路径的数据需要在本机端。例如,如果您的实体具有更新每个帧的位置和速度,并且您有数千个实体,则需要在 C++ 中执行此操作。但是,您可以使用 Lua 在这些实体之上分层“清单”(清单不需要每帧更新)。
现在,我想作为一般 FYI 和对其他一些答案的回应提出更多的注意事项。
一般来说,基于事件的方法对任何游戏的性能都至关重要,但对于用 Lua 编写的系统来说,这会加倍。我说调用 Lua @ 60hz 完全没问题。但这不是在 Lua 中的每一帧中,在大量游戏对象上运行紧密循环非常好。您可能会在 C++ 中对宇宙中的所有内容进行浪费性地调用 update() (尽管您不应该这样做),但在 Lua 中这样做会开始过快地消耗这些宝贵的毫秒。相反,正如其他人所提到的,您需要将 Lua 逻辑视为“反应式”——通常,这意味着处理事件。例如,不要在 Lua 中每帧检查一个实体是否在另一个实体的范围内(我的意思是,这对一个实体来说很好,但一般来说,当你扩大游戏规模时,你不需要这样想)。相反,请告诉您的 C++ 引擎在两个实体彼此相距一定距离时通知您。这样,Lua 就成为了游戏逻辑的高级控制器,
警惕“混合代码”很慢的建议。Lua C API 轻量且快速。与 Lua 一起使用的包装函数在最坏的情况下非常容易(如果您花一些时间了解 Lua 如何与 C 与虚拟堆栈等接口,您会注意到它是专门为最小化调用开销而设计的),并且充其量,微不足道(不需要包装)和 100% 的性能与原生调用一样(感谢 LuaJIT FFI!)在大多数情况下,我发现 Lua 和 C 的仔细混合(通过 LJ FFI)优于纯 Lua。即使是向量操作,我相信 LuaJIT 手册中提到的某处作为代码应该保留到 Lua 的示例,我发现当我通过 Lua->C 调用执行时会更快。最终,不要
大多数小型游戏在游戏状态、核心循环、输入处理等方面完全可以在 Lua 中完成并在 LuaJIT 下运行。如果您正在构建一个小型游戏,请考虑“根据需要迁移到 C++”方法(延迟编译!)用 Lua 编写,当您确定一个明显的瓶颈时(并对其进行测量以确保它是罪魁祸首),移动那一点到 C++ 并在你的路上。如果您正在编写一个包含 1000 多个具有复杂逻辑、高级渲染等的复杂对象的大型游戏,那么您将受益于更多的前期设计。
祝你好运:)
Lua 和 C++ 之间的绑定会损失大部分性能。函数调用实际上需要被包装和重新包装,并且通常会这样几次。纯 Lua 或纯 C++ 代码通常比混合代码更快(对于小型操作)。
话虽如此,我个人并没有看到每帧运行 Lua 脚本有什么强大的性能。
通常脚本在高层次上是好的。Lua 已被用于机器人(雷神之锤 3)和用户界面(魔兽世界)的著名游戏中。在高层使用 Lua 微线程很方便:协程可以节省很多(与真正的线程相比)。例如偶尔运行一些 Lua 代码。