第2章 让画面动起来

本章内容:本节课主要讲解如何让游戏对象动起来。包括匀速运动,匀变速运动,变加速运动等等。可能有的人一听这些物理就快疯了。实际上他们并不复杂,尤其用代码实现更为直观。

让画面动起来

运动

我们说的运动,或者叫做位移,换个说法很容易就是位置的改变,而对于2d游戏来讲,一个物体的位置只有两个元素x,y即他们在世界坐标(虚拟游戏坐标,默认条件下在与屏幕坐标系重合,但也可以移动和旋转,详细后面关于摄像机章节会讲到)。而改变了一个物体的位置,说明他们的x,y产生了变化。
我们在屏幕中看到的一个移动的圆,实际上移动的不是真正的圆,而是圆心所在的坐标,而圆是被画到那个坐标上的。
另外,游戏中物体的运动与现实一个很大的区别是,他们的运动并不是连续的,而是跳跃的。(额,我有种科幻的想法)。也就是比如一个物体以每帧100像素的速度运动,前一秒和后一帧之间的距离是100,但是并没有0.5帧的概念,也就是这个物体永远都不会存在在50的位置上。但是我们看起来它还是很流畅的移动。是因为我们大脑自动的把两帧连接到一起了。或者大脑欺骗我们说,他们是连续的,并忽略了这种跳跃。

匀速运动

匀速运动,也就是x,y在每帧的变化量是一样的。我们用vx,xy表示这个定值,用代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
function love.load()
ball = {
x = 100,
y = 100,
vx = 1,
vy = 2
}
end
function love.update(dt)
ball.x = ball.x + ball.vx
ball.y = ball.y + ball.vy
end

这个例子实际上跟我们上一章的移动方式是一样的。关于速度(向量),速率(标量),速度沿坐标轴的分量(向量),这些概念,如果你不是很熟悉,希望回过头翻书或者百度学习一下,这里先不管,后面我们再做一个专题。
这样移动的例子也比较多,比如超级玛丽里面一些平台的移动,大多数怪物的移动,一般游戏的子弹移动等。这种移动模式的好处是简单,当然不好的地方是看起来比较生硬,但是类似子弹用这种方式就很好。

匀加速运动

物理经常会说一个力施加在物体上就会产生一个匀加速运动。停止施力,由于摩擦,又会变成匀减速运动。所谓匀变速运动就是它的加速度(速度的变化量是恒定的)。我们这时需要引入一个ax,ay来表示他们的加速度。(实际上这些跟我们数学、物理)经常使用的变量名称保持一致。用代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function love.load()
ball = {
x = 100,
y = 100,
vx = 10,
vy = -5,
ax = 0,
ay = 0.1
}
end
function love.update(dt)
ball.vx = ball.vx + ball.ax
ball.vy = ball.vy + ball.ay
ball.x = ball.x + ball.vx
ball.y = ball.y + ball.vy
end

看了上面的代码,有的同学就要说了,中学学的是vt = v0 + a*t,而这又是什么?注意,游戏是一个过程,是一个迭代叠加的结果,而不是站在起点来预测未来某点的状态。可能比较绕口,理解起来就是游戏实际上回到了这个公式的本源,就是速度不断叠加,位置不断更新。另外,由于我们有可能希望他们的加速度为0,来停止加速,那么上面的物理公式就不适用了。
匀加速的应用十分广泛,因为它最符合现实的移动,比如模拟重力,马里奥的跑动和跳跃,一些抛物线运动,比如愤怒的小鸟。赛车游戏的加速,减速等。

变加速运动

变加速运动,往往不是只时而停止,时而加速(比如小汽车)这种情况,而是指物体的移动轨迹跟时间成一定函数关系式。可能叫起来比较复杂,我们通常叫缓动(tween)。它是一种算法,或者是一类库,来帮助你实现这种效果。比如下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Tween = require "tween"
function love.load()
ball = {
x = 100,
y = 100,
}
--参数为:1 持续时间,2 对象 3 目标属性 4 曲线
ball.tween =Tween.new(1,ball,{x = 500, y = 500 },"inElastic")
end
function love.update(dt)
ball.tween:update(dt)
end

上面的代码引用了一个tween库,你需要首先把tween库(一个.lua文件)下载下来,放入项目根目录。Tween内置了数种变加速公式,可以在tween的文档中查询到。(一般而言,对于任意的库,在awesome-love2d中都可以找到其github的项目,收录到awesome里的基本文档都很齐全。如果找不到则搜论坛,如果搜不到要么问群,要么自己看源码)。关于库的引用及自定义库相关内容,后文将单独介绍,这里可以当做黑箱使用——你不需要知道原理,仅仅按照案例使用就行了。功能类似的库还有flux,hump.timer等。
上面代码的含义是,把ball的位置托管给了tween来管理,tween会在1秒内,使用inElastic的轨迹算法,把ball移动到500,而内部具体它的速度是多少,加速度是多少,我们均不用管。它的效果很明显,也很好看。
缓动移动的应用也很普遍,比如ui(用户界面,比如“开始游戏”按钮)界面的移动,一些粒子效果的移动等等。另外,tween的作用远远不只在移动位置上,也可以用来改变速度,改变颜色等等一切数值的改变。因为他们看起来很漂亮,而且我们不需要关心内部的速度如何。
注意: 当你把上面通过速度控制位置和缓动控制位置混合使用,会造成混乱。

其他移动

除了上述介绍的一些运动外,我们常见的还有沿圆弧轨迹运动,沿正弦曲线轨迹运动等等,实际上,他们都是对x,y按一定函数关系进行变换,我们涉及的主要参数有x,y和t,其中,t是时间,因为我们要计算的是某个时间,对于x,y的瞬时位置。而我们从update只能得到dt,也就是帧时间,因此,我们要做一个简单的叠加,来让时间能够累计。
下面给出一个简单的让物体沿正弦曲线运动的方法:

1
2
3
4
5
6
7
local ball = {x = 0,y = 300}
local t = 0
function love.update(dt)
t = t+ dt*5 --这里可以控制正弦的频率
ball.x = ball.x + 3
ball.y = 300 + math.sin(t)* 100 --这里调整振幅
end

这里一定注意一个叠加关系,如果上面的代码改为 ball.y = ball.y + math.sin(t)*100,将变成一个正弦的不断叠加。可能这并不是你所预期的。
再给出一个运动速度与距离相关的代码,这里的效果是离预期越近,你的速度越慢。当然,你也可以用tween来解决。

1
2
3
4
5
6
7
8
9
local ball = {x = 0, y = 0}
local target = {x = 400, y = 300}
function love.update(dt)
ball.vx = (target.x - ball.x) / 15
ball.vy = (target.y - ball.y) / 15
ball.x = ball.x + ball.vx
ball.y = ball.y + ball.vy
end

应该很容易理解,当目标和小球的距离减少时,它的速度也将减少,而距离为0时,速度也为0,就停止了。
其他改变速度(冲量),改变位置(速度),甚至改变加速度(作用力)的方法,这里就不再介绍了,根据你的需求自行开发吧。

关于帧时间间隔

帧时间间隔的概念,是因为我们的游戏循环之间是存在间隔的,就像我们刚才说的,帧与帧之间是跳跃的而非连续的(离散)。而他们之间的时间就这叫做帧间隔时间、帧时间、delta time、dt。而每秒钟帧的计数,叫做帧率,或者叫FPS(frame per second),我们通常游戏听到的,显卡越好fps越高就是说的它,fps越高画面越流畅,视觉感觉越好,而低fps会让人感觉顿挫(连大脑都骗不了了)。

1
fps = 1 / dt --(dt的单位是 秒/帧,所以fps的单位是帧/秒)

另外还有个概念就是垂直同步,Vertical synchronization, Vsync,它可以很直接的限制fps到60。它本来的作用是为了与显示器的刷新率同步的,对于画面高速移动的游戏,有可能会造成画面不稳定,但也可以用来限制没有必要的高刷新率来节能,或者保持帧率稳定。love默认开启垂直同步的,你也可以关掉,这时你要特别注意帧率对物体速度和其他行为的影响。

说了这么多,它对于游戏有什么用呢?因为dt即使很简单的游戏,也有可能因为外部原因导致不稳定。如果你的速度是每帧10像素,而帧的速度是改变的,那么你物体的速度也会因此而改变,帧速度越高,你物体运动的越快,反之亦然。
为了避免这种情况,就要把速度 vx,vy的单位,由 像素/帧 改为 像素/秒 ,如何改呢,我们之前说了,dt的单位是秒/帧,用像素/秒和秒/帧相乘,得到的就是像素/帧,也就是每个update物体应该移动的距离,所以有下面的代码:

1
2
3
4
function love.update(dt)
ball.x = ball.x + ball.vx * dt
ball.y = ball.y + ball.vy * dt
end

此时,小球的移动速度就跟帧率无关了。对于加速度同样适用只要你的单位中有时间因素就要进行相应的变换,因为我们实际运行的单位是帧。

love编程时间

好啦,上面啰嗦了那么多,我们开始实际操作了。

设计阶段

这次,我们希望做一个小球,这个小球只要鼠标点击屏幕任何位置,它就会以几种形式移动向鼠标所在的位置。(小球不会自动停止)

  1. 点击鼠标左键,小球匀速移动
  2. 点击鼠标右键,小球加速移动
  3. 点击鼠标中键,小球以弹性轨迹移动到指定位置。
  4. 按下空格键,小球停止,并回到屏幕中心。
    这里有一个移动方式切换,看起来有点复杂,实际上用一个判断跳转即可,读了代码我们再详细解释。

实现阶段

首先是建立小球的对象。因为这回有了几种操作模式,我们就把上面的代码综合起来就行啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Tween = require "tween"
function love.load()
ball = {
x = 400, --屏幕中心
y = 300,
vx = 0,
vy = 0,
ax = 0,
ay = 0,
mode = "匀速",
tween = nil, --预留给tween的,实际有没有都可以,但是这里是方便查阅的
tx = 0, --用来存储目标位置的
ty = 0
}
end

然后是点击鼠标后的响应阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function love.mousepressed(key,x,y) --关于参数,自己wiki
ball.tx,ball.ty = x,y
if key == 1 then
ball.mode = "匀速"
ball.vx = (ball.tx - ball.x)/3 --速度为3秒到达,小学数学
ball.vy = (ball.ty - ball.y)/3
elseif key == 2 then
ball.mode = "加速"
ball.vx = 0 --速度归零
ball.vy = 0
ball.ax = (ball.tx - ball.x)/30 --这个量。。。好吧 我没算过,自己用你的物理公式来算抵达时间吧
ball.ay = (ball.ty - ball.y)/30
else --key==3
ball.mode = "变速"
ball.tween = Tween.new(3,ball,{x = ball.tx,y = ball.ty},"inElastic")
end
end
function love.keypressed(key)
if key == "space" then --空格键
ball.mode = "匀速"
ball.x = 400,
ball.y = 300,
ball.vx = 0,
ball.vy = 0
end
end

上面的代码,除了设置了一个mode参数用来判断当前的的移动模式,另外设置了一些速度位置的量,都是很简单的数学。
下面是分条件更新部分。
除了用参数的方法获取鼠标位置,也可以用love.mouse.getPosition()来实时获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function love.update(dt)
if ball.mode == "匀速" then
ball.x = ball.x + ball.vx * dt
ball.y = ball.y + ball.vy * dt
elseif ball.mode == "加速" then
ball.vx = ball.vx + ball.ax * dt
ball.vy = ball.vy + ball.ay * dt
ball.x = ball.x + ball.vx * dt
ball.y = ball.y + ball.vy * dt
else
ball.tween:update(dt)
end
end

实际上就是上面讲过的几种形式的综合。
接下来是绘制部分。简单的演示,只用了一个描边的小球。之所以用球是因为它自带的以圆心为中心。

1
2
3
4
5
6
function love.draw()
love.graphics.setColor(255,55,55,155)
love.graphics.circle("fill",ball.x,ball.y,30)
love.graphics.setColor(255,155,155,255)
love.graphics.circle("line",ball.x,ball.y,30)
end

好啦,对于游戏单位的移动部分就讲到这里,总体的方式就是上面几种,当然仍然有很多其他方式,比如让物体沿轨迹移动,让物体沿曲线做匀速运动等等。这些的处理方法就另当别论了,主要应用的是一些数学内容,以后如果碰到的话,我们单独讲解。

游戏单位的位置移动就讲到这里,下一节课我们要思考各种角度的问题,包括自转,公转,相对角度等等。

作业

  1. 按本次教程内容修改参数,自己实现另一种形式。
  2. 设计一个边界判断,即当物体的x大于800时或小于0时,速度反向(ball.vx = -ball.vx)。y轴同理。这样就可以实现让小球呆在盒子里了。关于边界判断问题后面还会具体讲。
  3. 在上面的基础上实现一个抛物线小球。实际上就是一个具有初速度的,ay = 某个正值的小球。
  4. 在上面的基础上,按空格键让小球跳起来。(当按键按下时,vy = 某个负值)
  5. 根据tween或者flux库的demo示例,自己试试做一个很好看的东西出来。

本课代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Tween = require "assets/tween"
function love.load()
ball = {
x = 400, --屏幕中心
y = 300,
vx = 0,
vy = 0,
ax = 0,
ay = 0,
mode = "匀速",
tween = nil, --预留给tween的,实际有没有都可以,但是这里是方便查阅的
tx = 0, --用来存储目标位置的
ty = 0
}
end
function love.mousepressed(x,y,key) --关于参数,自己wiki
ball.tx,ball.ty = x,y
if key == 1 then
ball.mode = "匀速"
ball.vx = (ball.tx - ball.x)/3 --速度为3秒到达,小学数学
ball.vy = (ball.ty - ball.y)/3
elseif key == 2 then
ball.mode = "加速"
ball.vx = 0 --速度归零
ball.vy = 0
ball.ax = (ball.tx - ball.x)/10 --这个量。。。好吧 我没算过,自己用你的物理公式来算抵达时间吧
ball.ay = (ball.ty - ball.y)/10
else --key==3
ball.mode = "变速"
ball.tween = Tween.new(3,ball,{x = ball.tx,y = ball.ty},"outElastic")
end
end
function love.update(dt)
if ball.mode == "匀速" then
ball.x = ball.x + ball.vx * dt
ball.y = ball.y + ball.vy * dt
elseif ball.mode == "加速" then
ball.vx = ball.vx + ball.ax * dt
ball.vy = ball.vy + ball.ay * dt
ball.x = ball.x + ball.vx * dt
ball.y = ball.y + ball.vy * dt
else
ball.tween:update(dt)
end
end
function love.draw()
love.graphics.setColor(255,55,55,155)
love.graphics.circle("fill",ball.x,ball.y,30)
love.graphics.setColor(255,155,155,255)
love.graphics.circle("line",ball.x,ball.y,30)
end