第4章 碰撞检测及反馈

本章内容:运用了之前的知识,我们应该可以很随意的移动我们的游戏对象了,当然还有其他移动游戏对象的方法,实际也是基于前面讲的内容,比如一个像魂斗罗里来回旋转的那种子弹。既然提到了子弹,我们就不得不开始游戏的另一个重要构成,碰撞。

碰撞检测及反馈

碰撞对于游戏而言往往并不是我们通常的物理碰撞,而是只任何两个游戏对象,有所交叠。一般而言,碰撞测试的结果只有两个,真或假。
2d游戏的碰撞的种类主要可以分为碰撞球,碰撞盒及多边形碰撞。而对于每一种碰撞,能够提供的检测方式有点测试,面交叠,射线测试几种。下面我们分别来介绍一下。

碰撞球

碰撞球的意思是,把任何需要感知碰撞的物体,绑定在一个球上(平面上是圆),这个球最好能尽可能多的覆盖游戏对象,并且尽可能少的溢出游戏对象。任何与这个球发生碰撞,就相当于与这个游戏对象发生了碰撞。
这种碰撞方式,从感官上是最为简单的,因为它仅仅涉及连个变量,一个是圆心的位置,一个是圆半径。两个球如何碰撞?自然是两个球的距离小于等于两个球的半径之和了。对于碰撞球而言,最关键的部分在于到圆心的距离。
所以对于点是否在球上则有

1
2
3
4
5
6
function dist(x1,y1,x2,y2) --两点间距离公式
return math.sqrt((x1-x2)^2+(y1-y2)^2)
end
function pointTest(x,y,ox,oy,r) -- 待测试点,碰撞球圆心及半径
return dist(x,y,ox,oy)<=r
end

对于两圆是否碰撞则有

1
2
3
function bodyTest(x1,y1,r1,x2,y2,r2)
return dist(x1,y1,x2,y2)<= r1+r2
end

对于直线与圆是否碰撞有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function pointToLine(a,b,c,x,y)
return math.abs(a*x+b*y+c)/math.sqrt(a*a+b*b)
end
function getLineABC(x,y,tx,ty)
local a = (ty-y)/(tx-x)
local b = -1
local c = -x*a+y
return a,b,c
end
function lineTest(x1,y1,x2,y2,ox,oy,r)
local a,b,c = getLineABC(x1,y1,x2,y2)
return pointToLine(a,b,c,ox,oy)<= r
end

线到圆公式用向量计算更为简便。这里仅用最基本几何计算。具体过程不再详细描述。
碰撞球的优点在于变量较少,无偏移计算(圆心即为中心),比较容易理解。缺点在于,仅能支持形状比较规则,接近圆的物体,碰撞计算涉及乘方和开方计算量大,拓展性很差。

碰撞盒

碰撞盒是最常见的碰撞形式了,经常听到的AABB指的就是它,就是把物体的轮廓抽象为矩形,并用它来检测跟其他碰撞盒的互动。
碰撞盒需要提供的是,物体所在的矩形,实际上需求的是ltrb(left,top,right,bottom),即左上角和右下角的坐标。而判断的方式很简单,就是作比较。

一个点和碰撞盒进行碰撞检测。

1
2
3
4
function pointTest(x,y,l,t,r,b)
if x< l or x> r or y< t or y>b then return end
return true
end

这里有个小技巧就是“提前退出”,有的时候,不需要把所有情况都计算到,能提早结束就提早结束。所以使用or而非使用and.
两个碰撞盒间碰撞。

1
2
3
4
5
6
7
8
function bodyTest(Al,At,Ar,Ab,Bl,Bt,Br,Bb)
if pointTest(Al,At,Bl,Bt,Br,Bb)
or pointTest(Ar,At,Bl,Bt,Br,Bb)
or pointTest(Al,Ab,Bl,Bt,Br,Bb)
or pointTest(Ar,Ab,Bl,Bt,Br,Bb) then
return true
end
end

意思是,对于A来讲,任意一个点如果跟B发生碰撞则碰撞。当然,还要考虑到包含关系,如果A正好圈到B外面,那么A的所有点跟B都不会碰撞,但是他们会交叠,因此,要以较大的形状作为点测试对象。以及要考虑那种两个矩形无顶点交叠只有中间交叠的情况。更标准的做法是用轴检测:

1
2
3
4
5
function bodyTest(Al,At,Ar,Ab,Bl,Bt,Br,Bb)
if Ab < Bt or Bb < At then return end
if Ar > Bl or Br < Al then return end
return true
end

这里还是提前退出机制,如果a顶在b顶之上,而a的底还在b的顶之上则,a整个图形都在b之上,不相交。
同理,

碰撞盒的射线,这里暂时不写,之所以给出上面一些代码,并不是说让你们在实际游戏中直接拿来用,而是将通一些原理,方便你能够更好的理解别人的库,以及自己做些简单的应用库。

多边形碰撞

多边形碰撞实际上更加符合现实的情形,只不过对于一般游戏而言,往往不必要那么精确,因为精确意味着更大的计算量,更大的散热,甚至游戏的卡顿。由于多边形的碰撞更加复杂,这里暂时对原理部分不再过多阐述了,仅仅提供一个常用的多边形点测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function pointTest(x,y,verts)
local pX={}
local pY={}
for i=1,#verts,2 do
table.insert(pX, verts[i])
table.insert(pY, verts[i+1])
end
local oddNodes=false
local pCount=#pX
local j=pCount
for i=1,pCount do
if ((pY[i]< y and pY[j]>=y) or (pY[j]< y and pY[i]>=y))
and (pX[i]<=x or pX[j]<=x) then
if pX[i]+(y-pY[i])/(pY[j]-pY[i])*(pX[j]-pX[i])< x then
oddNodes=not oddNodes
end
end
j=i
end
return oddNodes
end

矩阵碰撞

这种碰撞主要用于过去老式的矩阵游戏机,比如俄罗斯方块。不过现在也有个别用像素是否填充作为碰撞标的。如果对于小的矩阵还是可以的,但是如果是动辄1080p的画面来讲,就很不经济了,而且像素往往也不太能够携带额外的信息。这种像素级的检测方式逐渐发展为后文的网格碰撞策略。

碰撞优化策略

由于碰撞的检测往往要遍历大量的游戏对象,进行较大的计算,为了避免一些不必要的碰撞检测,因此成熟的碰撞库往往都进行了策略的优化,这里仅仅介绍两种。
一种是分格的策略,由于很多物体距离很远,根本不需要进行很精细的碰撞检测,因此先把所有单位划分到大的格子中,碰撞检测仅在相邻的格子中所有的单位间进行。
另外一种是行为策略,仅当一个物体移动时,才跟其他进行碰撞检测,否则不进行检测(休眠)。

碰撞反馈

碰撞反馈是单位之间发生碰撞后,由程序自动设定的一些行为。碰撞反馈实际包含两个方面。一个方面是碰撞行为,另一个是碰撞用户界面。
碰撞行为是指,当检测到物体碰撞时,物体之间的位置如何设置。比如,当两个碰撞盒有交叠是,是让他们保持交叠的样子,还是退到两个相接的情况,还是反弹交叠的距离。这是三种最典型的碰撞行为,分别用常用的,“cross”,”slide”,”bounce”来指代上面的情形。注意,这个碰撞行为,仅仅控制物体的位置,对物体运行速度并没有干预。实际上,除了有物理成分的碰撞库,其他都不会对你物体的速度有影响,仅仅是对当前的位置进行判断。
碰撞用户界面是指,当发生碰撞时,把控制权交给用户,来处理如何影响物体的速度,行为等。这种控制权有两种形式,一种是以回调形式的,即一旦发生碰撞,碰撞的对象将以参数的形式激活回调,用户在回调内编程。另一种是,系统将发生碰撞的单位以列表形式传回来,本质上都是一样的。
对于物理碰撞引擎(库)而言,碰撞反馈是在定义碰撞体时就设定好了的。有些碰撞库则需要更多的手动控制。

碰撞的一般流程

对于游戏而言,碰撞有如下流程:

  1. 引入碰撞库;当然如果你没使用碰撞库的化,就要自己写碰撞程序。
  2. 启动碰撞世界,碰撞世界和游戏世界是两个概念,因为碰撞世界里只有碰撞体,游戏世界是游戏对象,他们之间有相互的引用保持联系,一个世界不会直接干预另一个世界。
  3. 对游戏对象绑定碰撞体,碰撞体最好能贴合游戏对象,这样更加真实。在初始化时,游戏对象在游戏世界的位置(x,y坐标),与碰撞世界的位置保持一致。
  4. 游戏对象移动控制。由于对于大多情况,游戏对象的坐标与物理对象坐标并不一致。需要用一个专门的代码来同步。保证当移动了一个世界物体时,另一个世界也能做出相应的反馈。
  5. 物理世界更新,物理世界更新主要是更新物理世界对象的速度,位置,碰撞检测,内置的碰撞回馈等等。
  6. 碰撞处理,当发生碰撞时,游戏世界发生了什么,碰撞后,游戏世界和物理世界如何行为。这是这里要解决的问题。
  7. 同步物理世界与游戏世界。跟上面一样,只不过这次是从物理世界到游戏世界。
  8. 游戏对象删除是一定要删除物理对象,否则物理对象将留在物理世界里,从而对碰撞产生影响。

边缘检测

边缘检测实际上是一种特殊的碰撞,世界到了边了,如何处理呢?有一些是让他们看起来围成一个圈,就是从左边出去,从右边回来。有的让他们碰到边界了就不能再往外移动了,有的是碰到边界又反弹回来。从统一性来讲,可以在物理世界做一个叫做边界的物体,并赋予相应行为。也可以用单独的程序控制,如果选择后者,别忘了同步物理世界和游戏世界。

碰撞跳跃

由于游戏的物理是非线性的,而是离散的,物体的移动是非连续的,所以,很有可能前一帧没有碰撞,后一帧也没有碰撞,但是中间穿过了一个本应发生碰撞的物体,这种现象经常发生在物体移动速度过快上。当然过快的移动,往往导致屏幕上也仅仅是一闪而过。为了避免这种情况,比如子弹来讲,就不再使用实体子弹碰撞了,而是采用射线检测。不过射线检测的效率往往比一般低多了。

Love常用的碰撞库

下面介绍几种love常用的碰撞库,还是像之前所说的,先当作黑箱来用,我们编程能力高了之后,再去理解内部实现以及自己订制和修改库。

box2d

box2d是love内建的物理库,在love.physics下。box2d是一个开源的C++物理引擎,用来模拟物理效果和行为的。比较出名的愤怒的小鸟,割绳子,粘粘世界等游戏都用了这个物理引擎。当然还有比较直接的应用,比如物理蜡笔等等。box2d有别一般碰撞检测库在于它不但能够侦测物体碰撞,而且能够仿真的模拟物体碰撞结果,以及各种受力情况。因此使用了物理引擎的游戏在碰撞表现上更加真实。我们使用box2d,实际上就是跟box2d的各种对象打交道,它主要的对象有以下几种,世界,刚体,形状,部件,连接,碰撞。我们通过创建这些对象,赋予他们属性,使用他们的方法来实现物理模拟。
关于box2d的内容,笔者有一个小工具角Alexar的box2d编辑器。在笔者github上可以下载。这个编辑器可以可视编辑box2d碰撞体,可以导出或导入场景,编辑脚本等等。有兴趣的同学可以去看一下,内部的文档也算齐全,理解起来应该没有问题。
就像在lovewiki中讲的,对于一般的碰撞情形而言,使用box2d就像用一个巨型大锤来敲一个图钉,可以是可以,没有必要。因为box2d的使用并不算十分容易。即使你包装了原来的api。只有当你想要真实的物理模拟时(主要是多边形碰撞,圆形碰撞以及他们的物理反馈,包括摩擦,弹性等等。),才用得上。

bump

bump是一个很小巧的碰撞库,仅仅适用于碰撞盒碰撞。所以别想着它能够用来绑定其他形状的物体,或者旋转。它真的不能。
它的很大一个特点是,并直接在update里面进行物理世界的更新,而是在每一次物体移动时进行碰撞检测。

HC-HardonCollider

hc是一个十分强大的碰撞库,只要你不要求物理行为,完全可以替代box2d在碰撞时的作用。它支持多边形和旋转。经过几次改版,从原来的对象式的转变为立即式的编程模式,有些人用不习惯,其实很好用啦。以后我会开设专题来讲解面向对象式编程和面向组件式编程的各自特点。

上面的库的文档十分齐全,在编程上基本都是大同小异的,只所以上面码了这么多文字,看起来很枯燥,为的是在对碰撞有了较深入的理解后,你们可以快速的上手不同的碰撞库,比较他们的区别,然后有选择的去使用他们。

编程时间

了解到碰撞是什么后,我们可以更加丰富的展现人机互动了。我们这次来实现一个很经典的游戏。乒乓。关于这个游戏的历史,请自行百度吧。

设计阶段

  1. 做一个球桌,用线框画出来,球碰到球桌边界时,会反弹。
  2. 做两个球拍,分别用w,s 和 上下键控制,注意,球拍不能移出球桌边缘。
  3. 做一个球,开始时给球一个向左的初速度。
  4. 球与球拍接触时反弹,并给予球一个球拍移动方向的冲量(改变速度)
  5. 计分系统,每一次球与球拍接触,就给球拍加分。
    我们的碰撞算法决定使用手动的碰撞盒来模拟。手动的边缘检测。球拍的控制已经很容易了,这里不多说。
    我们动手开整。

实现阶段

首先是球桌对象。

1
2
3
4
5
6
local board = {
x = 0, --为了方便,我们把球桌就定为窗体了。
y = 0,
w = 800,
h = 600
}

然后是球,因为我们不希望球能够有加速度,比如重力(因为这个是个俯视游戏),所以只考虑匀速状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
local ball = {
x = 400,
y = 300,
r = 20, --画面为20的圆,但是物理世界是40*40的矩形
--它的碰撞盒是以球位置为中心的一个正方形。
l = -20,
t = -20,
r = 20,
b = 20,
vx = -2,
vy = 0
}

然后是球拍,因为暂时我们还不接触面对对象编程,所以球拍仅仅是数据而且不能被继承。

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
local leftPad = {
x = 100,
y = 300,
w = 30,
h = 150,
l = -15,
t = -75,
r = 15,
b = 75,
}
local rightPad = {
x = 700,
y = 300,
w = 30,
h = 150,
l = -15,
t = -75,
r = 15,
b = 75
}

对象已经建立好啦,我们让各种对象能够移动起来。

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
function keyTest()
local down = love.keyboard.isDown
if down("w") then
leftPad.y = leftPad.y - 1
leftPad.vy = -1
if leftPad.y - leftPad.h/2 < board.y then --边界
leftPad.y = leftPad.h
end
elseif down("s") then
leftPad.y = leftPad.y + 1
leftPad.vy = 1
if leftPad.y + leftPad.h/2 > board.y + board.h then
leftPad.y = board.h + board.y - leftPad.h/2
end
else
leftPad.vy = 0
end
if down("up") then
rightPad.y = rightPad.y - 1
rightPad.vy = -1
if rightPad.y - rightPad.h/2 < board.y then --边界
rightPad.y = rightPad.h
end
elseif down("down") then
rightPad.y = rightPad.y + 1
rightPad.vy = 1
if rightPad.y + rightPad.h/2 > board.y + board.h then
rightPad.y = board.h + board.y - rightPad.h/2
end
else
rightPad.vy = 0
end
end
function ballMove()
ball.x = ball.x + ball.vx
ball.y = ball.y + ball.vy
if ball.x-ball.r < board.x then
ball.x = ball.r + board.x
ball.vx = - ball.vx
elseif ball.x + ball.r > board.w + board.x then
ball.x = board.x + board.w - ball.r
ball.vx = - ball.vx
end
if ball.y-ball.r < board.y then
ball.y = ball.r + board.y
ball.vy = - ball.vy
elseif ball.y + ball.r > board.h + board.y then
ball.y = board.y + board.h - ball.r
ball.vy = - ball.vy
end
end

下面就进入碰撞阶段了,因为本案例游戏对象只有3个(能动的),所以直接进行判断了,而不是遍历。

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
function pointTest(x,y,l,t,r,b)
if x< l or x> r or y< t or y>b then return end
return true
end
function bodyTest(Al,At,Ar,Ab,Bl,Bt,Br,Bb)
if pointTest(Al,At,Bl,Bt,Br,Bb)
or pointTest(Ar,At,Bl,Bt,Br,Bb)
or pointTest(Al,Ab,Bl,Bt,Br,Bb)
or pointTest(Ar,Ab,Bl,Bt,Br,Bb) then
return true
end
end
function leftCollTest()
local al, at, ar, ab =
ball.x + ball.l, ball.y + ball.t, ball.x + ball.r, ball.y + ball.b
local bl, bt, br, bb =
leftPad.x - leftPad.w/2
leftPad.y - leftPad.h/2
leftPad.x + leftPad.w/2
leftPad.y + leftPad.h/2
local coll = bodyTest(al, at, ar, ab,bl, bt, br, bb)
if coll then
ball.vx = -ball.vx
ball.vy = ball.vy + leftPad.vy/2
end
end
function rightCollTest()
local al, at, ar, ab =
ball.x + ball.l, ball.y + ball.t, ball.x + ball.r, ball.y + ball.b
local bl, bt, br, bb =
rightPad.x - rightPad.w/2
rightPad.y - rightPad.h/2
rightPad.x + rightPad.w/2
rightPad.y + rightPad.h/2
local coll = bodyTest(al, at, ar, ab,bl, bt, br, bb)
if coll then
ball.vx = -ball.vx
ball.vy = ball.vy + rightPad.vy/2
end
end

好啦碰撞做好了,我们就可以绘制所有物品了,这里不再赘述,放到后面代码自己查看。
上述代码并不是最终的库,但是解释了一些碰撞的原理。我希望大家回头可以仔细研究下别人比较成熟的碰撞库,最终能够为自己所用。

作业

  1. 自行阅读bump的案例和文档,使用bump库来完成本课的案例。
  2. 既然会碰撞了,我们试着做一个方块马里奥跑酷吧,前面的课程可以让他跳跃,今天的课程可以让他跳到障碍物上了。也许现在有些困难,没关系,我们要勇于尝试,至少把自己能做出的东西做出来。
  3. 试着用box2d来完成今天的案例,体会不同碰撞库的代码形式及特点。

本章代码

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
function love.load()
board = {
x = 0, --为了方便,我们把球桌就定为窗体了。
y = 0,
w = 800,
h = 600
}
ball = {
x = 400,
y = 300,
r = 20, --画面为20的圆,但是物理世界是40*40的矩形
l = -20,
t = -20,
r = 20,
b = 20,
vx = -4,
vy = 0
}
leftPad = {
x = 100,
y = 300,
w = 30,
h = 150,
l = -15,
t = -75,
r = 15,
b = 75,
}
rightPad = {
x = 700,
y = 300,
w = 30,
h = 150,
l = -15,
t = -75,
r = 15,
b = 75,
}
end
function keyTest()
local down = love.keyboard.isDown
if down("w") then
leftPad.y = leftPad.y - 2
leftPad.vy = -2
if leftPad.y - leftPad.h/2 < board.y then --边界
leftPad.y = leftPad.h
end
elseif down("s") then
leftPad.y = leftPad.y + 2
leftPad.vy = 2
if leftPad.y + leftPad.h/2 > board.y + board.h then
leftPad.y = board.h + board.y - leftPad.h/2
end
else
leftPad.vy = 0
end
if down("up") then
rightPad.y = rightPad.y - 2
rightPad.vy = -2
if rightPad.y - rightPad.h/2 < board.y then --边界
rightPad.y = rightPad.h
end
elseif down("down") then
rightPad.y = rightPad.y + 2
rightPad.vy = 2
if rightPad.y + rightPad.h/2 > board.y + board.h then
rightPad.y = board.h + board.y - rightPad.h/2
end
else
rightPad.vy = 0
end
end
function ballMove()
ball.x = ball.x + ball.vx
ball.y = ball.y + ball.vy
if ball.x-ball.r < board.x then
ball.x = ball.r + board.x
ball.vx = - ball.vx
elseif ball.x + ball.r > board.w + board.x then
ball.x = board.x + board.w - ball.r
ball.vx = - ball.vx
end
if ball.y-ball.r < board.y then
ball.y = ball.r + board.y
ball.vy = - ball.vy
elseif ball.y + ball.r > board.h + board.y then
ball.y = board.y + board.h - ball.r
ball.vy = - ball.vy
end
end
function pointTest(x,y,l,t,r,b)
if x< l or x> r or y< t or y>b then return end
return true
end
function bodyTest(Al,At,Ar,Ab,Bl,Bt,Br,Bb)
if pointTest(Al,At,Bl,Bt,Br,Bb)
or pointTest(Ar,At,Bl,Bt,Br,Bb)
or pointTest(Al,Ab,Bl,Bt,Br,Bb)
or pointTest(Ar,Ab,Bl,Bt,Br,Bb) then
return true
end
end
function leftCollTest()
local al, at, ar, ab =
ball.x + ball.l, ball.y + ball.t, ball.x + ball.r, ball.y + ball.b
local bl, bt, br, bb =
leftPad.x - leftPad.w/2,
leftPad.y - leftPad.h/2,
leftPad.x + leftPad.w/2,
leftPad.y + leftPad.h/2
local coll = bodyTest(al, at, ar, ab,bl, bt, br, bb)
if coll then
ball.vx = -ball.vx
ball.vy = ball.vy + leftPad.vy/2
end
end
function rightCollTest()
local al, at, ar, ab =
ball.x + ball.l, ball.y + ball.t, ball.x + ball.r, ball.y + ball.b
local bl, bt, br, bb =
rightPad.x - rightPad.w/2,
rightPad.y - rightPad.h/2,
rightPad.x + rightPad.w/2,
rightPad.y + rightPad.h/2
local coll = bodyTest(al, at, ar, ab,bl, bt, br, bb)
if coll then
ball.vx = -ball.vx
ball.vy = ball.vy + rightPad.vy/2
end
end
function love.update()
leftCollTest()
rightCollTest()
keyTest()
ballMove()
end
function love.draw()
love.graphics.setColor(55, 50, 50, 250)
love.graphics.rectangle("fill", board.x, board.y , board.w, board.h)
love.graphics.setColor(255, 0, 0, 255)
love.graphics.circle("fill", ball.x, ball.y, ball.r)
love.graphics.setColor(0, 255, 0, 255)
love.graphics.rectangle("fill", leftPad.x - leftPad.w/2, leftPad.y - leftPad.h/2, leftPad.w, leftPad.h)
love.graphics.rectangle("fill", rightPad.x - rightPad.w/2, rightPad.y - rightPad.h/2, rightPad.w, rightPad.h)
end