这一章描述如何以一种可以预测的方式在屏幕上移动对象,和如何使运动在其他人电脑里面一致。

理解帧率

我们需要知道计算机游戏中关于运动的第一件事情就是没有什么东西真正在移动。电脑屏幕或电视机展示给我们一系列图片,当两张图片间隔时间够短时,我们的大脑将这些图片混合在一起从而制造了流畅运动的假象。一张图片称为一帧,FPS(Frame Per Second)是每秒的帧数,也就是帧率。产生流畅运动需要的帧数,因人而异。

游戏的帧率也受限于显示设备的刷新速度。比如,显示器的刷新速度为60HZ,也就是每秒刷新60次。产生帧的速度比刷新速度快会导致“tearing”现象,即下一个帧混进前一个帧。电脑要做的事情越多,帧率就会越慢。好消息是现在的桌面电脑已经足以产生你想要的视觉效果。

只需记住几个常量:一般的电视画面是24FPS;30FPS基本可以给玩家提供流畅的体验了;60FPS是LCD常用的刷新率,所以你的游戏的帧率再高也没什么意义了;在70FPS以上,很少有人能察觉任何提升了!

直线运动

 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
background_image_filename = 'sushiplate.jpg'
sprite_image_filename = 'fugu.png'

import pygame
from pygame.locals import *
from sys import exit

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)

# The x coordinate of our sprite
x = 0.

while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            exit()

    screen.blit(background, (0, 0))
    screen.blit(sprite, (x, 100))
    x += 10.

    # If the image goes off the end of the screen, move it back
    if x > 640.:
        x -= 640.

    pygame.display.update()

这段代码有两个问题,第一个是我们不能精确知道画一张图片到屏幕需要多长时间,另一个问题是这个精灵在配置差的机器上移动慢,在配置好的机器上移动更快。

关于时间

解决第一个问题的技巧是使运动基于时间。我们需要知道距离上一个帧已经过去多长时间,据此我们能相应地在屏幕上放置任何物体。pygame.time模块有一个Clock对象可以用来跟踪时间。使用**pygame.time.Clock()**创建clock对象。

1
clock = pygame.time.Clock()

一旦创建了clock对象,你应该每一帧调用一次tick函数,返回上次调用该函数已过去的时间,单位毫秒。

1
time_passed = clock.tick()

tick函数也接收一个可选参数表示最大帧率。

1
2
3
# Game will run at a maximum 30 frames per second
time_passed = clock.tick(30)
time_passed_seconds = time_passed / 1000.0

我们要怎样使用这个time_passed_seconds来移动精灵呢?需要做的第一件事就是选择一个精灵移动的速度。接下来只要用这个速度乘于时间就能得出精灵该移动多远。

 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
background_image_filename = 'sushiplate.jpg'
sprite_image_filename = 'fugu.png'

import pygame
from pygame.locals import *
from sys import exit

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)

# Our clock object
clock = pygame.time.Clock()

# X coordinate of our sprite
x = 0.
# Speed in pixels per second
speed = 250.

while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            exit()

    screen.blit(background, (0, 0))
    screen.blit(sprite, (x, 100))

    time_passed = clock.tick()
    time_passed_seconds = time_passed / 1000.0

    distance_moved = time_passed_seconds * speed
    x += distance_moved

    if x > 640.:
        x -= 640.

    pygame.display.update()

理解帧率和精灵移动速度的不同很重要。

 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
background_image_filename = 'sushiplate.jpg'
sprite_image_filename = 'fugu.png'

import pygame
from pygame.locals import *
from sys import exit

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)

# Our clock object
clock = pygame.time.Clock()

x1 = 0.
x2 = 0.
# Speed in pixels per second
speed = 250.

frame_no = 0

while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            exit()

    screen.blit(background, (0, 0))
    screen.blit(sprite, (x1, 50))
    screen.blit(sprite, (x2, 250))

    time_passed = clock.tick(30)
    time_passed_seconds = time_passed / 1000.0

    distance_moved = time_passed_seconds * speed
    x1 += distance_moved

    if frame_no % 5 == 0:
        distance_moved = time_passed_seconds * speed
        x2 += distance_moved * 5

    # If the image goes off the end of the screen, move it back
    if x1 > 640.:
        x1 -= 640.
    if x2 > 640.:
        x2 -= 640.

    pygame.display.update()
    frame_no += 1

斜线运动

直线运动很有用,但是一个游戏如果所有物体都水平或垂直移动看起来就很笨。我们需要能够在任何方向移动精灵。可以通过调整每个帧的x和y坐标做到。

 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
background_image_filename = 'sushiplate.jpg'
sprite_image_filename = 'fugu.png'

import pygame
from pygame.locals import *
from sys import exit

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()

x, y = 100., 100.
speed_x, speed_y = 133., 170.

while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            exit()

    screen.blit(background, (0, 0))
    screen.blit(sprite, (x, y))

    time_passed = clock.tick(30)
    time_passed_seconds = time_passed / 1000.0

    x += speed_x * time_passed_seconds
    y += speed_y * time_passed_seconds

    # If the sprite goes off the edge of the screen,
    # make it move in the opposite direction
    if x > 640 - sprite.get_width():
        speed_x = -speed_x
        x = 640 - sprite.get_width()
    elif x < 0:
        speed_x = -speed_x
        x = 0

    if y > 480 - sprite.get_height():
        speed_y = -speed_y
        y = 480 - sprite.get_height()
    elif y < 0:
        speed_y = -speed_y
        y = 0

    pygame.display.update()

为了达到反弹的效果,我们首先必须检查是否撞击到边缘。在坐标上做点简单的算术就行。如果x坐标小于0,则我们超过了屏幕的左边缘。如果x加上精灵的宽度比屏幕的宽度大,则精灵的右边缘超过屏幕的宽度。y坐标的判断类似,只是使用高度而不是宽度。

探索向量

游戏开发者从数学借来向量并用到很多领域,比如2D和3D游戏。向量和点类似,它们都有x和y值(在2D中),但是用途不一样。一个点的坐标(10, 20)在屏幕上总是同一个位置,而一个向量(10, 20)意思是从当前位置x坐标加上10,y坐标加上20。因此你可以认为一个点的坐标就是从原点(0, 0)到该点的向量。

创建向量

你可以从任意2个点计算向量,只要用第二个点坐标减去第一个点坐标。比如点A(10, 20),点B(30, 35),则向量AB就是(20, 15)。这个向量告诉我们从A到B需要在x方向移动20个单位,在y方向移动15个单位。

存储向量

在Python里面没有内置的向量类型,但是你可以将向量存入list,或者自己定义向量类。方便起见,我们选择定义自己的向量类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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])

@classmethod装饰使函数from_points变成一个类方法。类方法是通过类调用的,不是通过类实例调用,比如Vector2.from_points(P1, P2)。将from_points定义为一个类方法是因为它创建一个新的Vector2对象,而不是修改已经存在的对象。

向量大小

从A到B的向量大小就是2个点之间的距离。

1
2
def get_magnitude(self):
    return math.sqrt(self.x ** 2 + self.y ** 2)

单位向量

向量实际描述了两件事情:大小和方向。通常这两个信息绑定在一个向量里面,但有时候你只需其中一个。有一种特殊的向量叫单位向量,它的大小总是为1。我们可以把任意向量缩放到一个单位向量,这叫向量的规格化。

1
2
3
4
def normalize(self):
    magnitude = self.get_magnitude()
    self.x /= magnitude
    self.y /= magnitude

向量加法

向量加法是将两个向量组合产生一个向量,它有两个向量组合的效果。也就是AC=AB+BC。

1
2
def __add__(self, rhs):
    return Vector2(self.x + rhs.x, self.y + rhs.y)

向量减法

向量减法和加法类似。

1
2
def __sub__(self, rhs):
    return Vector2(self.x - rhs.x, self.y - rhs.y)

否定向量

如果想要改变向量的方向,使向量AB变成向量BA,需要将向量每个元素都改变符号。

1
2
def __neg__(self):
    return Vector2(-self.x, -self.y)

向量乘法和除法

将一个向量乘于或除于一个系数(数字),效果是改变向量的大小。如果向量乘于一个整数,则产生同一方向的向量,如果乘于一个负数,则产生相反方向的向量。

1
2
3
4
5
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秒,我们可以计算出每一秒我们到达的坐标。

1
2
3
4
5
6
7
8
A = (10.0, 20.0)
B = (30.0, 35.0)
AB = Vector2.from_points(A, B)
step = AB * .1
position = Vector2(A.x, A.y)
for n in range(10):
    position += step
    print position

当在两个点之间移动,计算中间位置是基本的。你还可以用向量计算在重力,外部作用力和摩擦力作用下很多现实的运动。

游戏对象向量类

作者已经写了一个二维向量类作为游戏对象的一部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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()

使用向量创建运动

既然我们了解了向量,我们可以使用它以多种方式移动游戏角色,而且可以实现简单的,基于力学的物理现象,使得游戏更加可信。

斜线运动

让我们使用向量创建更加精确的斜线运动。

 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
background_image_filename = 'sushiplate.jpg'
sprite_image_filename = 'fugu.png'

from sys import exit
import pygame
from pygame.locals import *
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)
speed = 250
heading = Vector2()

while True:
    for event in pygame.event.get():
        if event.type == QUIT:
            exit()
        if event.type == MOUSEBUTTONDOWN:
            destination = Vector2(*event.pos) - Vector2(*sprite.get_size()) / 2.
            heading = Vector2.from_points(position, destination)
            heading.normalize()

        screen.blit(background, (0, 0))
        screen.blit(sprite, position)

        time_passed = clock.tick()
        time_passed_seconds = time_passed / 1000.0

        distance_moved = time_passed_seconds * speed
        position += heading * distance_moved
        pygame.display.update()

目的坐标的计算需要解释一下。星号(*)用在函数的参数时,扩展为一个元组或列表。因此Vector2(*event.pos)相当于Vector2(event.pos[0], event.pos[1])。