用Python和Pygame写游戏-从入门到精通(8)

By | 2011/06/16

是时候让我们的游戏活泼起来了。电脑游戏和桌面游戏的一个巨大差别,想来就是这个“动”。伟大的哲学家们告诉我们,“运动是绝对的,静止时相对的”,同样的在游戏中,只有活动起来,游戏才会拥有生命,否则和看连环画有什么差别呢?

这几章讲述的东西需要一些线性代数的知识,好吧有些夸张,如果你不明白,完全没关系,高中物理的知识就绝对足够了(或者说嫌多了)!

现实生活中的物体,运动起来总是按照某种规律的(去问问牛顿就知道了),而游戏中,有些动作就可以非常的不靠谱,比如吃豆人,大嘴巴永远以恒定的速度前进,可以瞬间转身或停止,要知道,这可是逆天的行为……现在的游戏中,制作者总是尽量的把运动做的和现实贴近(尤其是赛车游戏等),一辆车的运动,可能是上百种力同时作用的结果。不过幸好,我们只要知道一些基础的东西,很多运动和力的计算,都有现成的代码供我们使用。

理解帧率

这是一个被说烂了的词,FPS(Frame Per Second)是游戏和硬件间较量的永恒话题,我也不想多插话了,相信玩游戏的朋友都知道。

只是记住几个常用的量:一般的电视画面是24FPS;30FPS基本可以给玩家提供流程的体验了;LCD的话,60FPS是常用的刷新率,所以你的游戏的帧率再高也就没什么意义了;而绝大多数地球人都无法分辨70FPS以上的画面了!

直线运动

我们先来看一下初中一开始就学习的直线运动,我们让一开始的程序中出现的那条鱼自己动起来~

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)

# sprite的起始x坐标
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 x > 640.:
        x = 0.    

    pygame.display.update()

我想你应该需要调节一下“x += 10.”来让这条鱼游的自然一点,不过,这个动画的帧率是多少的?在这个情形下,动画很简单,所以应该会很快;而有些时候动画元素很多,速度就会慢下来。这可不是我们想看到的!

关于时间

有一个解决上述问题的方法,就是让我们的动画基于时间运作,我们需要知道上一个画面到现在经过了多少时间,然后我们才能决定是否开始绘制下一幅。pygame.time模块给我们提供了一个Clock的对象,使我们可以轻易做到这一些:

clock = pygame.time.Clock()
time_passed = clock.tick()
time_passed = clock.tick(30)

第一行初始化了一个Clock对象;第二行的意识是返回一个上次调用的时间(以毫秒计);第三行非常有用,在每一个循环中加上它,那么给tick方法加上的参数就成为了游戏绘制的最大帧率,这样的话,游戏就不会用掉你所有的CPU资源了!但是这仅仅是“最大帧率”,并不能代表用户看到的就是这个数字,有些时候机器性能不足,或者动画太复杂,实际的帧率达不到这个值,我们需要一种更有效的手段来控制我们的动画效果。

为了使得在不同机器上有着一致的效果,我们其实是需要给定物体(我们把这个物体叫做精灵,Sprite)恒定的速度。这样的话,从起点到终点的时间点是一样的,最终的效果也就相同了,所差别的,只是流畅度。看下面的图试着理解一下~

使用时间控制来完成一致的动画效果

我们把上面的结论实际试用一下,假设让我们的小鱼儿每秒游动250像素,这样游动一个屏幕差不多需要2.56秒。我们就需要知道,从上一帧开始到现在,小鱼应该游动了多少像素,这个算法很简单,速度*时间就行了,也就是250 * time_passed_second。不过我们刚刚得到的time_passed是毫秒,不要忘了除以1000.0,当然我们也能假设小鱼每毫秒游动0.25像素,这样就可以直接乘了,不过这样的速度单位有些怪怪的……

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)

# Clock对象
clock = pygame.time.Clock()

x = 0.
# 速度(像素/秒)
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

    # 想一下,这里减去640和直接归零有何不同?
    if x > 640.:
        x -= 640.    

    pygame.display.update()

好了,这样不管你的机器是更深的蓝还是打开个记事本都要吼半天的淘汰机,人眼看起来,不同屏幕上的鱼的速度都是一致的了。请牢牢记住这个方法,在很多情况下,通过时间控制要比直接调节帧率好用的多。

斜线运动

下面有一个更有趣一些的程序,不再是单纯的直线运动,而是有点像屏保一样,碰到了壁会反弹。不过也并没有新的东西在里面,原理上来说,反弹只不过是把速度取反了而已~ 可以先试着自己写一个,然后与这个对照一下。

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

OK,这次的运动就说到这里。仔细一看的话,就会明白游戏中的所谓运动(尤其是2D游戏),不过是把一个物体的坐标改一下而已。不过总是不停的计算和修改x和y,有些麻烦不是么,下次我们引入向量,看看使用数学怎样可以帮我们减轻负担。

>> 用Python和Pygame写游戏-从入门到精通(9)

 

39 thoughts on “用Python和Pygame写游戏-从入门到精通(8)

  1. Lucy

    为什么在我的机器上 把time_passed_seconds = time_passed/1000.0写为time_passed_seconds = time_passed/1000 小鱼就不会移动呢?

    Reply
    1. xishui Post author

      看来Lucy童鞋对Python还没有很熟悉,念在你名字很可爱我详细的说明一下……
      在Python2.X中,500/1000=0,而500/1000.0=0.5,整数相除,结果还是整数,而且是小于等于实际结果的整数,这是从C语言等一些传统语言继承来的。
      而在Python3.X中,除法已经是自然除法,因为很多人初学编程就是Python,对这样的结果很不理解,所以修改了。在3.X中要想使结果是整数,得使用地板除“//”。

      Reply
  2. 走流沙

    x = 640 – sprite.get_width()
    y = 480 – sprite.get_height()

    上面这两句去掉也能运行,那么去掉可不可以,有没有什么看不见的影响

    Reply
    1. xishui Post author

      追根究底是好习惯^_^
      可能看起来确实不明显,这两句的意义就是,让精灵迅速回到屏幕中,看起来每一边都不会出界。当然即使不执行,速度反向基本也能保证在一帧时间内回来了,这么写只是“尽可能的好”。

      Reply
    2. partical

      这个不能去掉的,去掉会出bug, x > 640 – sprite.get_width()之后会反向,下一帧之后x有可能还是大于640-sprite.get_width() 当然理论不会这样, 但是运行起来发生这种情况的,小鱼靠边移动了(一个方向速度似乎为0了)。

      Reply
      1. Jeanne

        Guys private message me i will show you how to earn between 60-150 dollars per month i got paid 450 dollars in 4 months depend on your time get paid via paypal or spend on website el,cortnics,dvdegift cards etc..you might earn on some sites 1000s but you will never get paid the one im using is very legit website and i will tell u some of my tricks no scam i can prove my payments if u dont believe me just pm me and and i will help you out u will be glad u joined the site (website is free)

        Reply
  3. aiqier

    减去640.0和直接归零有什么不同?能不能解释一下

    Reply
    1. xishui Post author

      说实话差别不大,你把帧率调低仔细看看,也许能看出一些端倪来~

      Reply
      1. nanjingyueliang

        把速度调整为25,也没发现有啥区别啊?
        x>640,执行640-640=0 ,和x直接等于0,在逻辑上,好像没有区别啊,求指点!

        Reply
        1. boss

          区别在于,一个是瞬间回到左边界吧,另外一个好像是整个鱼身进入右边界,然后从左边界出来?

          Reply
  4. mmmmmjjjuuu

    为什么在这句中
    sprite = pygame.image.load(sprite_image_filename)
    中没有.convert()呢?

    Reply
    1. xishui Post author

      会自动转的呀…… 好像前面说过了,孩纸,看书要仔细:)

      Reply
      1. 114514

        加了的话可以加速,官网上是这么说的(https://www.pygame.org/docs/ref/surface.html#pygame.Surface.convert)~

        Reply
    1. Francis7999

      如果把速度调块到原来的5倍就会非常清楚了,-640的话鱼起点x坐标不是每次都为0,起点经常是到了屏幕中间,因为刷新频率不够

      Reply
  5. xumaomao

    @erhuabushuo: 0的话就当然是设为0,-640的话就有可能是0,或者0.4233437,或者1.23187978什么的,取决于速度和刷新频率,因为有可能x从638或者639或者639.9999之类的稍微小640一点的数值跳过640到640.353098什么的,然后触发归位的动作。
    另外希望博主继续更新更多教程。

    Reply
    1. nanjingyueliang

      应该是这样的。目测正解…..

      Reply
    2. Francis7999

      如果把速度调块到原来的5倍就会非常清楚了,-640的话鱼起点x坐标不是每次都为0,起点经常是到了屏幕中间,因为刷新频率不够

      Reply
  6. 姐夫别这样

    感觉这样学习python好有意思 有动力

    Reply
  7. halcy0n

    引用原文:”第一行初始化了一个Clock对象;第二行的意识是返回一个上次调用的时间(以毫秒计);第三行非常有用”

    其中”第二行中的意识” 可能是 ‘第二行中的意思’ 的意思吧,不知道是不是

    Reply
  8. test1

    有一个疑问, tick() 不加参数是返回从上次调用经过的时间,pygame.org 上是这样写的This method should be called once per frame. It will compute how many milliseconds have passed since the previous call.
    既然是返回两帧间的间隔,那么在每个while中的blit() 不同的机器肯定会有差距,为什么还会
    “””不管你的机器是更深的蓝还是打开个记事本都要吼半天的淘汰机,人眼看起来,不同屏幕上的鱼的速度都是一致的了”””
    有些不明白,请博主指点下,

    Reply
    1. xishui Post author

      嗯,怎么说呢……我花力气画那个星星图,就是为了说明这个事情的……

      Reply
  9. Pingback: 用Python和Pygame写游戏-从入门到精通(8)-演道网

    1. jay

      好吧,当我没问。我想想还是能实现的

      Reply
  10. ouyang

    鱼直线运动时,设置了一个参数i和时间参数t, 记录鱼从左边移动到右边的过程中,while需循环的次数。在我的电脑上,第一次运行代码while循环了1140次,第二次运行循环了1181次。画布update的次数不一样,但两次运行所花时间都是0:00:02.93。
    理解了用时间控制来控制的意义了。一段距离,不管你跳多少次,反正得在规定的时间内跳到目的地。一次跳不远,那就多跳几次,跳的时间缩短。或者每次跳前花点时间准备,然后一次跳远点。具体怎么跳我们无法控制,但在规定时间内到达是可控的。

    Reply
  11. Pingback: 用Python和Pygame写游戏-从入门到精通(1) – 易可信blog系统

  12. Pingback: 用 Python 和 Pygame 写游戏 – 从入门到精通(目录) – ITPCB

  13. Pingback: pygame学习_Johngo学长

发表评论

您的电子邮箱地址不会被公开。