Lua、游戏状态和游戏循环
在每次游戏循环迭代时调用 main.lua 脚本 - 这是好还是坏的设计?它对性能有何影响(相对)?
从a维持游戏状态。 C++ 主机程序或b。来自 Lua 脚本或 c。来自两者并同步它们?
(有关该主题的上一个问题:Lua 和 C++:职责分离)
Call main.lua script at each game loop iteration - is it good or bad design? How does it affect on the performance (relatively)?
Maintain game state from a. C++ host-program or b. from Lua scripts or c. from both and synchronise them?
(Previous question on the topic: Lua and C++: separation of duties)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
我对 lua 的基本规则是 - 或游戏中的任何脚本语言 -
基本上,任何在 >33-100Hz 调用的代码(取决于帧速率)是 C++
我尝试调用脚本引擎<10Hz。
基于任何类型的实际指标?并不真地。但它确实在设计中划出了一条分界线,明确划分了 c++ 和 lua 任务 - 如果没有预先划分,每帧 lua 任务将会增长,直到它们阻碍每帧的处理 - 然后就没有明确的指导方针来修剪什么。
My basic rule for lua is - or any script language in a game -
Basically, any code thats called at >33-100Hz (depending on frame rate) is C++
I try to invoke the script engine <10Hz.
Based on any kind of actual metric? not really. but it does put a dividing line in the design, with c++ and lua tasks clearly delineated - without the up front delineation the per frame lua tasks will grow until they are bogging processing per frame - and then theres no clear guideline on what to prune.
lua 最好的一点是它有一个轻量级的虚拟机,在预编译块之后在虚拟机中运行它们实际上相当快,但仍然不如 C++ 代码快,而且我不认为每次都调用 lua渲染帧将是一个好主意。
我将游戏状态放在C++中,并在lua中添加可以到达的函数,并修改状态。基于事件的方法几乎更好,其中事件注册应该在 lua 中完成(最好只在游戏开始或特定游戏事件时,但每分钟不超过几次),但实际事件应该由C++ 代码。用户输入也是事件,它们通常不会在每一帧发生(也许 MouseMove 除外,但因此应该谨慎使用)。你处理用户输入事件的方式(是否在lua中处理所有事情(比如按下哪个键等),或者键盘上的每个键是否有单独的事件(在极端情况下)取决于你的游戏)正在尝试制作(回合制游戏可能只有一个事件处理程序来处理所有事件,RTS 应该有更多事件,而 FPS 应该小心处理(主要是因为移动鼠标每一帧都会发生))。你拥有的不同类型的事件,你在 lua 中编写的代码就越少(这将提高性能),但是如果你需要处理的“真实事件”实际上是由更多单独的“编程级别事件”触发的,那么它就会变得更加困难(这实际上可能会降低性能,因为 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 个操作码不会让您走得太远(一个简单的 if-then-else 可能需要多达 10-20 个或更多操作码,具体取决于表达式)。The best thing about lua is that it has a lightweight VM, and after the chunks get precompiled running them in the VM is actually quite fast, but still not as fast as a C++ code would be, and I don't think calling lua every rendered frame would be a good idea.
I'd put the game state in C++, and add functions in lua that can reach, and modify the state. An event based approach is almost better, where event registering should be done in lua (preferably only at the start of the game or at specific game events, but no more than a few times per minute), but the actual events should be fired by C++ code. User inputs are events too, and they don't usually happen every frame (except for maybe MouseMove but which should be used carefully because of this). The way you handle user input events (whether you handle everything (like which key was pressed, etc) in lua, or whether there are for example separate events for each keys on the keyboard (in an extreme case) depends on the game you're trying to make (a turn based game might have only one event handler for all events, an RTS should have more events, and an FPS should be dealt with care (mainly because moving the mouse will happen every frame)). Generally the more separate kinds of events you have, the less you have to code in lua (which will increase performance), but the more difficult it gets if a "real event" you need to handle is actually triggered by more separate "programming level events" (which might actually decrease performance, because the lua code needs to be more complex).
Alternatively if performance is really important you can actually improve the lua VM by adding new opcodes to it (I've seen some of the companies to do this, but mainly to make decompilation of the compiled lua chunks more harder), which is actually not a hard thing to do. If you have something that the lua code needs to do a lot of times (like event registering, event running, or changing the state of the game) you might want to implement them in the lua VM, so instead of multiple
getglobal
andsetglobal
opcodes they would only take one or two (for example you could make a SETSTATE opcode with a 0-255 and a 0-65535 parameter, where the first parameter descibes which state to modify, and the second desribes the new value of the state. Of course this only works if you have a maximum of 255 events, with a maximum of 2^16 values, but it might be enough in some cases. And the fact that this only takes one opcode means that the code will run faster). This would also make decompilation more harder if you intend to obscure your lua code (although not much to someone who knows the inner workings of lua). Running a few opcodes per frame (around 30-40 tops) won't hit your performance that badly. But 30-40 opcodes in the lua VM won't get you far if you need to do really complex things (a simple if-then-else can take up to 10-20 or more opcodes depending on the expression).我不喜欢C++。但我确实喜欢游戏。
我的方法可能有点不典型:我在 Lua 中尽我所能,而在 C++ 中只做了绝对最少的事情。游戏循环、实体等都是用Lua完成的。我什至用 Lua 完成了 QuadTree 实现。 C++ 处理图形和文件系统内容,以及与外部库的接口。
这不是基于机器的决定,而是基于程序员的决定;我在 Lua 中输出代码比在 C++ 中快得多。因此,我将程序员的周期花在新功能上,而不是节省计算机周期上。我的目标机器(过去 3 年的任何笔记本电脑)都能够非常轻松地处理这么多的 Lua。
Lua 的占用空间非常小(如果您不了解,请查看 luaJIT)。
这就是说,如果我发现瓶颈(我还没有),我会分析游戏以找到缓慢的部分,并且我会将该部分翻译为 C++...只有当我找不到瓶颈时使用 Lua 解决这个问题。
I don't like C++. But I do like games.
My approach might be a bit atypical: I do everything I can in Lua, and only the absolute minimum in C++. The game loop, the entities, etc are all done in Lua. I even have a QuadTree implementation done in Lua. C++ handles graphical and filesystem stuff, as well as interfacing with external libraries.
This is not a machine-based decision, but a programmer-based one; I output code much faster in Lua than In C++. So I spend my programmer cycles on new features rather than on saving computer cycles. My target machines (any laptop from the last 3 years) are able to cope with this amount of Lua very easily.
Lua is surprisingly low-footprint (take a look to luaJIT if you don't know it).
This said, if I ever find a bottleneck (I haven't yet) I'll profile the game in order to find the slow part, and I'll translate that part to C++ ... only if I can't find a way around it using Lua.
我第一次在我正在开发的游戏中使用 Lua。我的应用程序的 C++ 端实际上保存了指向每个游戏状态实例的指针。有些游戏状态是用C++实现的,有些是用Lua实现的(例如“游戏”状态)。
更新和主应用程序循环位于 C++ 方面。我公开了允许 Lua VM 在运行时向应用程序添加新游戏状态的函数。
即使在资源有限的硬件(带有集成视频的 Atom 处理器)上运行,我也没有遇到任何缓慢的问题。 每帧都会调用Lua函数。我的应用程序中最昂贵(就时间而言)的操作是渲染。
完全在 Lua 中创建新状态的能力是我在该项目中做出的最佳决定之一,因为它允许我自由添加游戏的某些部分,而无需重新编译整个游戏。
编辑:我正在使用 Luabind,我读过它的执行速度通常比其他绑定框架,当然还有 Lua C API。
I am using Lua for the first time in a game I've been working on. The C++ side of my application actually holds pointers to instances of each game state. Some of the game states are implemented in C++ and some are implemented in Lua (such as the "game play" state).
The update and main application loop live on the C++ side of things. I have exposed functions that allow the Lua VM to add new game states to the application at runtime.
I have not yet had any problems with slowness, even running on hardware with limited resources (Atom processor with integrated video). Lua functions are called every frame. The most expensive (in terms of time) operation in my application is rendering.
The ability to create new states completely in Lua was one of the best decisions I made on the project, since it allows me to freely add portions of the game without recompiling the whole thing.
Edit: I'm using Luabind, which I have read performs slower in general than other binding frameworks and of course the Lua C API.
您可能不想在每个帧迭代中执行整个 Lua 脚本,因为任何足够复杂的游戏都会有多个具有自己行为的游戏对象。换句话说,除非您有多个小脚本来处理大型游戏行为的特定部分,否则 Lua 的优势就会消失。您可以使用 lua_call 函数来调用脚本中任何适当的 lua 例程,而不仅仅是整个文件。
这里没有理想的答案,但绝大多数游戏状态传统上存储在游戏引擎(即 C++)中。你向 Lua 透露的信息足以让 Lua 做出你分配给 Lua 的决策。
您需要考虑哪种语言适合哪种行为。 Lua 对于高级控制和决策很有用,而 C++ 对于面向性能的代码很有用。 Lua 对于您需要调整而无需重新编译的游戏部分特别有用。例如,所有魔法常量和变量都可以进入 Lua。不要试图把 Lua 强行塞进不属于它的地方,即图形或音频渲染。
You probably don't want to execute the entire Lua script on every frame iteration, because any sufficiently complex game will have multiple game objects with their own behaviors. In other words, the advantages of Lua are lost unless you have multiple tiny scripts that handle a specific part of the behavior of a larger game. You can use the lua_call function to call any appropriate lua routine in your script, not just the entire file.
There's no ideal answer here, but the vast majority of your game state is traditionally stored in the game engine (i.e. C++). You reveal to Lua just enough for Lua to do the decision making that you've assigned to Lua.
You need to consider which language is appropriate for which behaviors. Lua is useful for high level controls and decisions, and C++ is useful for performance oriented code. Lua is particularly useful for the parts of your game that you need to tweak without recompiling. All magic constants and variables could go into Lua, for example. Don't try to shoehorn Lua where it does not belong, i.e. graphics or audio rendering.
恕我直言,Lua 脚本适用于特定行为,如果每秒调用 Lua 脚本 60 次,肯定会损害性能。
Lua 脚本通常用于将行为和特定事件与游戏引擎逻辑(GUI、项目、对话框、游戏引擎事件等)分开。例如,Lua 的一个很好的用法是当触发爆炸(粒子 FX)时,如果游戏角色走到某个地方,在引擎中硬编码该事件的输出将是一个非常丑陋的选择。不过,让引擎触发正确的脚本将是更好的选择,将特定行为与引擎分离。
我建议,尝试将你的游戏状态保持在一个部分,而不是提高在两个地方(Lua 和 Engine)保持状态同步的复杂程度,而是添加线程,你最终会遇到一团非常难看的混乱。保持简单。 (在我的设计中,我主要将游戏状态保留在 C++ 中)
祝您的游戏好运!
IMHO Lua scripts are for specific behaviours, it's definitely going to hurt performance if you are calling a Lua script 60 times per second.
Lua scripts are often to separate stuff like Behaviour, and specific events from your Game Engine logic (GUI, Items, Dialogs, game engine events, etc...). A good usage of Lua for example would be when triggering an explosion (particle FX), if the Game Character walks somewhere, hard-coding the output of that event in your engine would be a very ugly choice. Though, making the engine trigger the correct script would be a better choice, decoupling that specific behavior off your engine.
I would recommend, to try to keep your Game State in one part, instead of upscaling the level of complexity of keeping states synchronized in two places (Lua and Engine), add threading to that, and you will end up having a very ugly mess. Keep it simple. (In my Designs I mostly keep Game State in C++)
Good luck with your Game!
我想投入我的两分钱,因为我坚信这里给出了一些不正确的建议。就上下文而言,我在一个大型游戏中使用 Lua,该游戏涉及密集的 3D 渲染以及密集的游戏逻辑模拟。我对 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 中并没有什么问题。也就是说,直到您开始谈论一个包含数千个对象的复杂游戏,每个对象都包含重要的状态。在这种情况下,混合是可行的方法,但准确地弄清楚如何在 C++ 和 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++ 即可开始使用。如果您正在编写一个包含数千个复杂对象、复杂逻辑、高级渲染等的大型游戏,那么您将受益于更多的前期设计。
祝你好运:)
I'd like to throw in my two cents since I strongly believe that there's some incorrect advice being given here. For context, I am using Lua in a large game that involves both intensive 3D rendering as well as intensive game logic simulation. I've become more familiar than I'd have liked to with Lua and performance...
Note that I'm going to talk specifically about LuaJIT, because you're going to want to use LuaJIT. It's plug-and-play, really, so if you can embed Lua you can embed LuaJIT. You'll want it, if not for the extra speed, then for the automagic foreign function interface module (require 'ffi') that will allow you to call your native code directly from Lua without ever having to touch the Lua C API (95%+ of cases).
It's perfectly fine to call Lua at 60hz (I call it at 90hz in VR..). The catch is that you are going to have to be careful to do it correctly. As someone else mentioned, it's critical that you load the script only once. You can then use the C API to get access to functions you defined in that script, or to run the script itself as a function. I recommend the former: for a relatively simple game, you can get by with defining functions like onUpdate (dt), onRender (), onKeyPressed (key), onMouseMoved (dx, dy), etc. You can call these at the appropriate time from your main loop in C++. Alternatively, you can actually have your entire main loop be in Lua, and instead invoke your C++ code for performance-critical routines (I do that). This is especially easy to do with the LuaJIT FFI.
This is the really hard question. It will depend on your needs. Can you quite easily hammer down the game state? Great, put it C++-side and access from LuaJIT FFI. Not sure what all will be in the game state / like to be able to prototype quickly? Nothing wrong with keeping it in Lua. That is, until you start talking about a complex game with 1000s of objects, each containing non-trivial state. In this case, hybrid is the way to go, but figuring out exactly how to split state between C++ and Lua, and how to martial said state between the two (especially in perf-critical routines) is something of an art. Let me know if you come up with a bulletproof technique :) As with everything else, the general rule of thumb is: data that goes through performance-critical pathways needs to be on the native side. For example, if your entities have positions and velocities that you update each frame, and you have thousands of said entities, you need to do this in C++. However, you can get away with layering an 'inventory' on top of these entities using Lua (an inventory doesn't need a per-frame update).
Now, a couple more notes I'd like to throw out both as general FYIs and in response to some of the other answers.
Event-based approaches are, in general, critical to the performance of any game, but that goes doubly for systems written in Lua. I said it's perfectly fine to call Lua @ 60hz. But it's not perfectly fine to be running tight loops over lots of game objects each frame in said Lua. You might get away with wastefully calling update() on everything in the universe in C++ (though you shouldn't), but doing so in Lua will start eating up those precious milliseconds far too quickly. Instead, as others have mentioned, you need to be thinking of Lua logic as 'reactive' -- usually, this means handling an event. For example, don't check that one entity is in range of another each frame in Lua (I mean, this is fine for one entity, but in general when you're scaling up your game you need to not think like this). Instead, tell your C++ engine to notify you when the two entities get within a certain distance of one another. In this way, Lua becomes the high-level controller of game logic, dispatching high-level commands and responding to high-level events, not carrying out the low-level math grind of trivial game logic.
Be wary of the advice that 'mixed code' is slow. The Lua C API is lightweight and fast. Wrapping functions for use with Lua is, at worst, quite easy (and if you take a while to understand how Lua interfaces with C w.r.t. the virtual stack, etc, you will notice that it has been designed specifically to minimize call overhead), and at best, trivial (no wrapping required) and 100% as performant as native calls (thanks, LuaJIT FFI!) In most cases I find that a careful mixture of Lua and C (via the LJ FFI) is superior to pure Lua. Even vector operations, which I believe the LuaJIT manuals mention somewhere as an example where code should be kept to Lua, I have found to be faster when I execute via Lua->C calls. Ultimately, don't trust anyone or anything but your own (careful) performance measurements when it comes to pieces of code that are performance-sensitive.
Most small-ish games could get away just fine with game state, the core loop, input handling, etc. done entirely in Lua and running under LuaJIT. If you're building a small-ish game, consider the "move to C++ as needed" approach (lazy compilation!) Write in Lua, when you identify a clear bottleneck (and have measured it to make sure it's the culprit), move that bit to C++ and be on your way. If you're writing a big game with 1000s of complex objects with complex logic, advanced rendering, etc. then you'll benefit from more up-front designing.
Best of luck :)
关于1的性能:如果
main.lua
没有改变,用lua_loadfile
或loadfile
加载一次,保存对返回函数的引用,然后在需要时调用它。About the performance of 1: if
main.lua
does not change, load it once withlua_loadfile
orloadfile
, save a reference to the returned function, and then call it when needed.Lua 和 C++ 的绑定会损失大部分性能。函数调用实际上需要被包装、重新包装,通常这样几次。纯 Lua 或纯 C++ 代码通常比混合代码更快(对于小型操作)。
话虽如此,我个人并没有看到每帧运行 Lua 脚本对性能有任何强烈影响。
通常脚本编写在高级方面表现良好。 Lua 已被用于著名的机器人游戏(雷神之锤 3)和用户界面(魔兽世界) >)。在高层使用 Lua 微线程很方便:协程可以节省很多(与真实线程相比)。例如,偶尔运行一些 Lua 代码。
Most of the performance will be lost through the binding between Lua and C++. A function call will actually need to be wrapped, and re-wrapped, and like that a couple of time usually. Pure Lua or pure C++ code is usually faster than mixed code (for small operations).
Having said that, I personally didn't see any strong performance hit running a Lua script every frame.
Usually scripting is good at high level. Lua has been used in famous games for the Bots (Quake 3) and for the User Interface (World of Warcraft). Used at high level Lua micro-threads come handy: The coroutines can save a lot (compared to real threads). For example to run some Lua code only once in a while.