上次我们说到了向量,不得不说向量是一个伟大的发明,在单纯的数字运算之中,居然就把方向也包含其中。对于如今的我们来看,非常普通的事情,几百年前的人们能够考虑到这个,实在是非常的不容易。不过同时我们也要有这样的意识——我们现在所使用的数学,未必就是最完美的。时代发展科技进步,或许我们会有更好的方式来诠释我们的世界。想想一片叶子飘落,有它独特的轨迹,如果要人类计算出来那个轨迹,即便可能,也是无比繁杂的。叶子懂我们的数学吗?不,它不懂,但它就优雅的落了下来。自然有着我们尚无法理解的思考方式,我们现在所使用的工具,还是太复杂!人类要向“道”继续努力才行啊。
扯远了,虽然不记得学校里是什么时候开始接触到向量的,不过肯定也不会太晚,如果你不知道什么是向量,最好先找一本书看看吧,这里只会有一些最最核心的讲解。
引入向量
我们先考虑二维的向量,三维也差不多了,而游戏中的运动最多只用得到三维,更高的留给以后的游戏吧~
向量的表示和坐标很像,(10,20)对坐标而言,就是一个固定的点,然而在向量中,它意味着x方向行进10,y方向行进20,所以坐标(0,0)加上向量(10,20)后,就到达了点(10,20)。
向量可以通过两个点来计算出来,如下图,A经过向量AB到达了B,则向量AB就是(30, 35) – (10, 20) = (20, 15)。我们也能猜到向量BA会是(-20, -15),注意向量AB和向量BA,虽然长度一样,但是方向不同。
在Python中,我们可以创建一个类来存储和获得向量(虽然向量的写法很像一个元组,但因为向量有很多种计算,必须使用类来完成):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
Vector2
(
object
)
:
def
__init__
(
self
,
x
=
0.0
,
y
=
0.0
)
:
self
.
x
=
x
self
.
y
=
y
def
__str__
(
self
)
:
return
"(%s, %s)"
%
(
self
.
x
,
self
.
y
)
@
classmethod
def
from_points
(
cls
,
P1
,
P2
)
:
return
cls
(
P2
[
0
]
–
P1
[
0
]
,
P2
[
1
]
–
P1
[
1
]
)
#我们可以使用下面的方法来计算两个点之间的向量
A
=
(
10.0
,
20.0
)
B
=
(
30.0
,
35.0
)
AB
=
Vector2
.
from_points
(
A
,
B
)
print
AB
|
原理上很简单,函数修饰符@不用我说明了吧?如果不明白的话,可以参考Python的编程指南。
向量的大小
向量的大小可以简单的理解为那根箭头的长度,勾股定理熟稔的各位立刻知道怎么计算了:
1
2
|
def
get_magnitude
(
self
)
:
return
math
.
sqrt
(
self
.
x
*
*
2
+
self
.
y
*
*
2
)
|
把这几句加入到刚刚的Vector2里,我们的向量类就多了计算长度的能力。嗯,别忘了一开始要引入math库。
单位向量
一开头说过,向量有着大小和方向两个要素,通过刚刚的例子,我们可以理解这两个意思了。在向量的大家族里,有一种比较特殊的向量叫“单位向量”,意思是大小为1的向量,我们还能把任意向量方向不变的缩放(体现在数字上就是x和y等比例的缩放)到一个单位向量,这叫向量的规格(正规)化,代码体现的话:
1
2
3
4
|
def
normalize
(
self
)
:
magnitude
=
self
.
get_magnitude
(
)
self
.
x
/=
magnitude
self
.
y
/=
magnitude
|
使用过normalize方法以后,向量就成了一个单位向量。单位向量有什么用?我们以后会看到。
向量运算
我们观察下图,点B由A出发,通过向量AB到达,C则有B到达,通过BC到达;C直接由A出发的话,就得经由向量AC。
由此我们得到一个显而易见的结论向量AC = 向量AB + 向量BC。向量的加法计算方法呼之欲出:
(20, 15) + (-15, 10) = (20-15, 15+10) = (5, 25)
把各个方向分别相加,我们就得到了向量的加法运算法则。很类似的,减法也是同样,把各个方向分别想减,可以自己简单验证一下。代码表示的话:
1
2
3
4
|
def
__add__
(
self
,
rhs
)
:
return
Vector2
(
self
.
x
+
rhs
.
x
,
self
.
y
+
rhs
.
y
)
def
__sub__
(
self
,
rhs
)
:
return
Vector2
(
self
.
x
-
rhs
.
x
,
self
.
y
-
rhs
.
y
)
|
两个下划线“__”为首尾的函数,在Python中一般就是重载的意思,如果不知道的话还需要稍微努力努力:)当然,功力稍深厚一点的,就会知道这里super来代替Vector2可能会更好一些,确实如此。不过这里只是示例代码,讲述一下原理而已。
有加减法,那乘除法呢?当然有!不过向量的乘除并不是发生在两个向量直接,而是用一个向量来乘/除一个数,其实际意义就是,向量的方向不变,而大小放大/缩小多少倍。如下图:
1
2
3
4
|
def
__mul__
(
self
,
scalar
)
:
return
Vector2
(
self
.
x
*
scalar
,
self
.
y
*
scalar
)
def
__div__
(
self
,
scalar
)
:
return
Vector2
(
self
.
x
/
scalar
,
self
.
y
/
scalar
)
|
向量的运算被广泛的用来计算到达某个位置时的中间状态,比如我们知道一辆坦克从A到B,中间有10帧,那么很显然的,把步进向量通过(B-A)/10计算出来,每次在当前位置加上就可以了。很简单吧?
更好的向量类
我们创造的向量类已经不错了,不过毕竟只能做一些简单的运算,别人帮我们已经写好了更帅的库(早点不拿出来?写了半天…… 原理始终是我们掌握的,自己动手,印象更深),是发挥拿来主义的时候了(可以尝试使用easy_install gameobjects简单的安装起来)。下面是一个使用的例子:
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from
gameobjects
.
vector2
import
*
A
=
(
10.0
,
20.0
)
B
=
(
30.0
,
35.0
)
AB
=
Vector2
.
from_points
(
A
,
B
)
print
"Vector AB is"
,
AB
print
"AB * 2 is"
,
AB
*
2
print
"AB / 2 is"
,
AB
/
2
print
"AB + (–10, 5) is"
,
AB
+
(–
10
,
5
)
print
"Magnitude of AB is"
,
AB
.
get_magnitude
(
)
print
"AB normalized is"
,
AB
.
get_normalized
(
)
# 结果是下面
Vector
AB
is
(
20
,
15
)
AB
*
2
is
(
40
,
30
)
AB
/
2
is
(
10
,
7.5
)
AB
+
(
-
10
,
5
)
is
(
10
,
20
)
Magnitude
of
AB
is
25.0
AB
normalized
is
(
0.8
,
0.6
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
from
gameobjects
.
vector2
import
*
A
=
(
10.0
,
20.0
)
B
=
(
30.0
,
35.0
)
AB
=
Vector2
.
from_points
(
A
,
B
)
print
"Vector AB is"
,
AB
print
"AB * 2 is"
,
AB
*
2
print
"AB / 2 is"
,
AB
/
2
print
"AB + (–10, 5) is"
,
AB
+
(–
10
,
5
)
print
"Magnitude of AB is"
,
AB
.
get_magnitude
(
)
print
"AB normalized is"
,
AB
.
get_normalized
(
)
# 结果是下面
Vector
AB
is
(
20
,
15
)
AB
*
2
is
(
40
,
30
)
AB
/
2
is
(
10
,
7.5
)
AB
+
(
-
10
,
5
)
is
(
10
,
20
)
Magnitude
of
AB
is
25.0
AB
normalized
is
(
0.8
,
0.6
)
|
使用向量的游戏动画
终于可以实干一番了!这个例子比我们以前写的都要帅的多,小鱼不停的在我们的鼠标周围游动,若即若离:
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
|
background_image_filename
=
'sushiplate.jpg'
sprite_image_filename
=
'fugu.png'
import
pygame
from
pygame
.
locals
import
*
from
sys
import
exit
from
gameobjects
.
vector2
import
Vector2
pygame
.
init
(
)
screen
=
pygame
.
display
.
set_mode
(
(
640
,
480
)
,
0
,
32
)
background
=
pygame
.
image
.
load
(
background_image_filename
)
.
convert
(
)
sprite
=
pygame
.
image
.
load
(
sprite_image_filename
)
.
convert_alpha
(
)
clock
=
pygame
.
time
.
Clock
(
)
position
=
Vector2
(
100.0
,
100.0
)
heading
=
Vector2
(
)
while
True
:
for
event
in
pygame
.
event
.
get
(
)
:
if
event
.
type
==
QUIT
:
exit
(
)
screen
.
blit
(
background
,
(
0
,
0
)
)
screen
.
blit
(
sprite
,
position
)
time_passed
=
clock
.
tick
(
)
time_passed_seconds
=
time_passed
/
1000.0
# 参数前面加*意味着把列表或元组展开
destination
=
Vector2
(
*
pygame
.
mouse
.
get_pos
(
)
)
-
Vector2
(
*
sprite
.
get_size
(
)
)
/
2
# 计算鱼儿当前位置到鼠标位置的向量
vector_to_mouse
=
Vector2
.
from_points
(
position
,
destination
)
# 向量规格化
vector_to_mouse
.
normalize
(
)
# 这个heading可以看做是鱼的速度,但是由于这样的运算,鱼的速度就不断改变了
# 在没有到达鼠标时,加速运动,超过以后则减速。因而鱼会在鼠标附近晃动。
heading
=
heading
+
(
vector_to_mouse
*
.
6
)
position
+=
heading
*
time_passed_seconds
pygame
.
display
.
update
(
)
|
虽然这个例子里的计算有些让人看不明白,但是很明显heading的计算是关键,如此复杂的运动,使用向量居然两句话就搞定了~看来没有白学。
动画总结
- 正如上一章所说,所谓动画,不过是在每一帧上,相对前一帧把精灵的坐标在加减一些而已;
- 使用时间来计算加减的量以在不同性能的计算机上获得一致的动画效果;
- 使用向量来计算运动的过程来减轻我们的劳动,在3D的情况下,简单的使用Vector3便可以了。
如今我们已经学习到了游戏动画制作的精髓,一旦可以动起来,就能创造无数让人叹为观止的效果,是不是应该写个程序在朋友们面前炫耀炫耀了?
在下面,我们要学习接受输入和游戏里的物体互动起来。