BYTEPATH #1 - Game Loop
Start
To start off you need to install LÖVE on your system and then figure out how to run LÖVE projects. The LÖVE version we'll be using is 0.10.2 and it can be downloaded here. If you're in the future and a new version of LÖVE has been released you can get 0.10.2 here. You can follow the steps from this page for further details. Once that's done you should create a main.lua file in your project folder with the following contents:
首先,你需要在系统里安装 LÖVE,并弄清楚 LÖVE 项目该怎么运行。本文使用的版本是 0.10.2,可以从这里下载。如果你读到这篇文章的时候 LÖVE 已经出了新版本,也可以从这里拿到 0.10.2。更详细的步骤可以参考这个页面。做好这些之后,在项目目录里新建一个 main.lua 文件,内容如下:
function love.load()
end
function love.update(dt)
end
function love.draw()
endIf you run this you should see a window popup and it should show a black screen. In the code above, once your LÖVE project is run the love.load function is run once at the start of the program and love.update and love.draw are run every frame. So, for instance, if you wanted to load an image and draw it, you'd do something like this:
运行之后,你应该会看到一个弹出的窗口,里面是一片黑屏。在上面的代码里,LÖVE 项目启动后,love.load 会在程序开始时执行一次,而 love.update 和 love.draw 则会在每一帧都执行。所以,如果你想加载一张图片并把它画出来,代码大概会是这样:
function love.load()
image = love.graphics.newImage('image.png')
end
function love.update(dt)
end
function love.draw()
love.graphics.draw(image, 0, 0)
endlove.graphics.newImage loads the image texture to the image variable and then every frame it's drawn at position 0, 0. To see that love.draw actually draws the image on every frame, try this:
love.graphics.newImage 会把图片纹理加载到 image 变量里,然后在每一帧都绘制到 (0, 0) 的位置。要更直观地看出 love.draw 的确会每帧都执行一次,可以试试下面这行:
love.graphics.draw(image, love.math.random(0, 800), love.math.random(0, 600))The default size of the window is 800x600, so what this should do is randomly draw the image around the screen really fast:
窗口的默认大小是 800x600,所以这段代码会让图片以非常快的速度在屏幕各处随机闪现:

Note that between every frame the screen is cleared, otherwise the image you're drawing randomly would slowly fill the entire screen as it is drawn in random positions. This happens because LÖVE provides a default game loop for its projects that clears the screen at the end of every frame. I'll go over this game loop and how you can change it now.
要注意的是,每一帧之间屏幕都会被清空。否则的话,你随机画出来的图片会随着不断叠加,慢慢把整个屏幕填满。之所以会这样,是因为 LÖVE 自带了一套默认游戏循环,它会在每一帧结束时清空屏幕。接下来我就来讲讲这套循环,以及你可以怎样修改它。
Game Loop
The default game loop LÖVE uses can be found in the love.run page, and it looks like this:
LÖVE 默认使用的游戏循环可以在 love.run 这个页面里找到,内容大致如下:
function love.run()
if love.math then
love.math.setRandomSeed(os.time())
end
if love.load then love.load(arg) end
-- We don't want the first frame's dt to include time taken by love.load.
if love.timer then love.timer.step() end
local dt = 0
-- Main loop time.
while true do
-- Process events.
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
return a
end
end
love.handlers[name](a,b,c,d,e,f)
end
end
-- Update dt, as we'll be passing it to update
if love.timer then
love.timer.step()
dt = love.timer.getDelta()
end
-- Call update and draw
if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled
if love.graphics and love.graphics.isActive() then
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.origin()
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then love.timer.sleep(0.001) end
end
endWhen the program starts love.run is run and then from there everything happens. The function is fairly well commented and you can find out what each function does on the LÖVE wiki. But I'll go over the basics:
程序启动时,首先执行的就是 love.run,之后所有事情都从这里展开。这个函数本身的注释已经写得比较清楚了,你也可以去 LÖVE wiki 查看每个函数具体是干什么的。不过我还是会把基础部分过一遍:
if love.math then
love.math.setRandomSeed(os.time())
endIn the first line we're checking to see if love.math is not nil. In Lua all values are true, except for false and nil, so the if love.math condition will be true if love.math is defined as anything at all. In the case of LÖVE these variables are set to be enabled or not in the conf.lua file. You don't need to worry about this file for now, but I'm just mentioning it because it's in that file that you can enable or disable individual systems like love.math, and so that's why there's a check to see if it's enabled or not before anything is done with one of its functions.
第一行是在检查 love.math 是否不是 nil。在 Lua 里,除了 false 和 nil 之外,其余所有值都算真值,所以只要 love.math 被定义成了任何东西,if love.math 这个条件就会成立。在 LÖVE 里,这些模块是否启用,是通过 conf.lua 来控制的。你现在还不用太关心这个文件,我提到它只是因为像 love.math 这样的系统模块都可以在里面单独开关,所以在调用它的函数之前,先检查一下模块有没有启用就说得通了。
In general, if a variable is not defined in Lua and you refer to it in any way, it will return a nil value. So if you ask if random_variable then this will be false unless you defined it before, like random_variable = 1.
更一般地说,在 Lua 里,如果一个变量没有被定义,你无论怎样引用它,得到的都会是 nil。所以像 if random_variable 这样的判断,除非你之前已经写过 random_variable = 1 之类的定义,否则结果就是假。
In any case, if the love.math module is enabled (which it is by default) then its seed is set based on the current time. See love.math.setRandomSeed and os.time. After doing this, the love.load function is called:
总之,如果 love.math 模块已经启用,而它默认确实是启用的,那么这里就会根据当前时间设置随机种子。可以参考 love.math.setRandomSeed 和 os.time。做完这一步之后,就会调用 love.load:
if love.load then love.load(arg) endarg are the command line arguments passed to the LÖVE executable when it runs the project. And as you can see, the reason why love.load only runs once is because it's only called once, while the update and draw functions are called multiple times inside a loop (and each iteration of that loop corresponds to a frame).
arg 指的是 LÖVE 可执行文件在运行项目时接收到的命令行参数。你也可以顺便看出来,love.load 之所以只执行一次,原因非常直接,就是它只被调用了一次;而 update 和 draw 则是在一个循环里反复执行,每一次循环都对应游戏中的一帧。
-- We don't want the first frame's dt to include time taken by love.load.
if love.timer then love.timer.step() end
local dt = 0After calling love.load and after that function does all its work, we verify that love.timer is defined and call love.timer.step, which measures the time taken between the two last frames. As the comment explains, love.load might take a long time to process (because it might load all sorts of things like images and sounds) and that time shouldn't be the first thing returned by love.timer.getDelta on the first frame of the game.
在 love.load 调用结束、它该做的初始化都做完之后,程序会检查 love.timer 是否存在,然后调用 love.timer.step。这个函数会测量最近两帧之间的时间差。正如注释里说的那样,love.load 可能会执行很久,比如去加载图片、音效之类的资源,而这些耗时不应该被算进游戏第一帧的 love.timer.getDelta 结果里。
dt is also initialized to 0 here. Variables in Lua are global by default, so by saying local dt it's being defined only to the local scope of the current block, which in this case is the love.run function. See more on blocks here.
这里还把 dt 初始化成了 0。Lua 里的变量默认都是全局的,所以写上 local dt 就表示它只在当前代码块的局部作用域里有效,而这里的代码块就是 love.run 函数本身。关于代码块和作用域,可以看这里。
-- Main loop time.
while true do
-- Process events.
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
return a
end
end
love.handlers[name](a,b,c,d,e,f)
end
end
endThis is where the main loop starts. The first thing that is done on each frame is the processing of events. love.event.pump pushes events to the event queue and according to its description those events are generated by the user in some way, so think key presses, mouse clicks, window resizes, window focus lost/gained and stuff like that. The loop using love.event.poll goes over the event queue and handles each event. love.handlers is a table of functions that calls the relevant callbacks. So, for instance, love.handlers.quit will call the love.quit function if it exists.
这里就是主循环真正开始的地方。每一帧最先做的事,是处理各种事件。love.event.pump 会把事件推入事件队列,这些事件一般都是由用户操作触发的,比如按键、鼠标点击、窗口尺寸变化、窗口失去或获得焦点之类。love.event.poll 这个循环则会依次取出事件队列里的内容并进行处理。love.handlers 是一个函数表,负责调用对应的回调。比如说,love.handlers.quit 在存在的情况下就会去调用 love.quit。
One of the things about LÖVE is that you can define callbacks in the main.lua file that will get called when an event happens. A full list of all callbacks is available here. I'll go over callbacks in more detail later, but this is how all that happens. The a, b, c, d, e, f arguments you can see passed to love.handlers[name] are all the possible arguments that can be used by the relevant functions. For instance, love.keypressed receives as arguments the key pressed, its scancode and if the key press event is a repeat. So in the case of love.keypressed the a, b, c values would be defined as something while d, e, f would be nil.
LÖVE 的一个特点是,你可以在 main.lua 里定义回调函数,当对应事件发生时,它们就会被自动调用。完整的回调列表可以看这里。后面我还会更详细地讲回调,不过它们大致就是通过这种方式被触发的。你看到传给 love.handlers[name] 的 a, b, c, d, e, f,其实就是给各种回调函数预留的所有可能参数。举个例子,love.keypressed 会接收按下的键、它的扫描码,以及这次按键是否为重复触发。所以在 love.keypressed 的场景里,a, b, c 会有具体值,而 d, e, f 就会是 nil。
-- Update dt, as we'll be passing it to update
if love.timer then
love.timer.step()
dt = love.timer.getDelta()
end
-- Call update and draw
if love.update then love.update(dt) end -- will pass 0 if love.timer is disabledlove.timer.step measures the time between the two last frames and changes the value returned by love.timer.getDelta. So in this case dt will contain the time taken for the last frame to run. This is useful because then this value is passed to the love.update function, and from there it can be used in the game to define things with constant speeds, despite frame rate changes.
love.timer.step 会测量最近两帧之间的时间差,并更新 love.timer.getDelta 返回的值。所以这里的 dt 就表示上一帧运行花了多少时间。这个值非常重要,因为它会被传给 love.update,从而让你在游戏里即使遇到帧率波动,也能让物体按照稳定的速度运动。
if love.graphics and love.graphics.isActive() then
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.origin()
if love.draw then love.draw() end
love.graphics.present()
endAfter calling love.update, love.draw is called. But before that we verify that the love.graphics module exists and that we can draw to the screen via love.graphics.isActive. The screen is cleared to the defined background color (initially black) via love.graphics.clear, transformations are reset via love.graphics.origin, love.draw is finally called and then love.graphics.present is used to push everything drawn in love.draw to the screen. And then finally:
调用完 love.update 之后,就会轮到 love.draw。不过在此之前,程序会先确认 love.graphics 模块存在,并且通过 love.graphics.isActive 检查当前是否能向屏幕绘图。接着用 love.graphics.clear 把屏幕清成设定好的背景色,初始情况下是黑色;再通过 love.graphics.origin 重置变换;然后才真正调用 love.draw;最后用 love.graphics.present 把 love.draw 里画出来的内容提交到屏幕上。再往后就是:
if love.timer then love.timer.sleep(0.001) endI never understood why love.timer.sleep needs to be here at the end of the frame, but the explanation given by a LÖVE developer here seems reasonable enough.
我一直没有完全搞明白,为什么 love.timer.sleep 要放在每一帧的最后面,不过这里一位 LÖVE 开发者给出的解释 看起来还是挺说得通的。
And with that the love.run function ends. Everything that happens inside the while true loop is referred to as a frame, which means that love.update and love.draw are called once per frame. The entire game is basically repeating the contents of that loop really fast (like at 60 frames per second), so get used to that idea. I remember when I was starting it took me a while to get an instinctive handle on how this worked for some reason.
到这里,love.run 函数就讲完了。while true 循环里的整套流程,就是我们说的一帧,也就是说 love.update 和 love.draw 都是在每一帧里各执行一次。整个游戏,本质上就是在以极高的速度不断重复这段循环,比如每秒 60 次。所以你最好尽早习惯这种思路。我自己刚开始学的时候,不知道为什么,也花了一阵子才真正形成这种直觉。
11.0+ 的默认函数
function love.run()
-- 1. 没有一开始设置一个随机种子
-- 2. arg 会先解析再传
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
-- We don't want the first frame's dt to include time taken by love.load.
if love.timer then love.timer.step() end
local dt = 0
-- Main loop time.
-- 3. 没有用无限循环
return function()
-- Process events.
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
-- 4. or 0 确保返回值是数字
return a or 0
end
end
love.handlers[name](a,b,c,d,e,f)
end
end
-- Update dt, as we'll be passing it to update
-- 5. timer.step() 直接返回了两帧的时间差
if love.timer then dt = love.timer.step() end
-- Call update and draw
if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled
if love.graphics and love.graphics.isActive() then
-- 6. 先 origin 后 clear,应该没啥区别
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then love.timer.sleep(0.001) end
end
endThere's a helpful discussion on this function on the LÖVE forums if you want to read more about it.
如果你还想继续深入,LÖVE 论坛上也有一篇关于这个函数的讨论,挺值得一看:LÖVE forums。
Anyway, if you don't want to you don't need to understand all of this at the start, but it's helpful to be somewhat comfortable with editing how your game loop works and to figure out how you want it to work exactly. There's an excellent article that goes over different game loop techniques and does a good job of explaining each. You can find it here.
当然了,一开始你也不一定非得把这些细节全都吃透。但至少对“游戏循环是怎么运作的、又可以怎样修改”这件事有点感觉,会非常有帮助。这里还有一篇很经典的文章,专门讲不同的游戏循环写法,而且解释得很好,你可以在这里看到。
Game Loop Exercises
1. What is the role that Vsync plays in the game loop? It is enabled by default and you can disable it by calling love.window.setMode with the vsync attribute set to false.
1. Vsync 在游戏循环里起什么作用?它默认是开启的,你可以通过调用 love.window.setMode,并把 vsync 属性设为 false 来关闭它。
2. Implement the Fixed Delta Time loop from the Fix Your Timestep article by changing love.run.
2. 修改 love.run,实现 Fix Your Timestep 文章中的 Fixed Delta Time 循环。
3. Implement the Variable Delta Time loop from the Fix Your Timestep article by changing love.run.
3. 修改 love.run,实现 Fix Your Timestep 文章中的 Variable Delta Time 循环。
4. Implement the Semi-Fixed Timestep loop from the Fix Your Timestep article by changing love.run.
4. 修改 love.run,实现 Fix Your Timestep 文章中的 Semi-Fixed Timestep 循环。
5. Implement the Free the Physics loop from the Fix Your Timestep article by changing love.run.
5. 修改 love.run,实现 Fix Your Timestep 文章中的 Free the Physics 循环。