pygame编程入门之八:Making Games With Pygame2
4. 游戏对象类
一旦您加载了模块,并编写了资源处理函数,您就需要继续编写一些游戏对象了。这样做的方式相当简单,尽管一开始看起来很复杂。你为游戏中的每一种对象编写一个类,然后为对象创建这些类的实例。然后,您可以使用这些类的方法来操作对象,给对象一些动作和交互功能。所以你的游戏在伪代码中,会是这样的:
#!/usr/bin/python
# [load modules here]
# [resource handling functions here]
class Ball:
# [ball functions (methods) here]
# [e.g. a function to calculate new position]
# [and a function to check if it hits the side]
def main:
# [initiate game environment here]
# [create new object as instance of ball class]
ball = Ball()
while 1:
# [check for user input]
# [call ball's update function]
ball.update()
当然,这是一个非常简单的例子,您需要输入所有的代码,而不是那些带括号的注释。但是你应该有基本想法。把一个类放在一个类中,你把所有的函数都放在一个球上,包括init,它会创造出所有的球的属性,然后更新,它会把球移动到它的新位置,然后在这个位置上移动blitting到屏幕上。
然后您可以为所有其他的游戏对象创建更多的类,然后创建它们的实例,这样您就可以在主函数和主程序循环中轻松地处理它们。与此形成对比的是,在主函数中启动球,然后有许多无类的函数来操作一个集合球对象,你将会看到为什么使用类是一个优势:它允许你把每个对象的所有代码放在一个地方;它使用对象更容易;它添加新对象和操作它们变得更加灵活。
您可以简单地为每个新球对象创建球类的新实例,而不是为每个新球对象添加更多的代码。魔法!
4.1. 一个简单的球类
这里有一个简单的类,它具有创建球对象所必需的功能,如果在主程序中调用update函数,那么就可以在屏幕上移动:
class Ball(pygame.sprite.Sprite):
"""A ball that will move across the screen
Returns: ball object
Functions: update, calcnewpos
Attributes: area, vector"""
def __init__(self, vector):
pygame.sprite.Sprite.__init__(self)
self.image, self.rect = load_png('ball.png')
screen = pygame.display.get_surface()
self.area = screen.get_rect()
self.vector = vector
def update(self):
newpos = self.calcnewpos(self.rect,self.vector)
self.rect = newpos
def calcnewpos(self,rect,vector):
(angle,z) = vector
(dx,dy) = (z*math.cos(angle),z*math.sin(angle))
return rect.move(dx,dy)
这里我们有球类,init球函数集,更新函数,改变了球的矩形在新的位置,和calcnewpos函数计算出球的新位置根据其当前位置,移动和向量。我马上就会解释物理。
另一件需要注意的事情是文档字符串,这段时间稍微长一点,并解释了类的基础知识。这些字符串不仅对您自己和其他程序员来说很方便,而且还可以用于解析代码并记录代码的工具。它们不会对程序产生很大的影响,但是对于大的程序来说它们是无价的,所以这是一个很好的习惯。
4.1.1. Diversion 1: Sprites
为每个对象创建类的另一个原因是精灵。你在游戏中渲染的每一个图像都是一个精灵对象,因此,首先,每个对象的类都应该继承精灵类。这是Python类继承的一个很好的特性。现在,球类拥有所有与Sprite类一起的功能,并且球类的任何对象实例都将被Pygame注册为精灵。而对于文本和背景,它们不移动,可以把对象放在背景上,Pygame以不同的方式处理精灵对象,当我们查看整个程序的代码时,你会看到它。
基本上,你为那个球创建一个球对象和一个精灵对象,然后你在sprite对象上调用球的更新函数,从而更新精灵。精灵还提供了复杂的方法来确定两个物体是否相撞。通常情况下,您可能只是在主循环中检查它们的矩形是否重叠,但这将涉及到大量的代码,这将是一种浪费,因为Sprite类提供了两个功能(spritecollide and groupcollide)来为您完成这项工作。
4.1.2. Diversion 2: Vector physics
除了球类的结构外,这段代码值得注意的是矢量物理,用来计算球的运动。任何涉及到角运动的游戏,除非你熟悉三角学,否则你不会走太远,所以我将介绍一些你需要知道的基础知识来理解calcnewpos函数。
首先,你会注意到球有一个属性向量,它是由角和z组成的,这个角是用弧度来表示的,它会告诉你球运动的方向。Z是球运动的速度。所以通过这个向量,我们可以确定球的方向和速度,以及它在x轴和y轴上的移动程度:
上面的图表说明了向量背后的基本数学。
在左手图中,你可以看到球的投影运动是由蓝线表示的。这条线的长度(z)表示它的速度,角度是它移动的方向。球运动的角度总是从右边的x轴上取下,从这条线顺时针方向测量,如图所示。
从球的角度和速度,我们可以算出它沿x轴和y轴移动了多少。因为Pygame不支持向量本身,我们只能通过沿着两个轴移动它的矩形来移动球。所以我们需要在x轴(dx)和y轴(dy)上解决这个角度和速度。这是一个简单的三角学问题,可以用图中所示的公式来完成。
如果你以前学过基本的三角学知识,这对你来说都不应该是新闻。但是,为了防止健忘,这里有一些有用的公式可以记住,这将帮助你对角度进行视觉化(用度来表示角度比弧度更直观)。
5. User-controllable objects
到目前为止,你可以创建一个Pygame窗口,并渲染一个可以在屏幕上运行的球。
下一步是制造一些用户可以控制的球拍。这可能比球简单得多,因为它不需要物理(除非你的用户控制的对象会以比上下更复杂的方式移动,比如像马里奥这样的平台角色,在这种情况下你需要更多的物理知识)。用户控制的对象很容易创建,这要归功于Pygame的事件队列系统,正如您将看到的。
5.1. 一个简单的球拍类
球拍类的原理与球类相似。你需要一个init函数来初始化这个球(这样你就可以为每只球拍创建一个对象实例),一个更新函数,在它被击到屏幕之前,在球棒上执行每帧的变化,以及定义这个类实际要做什么的功能。下面是一些示例代码:
class Bat(pygame.sprite.Sprite):
"""Movable tennis 'bat' with which one hits the ball
Returns: bat object
Functions: reinit, update, moveup, movedown
Attributes: which, speed"""
def __init__(self, side):
pygame.sprite.Sprite.__init__(self)
self.image, self.rect = load_png('bat.png')
screen = pygame.display.get_surface()
self.area = screen.get_rect()
self.side = side
self.speed = 10
self.state = "still"
self.reinit()
def reinit(self):
self.state = "still"
self.movepos = [0,0]
if self.side == "left":
self.rect.midleft = self.area.midleft
elif self.side == "right":
self.rect.midright = self.area.midright
def update(self):
newpos = self.rect.move(self.movepos)
if self.area.contains(newpos):
self.rect = newpos
pygame.event.pump()
def moveup(self):
self.movepos[1] = self.movepos[1] - (self.speed)
self.state = "moveup"
def movedown(self):
self.movepos[1] = self.movepos[1] + (self.speed)
self.state = "movedown"
正如你所看到的,这个类与它的结构中的球类非常相似。
但是每个函数的作用是不同的。首先,有一个reinit函数,它在回合结束时使用,而bat需要被设置回它的起始位置,任何属性都被设置回它们的必要值。
接下来,球拍移动的方式比球要复杂一些,因为它的运动很简单(向上/向下),但它依赖于使用者告诉它移动,不像球在每一帧中不断移动。为了理解球的运动方式,看一个快速的图来显示事件的顺序是很有帮助的:
这里发生的是控制球棒的人按下按钮,将球棒向上移动。主游戏循环的每个迭代(每一帧),关键是是否进行,球拍的状态属性对象被设置为“移动”,moveup函数将调用,导致球的y位置降低速度属性的值(在本例中,10)。换句话说,只要键盘被压住,球拍就会以每帧10个像素的速度向上移动屏幕。state属性还没有使用,但是在处理自旋还是想要一些有用的调试输出,也是很有用的。
一旦玩家过去,第二组框被调用,球拍的状态属性对象将回到“静止”状态,和movepos属性将回到(0,0),这意味着当更新函数被调用时,它不会把球拍移动。所以当玩家松开按键时,球拍就会停止移动。简单!
5.1.1. Diversion 3: Pygame events
那么我们怎么知道玩家什么时候把按键按下,然后释放呢
有了Pygame事件队列系统,年青人!这是一个非常容易使用和理解的系统,所以这不会花很长时间:)您已经在基本的Pygame程序中看到了事件队列,它用于检查用户是否退出了应用程序。移动球拍的代码就这么简单:
for event in pygame.event.get():
if event.type == QUIT:
return
elif event.type == KEYDOWN:
if event.key == K_UP:
player.moveup()
if event.key == K_DOWN:
player.movedown()
elif event.type == KEYUP:
if event.key == K_UP or event.key == K_DOWN:
player.movepos = [0,0]
player.state = "still"
这里假设您已经创建了一个bat的实例,并调用了object player。
您可以看到熟悉结构布局,它遍历Pygame事件队列中每个事件,并用event.get()函数检索。当用户点击按键,按下鼠标按钮并移动操纵杆时,这些动作会被注入到Pygame事件队列中,然后直到处理。
所以在主游戏循环的每次迭代中,你都要经历这些事件,检查它们是否是你想要处理的,然后适当地处理它们。在球拍身上的事件pump()函数。在每次迭代中调用update函数保持队列流。
首先,我们检查用户是否退出了程序,如果他们退出了,就退出。然后我们检查是否有任何键被按下,如果是,我们检查它们是否是移动球拍的指定键。如果是,然后调用对应移动功能,并设置适当的bat状态(尽管 moveup movedown改变了moveup()和movedown()函数,这使得简洁的代码,并且不破坏封装,这意味着您将属性分配给对象本身,没有引用该对象的实例的名称)。
注意这里我们有三个状态: still, moveup, and movedown。同样,如果您想要调试或计算旋转,这些都是很方便的。我们还会检查是否有任何键被“松开”(即不再被按住),如果是,我们就会阻止球拍移动。
6. 把它们放在一起
到目前为止,您已经学习了构建简单游戏所需的所有基础知识。您应该了解如何创建Pygame对象,Pygame如何显示对象,如何处理事件,以及如何使用物理将一些动作引入到您的游戏中。
现在,我将展示如何将所有这些代码块放到游戏中。首先要做的是让球触到屏幕的两侧,让球棒能够击球,否则就不会有太多的比赛了。我们用Pygame的碰撞方法来做这个。
6.1. 让球击中两边
让它在两侧弹跳的基本原理很容易理解。你利用球的四个角坐标,检查它们是否与屏幕边缘的x或y坐标相对应。如果右上角和左上角都有y坐标为0,你就知道这个球现在在屏幕的最上面。在我们计算出了球的新位置之后,我们在更新函数中做了所有这些。
if not self.area.contains(newpos):
tl = not self.area.collidepoint(newpos.topleft)
tr = not self.area.collidepoint(newpos.topright)
bl = not self.area.collidepoint(newpos.bottomleft)
br = not self.area.collidepoint(newpos.bottomright)
if tr and tl or (br and bl):
angle = -angle
if tl and bl:
self.offcourt(player=2)
if tr and br:
self.offcourt(player=1)
self.vector = (angle,z)
检查这个区域是否包含了球的新位置(它总是应该的,我们不需要有else子句,尽管在其他情况下你可能想要考虑它。)
然后检查四个角的坐标是否与该区域的边发生碰撞,并为每个结果创建对象。如果是的话,对象的值是1,或者是真值。如果不,那么价值将是零,或者是假的。
然后我们看它是否击中了顶部或底部,如果是,它改变了球的方向。使用弧度,我们可以简单地改变它的正/负的值来做到这一点。还会检查球是否从侧面消失了,如果它有的话,我们会调用offcourt函数。在游戏中,重新设置球,在调用该函数时指定的玩家的分数增加1点,并显示新分数。
最后,根据新的角度重新编译向量。就这样。球将欢快地从墙上弹回来,并以优雅的姿态离开墙面。
6.2. 让球碰到球拍
把球打到球拍身上很类似,它会撞到屏幕的两侧。仍然使用碰撞法,但是这次要检查球的矩形和球拍是否碰撞。在这段代码中,还添加了一些额外的代码来避免各种故障。您会发现,为了避免出现小故障和bug,您必须添加各种额外的代码,因此习惯了它是一件好事。
else:
# Deflate the rectangles so you can't catch a ball behind the bat
player1.rect.inflate(-3, -3)
player2.rect.inflate(-3, -3)
# Do ball and bat collide?
# Note I put in an odd rule that sets self.hit to 1 when they collide, and unsets it in the next
# iteration. this is to stop odd ball behaviour where it finds a collision *inside* the
# bat, the ball reverses, and is still inside the bat, so bounces around inside.
# This way, the ball can always escape and bounce away cleanly
if self.rect.colliderect(player1.rect) == 1 and not self.hit:
angle = math.pi - angle
self.hit = not self.hit
elif self.rect.colliderect(player2.rect) == 1 and not self.hit:
angle = math.pi - angle
self.hit = not self.hit
elif self.hit:
self.hit = not self.hit
self.vector = (angle,z)
用另一段语句开始这部分,因为这是前面的代码块中执行的,以检查球是否碰到了边。
如果它没有击中两边,它可能会击中一个球棒,所以继续进行条件。第一个故障修复是在这两个维度缩小球员矩形3像素,停止背后的球拍抓球(如果你想象你只是把球拍这球跟踪,矩形重叠,所以通常球将被“打击”)。
接下来检查这些矩形是否会发生碰撞,还有一个小故障。请注意,我已经对这些奇怪的代码进行了注释——对于那些查看代码的人来说,解释一些不寻常的代码总是好的,因此当看到它的时候,您就会理解它。如果没有修复,球可能会击中球棒的一角,改变方向,一帧后仍然会发现自己在球拍内。然后它会认为它再次被击中了,并改变了它的方向。这种情况可能会发生几次,使得球的运动完全不真实。
所以我们有一个变量,self.click,当它被击中时,我们将它设置为True,然后在后面加上一个False。当我们检查这些矩形是否发生碰撞时,我们也检查是否self命中是true/false,以阻止内部的反弹。
这里的代码很容易理解。所有矩形都有一个碰撞函数,你可以在其中输入另一个物体的矩形,如果这些矩形是重叠的,如果不是,它就会返回True。我们可以通过从pi中减去当前的角度来改变方向(同样,你可以用弧度来做一个简单转变,它会把角度调整90度,然后把它往正确的方向发送;你可能会发现,在这一点上,对弧度的彻底理解是有道理的!)为了完成故障检查,我们换了self.hit。如果们被击中后的框架,那就返回False。
然后重新编译这个向量。当然,您希望删除前一段代码中的同一行,这样您只需要在if-else条件语句之后才做一次。这是它!合并后的代码将允许球击中两侧和球拍。
6.3. 成品
最终的产品,加上所有的代码块,以及其他一些代码将它们整合在一起,看起来就像这样:
#
# Tom's Pong
# A simple pong game with realistic physics and AI
# http://www.tomchance.uklinux.net/projects/pong.shtml
#
# Released under the GNU General Public License
VERSION = "0.4"
try:
import sys
import random
import math
import os
import getopt
import pygame
from socket import *
from pygame.locals import *
except ImportError, err:
print "couldn't load module. %s" % (err)
sys.exit(2)
def load_png(name):
""" Load image and return image object"""
fullname = os.path.join('data', name)
try:
image = pygame.image.load(fullname)
if image.get_alpha is None:
image = image.convert()
else:
image = image.convert_alpha()
except pygame.error, message:
print 'Cannot load image:', fullname
raise SystemExit, message
return image, image.get_rect()
class Ball(pygame.sprite.Sprite):
"""A ball that will move across the screen
Returns: ball object
Functions: update, calcnewpos
Attributes: area, vector"""
def __init__(self, (xy), vector):
pygame.sprite.Sprite.__init__(self)
self.image, self.rect = load_png('ball.png')
screen = pygame.display.get_surface()
self.area = screen.get_rect()
self.vector = vector
self.hit = 0
def update(self):
newpos = self.calcnewpos(self.rect,self.vector)
self.rect = newpos
(angle,z) = self.vector
if not self.area.contains(newpos):
tl = not self.area.collidepoint(newpos.topleft)
tr = not self.area.collidepoint(newpos.topright)
bl = not self.area.collidepoint(newpos.bottomleft)
br = not self.area.collidepoint(newpos.bottomright)
if tr and tl or (br and bl):
angle = -angle
if tl and bl:
#self.offcourt()
angle = math.pi - angle
if tr and br:
angle = math.pi - angle
#self.offcourt()
else:
# Deflate the rectangles so you can't catch a ball behind the bat
player1.rect.inflate(-3, -3)
player2.rect.inflate(-3, -3)
# Do ball and bat collide?
# Note I put in an odd rule that sets self.hit to 1 when they collide, and unsets it in the next
# iteration. this is to stop odd ball behaviour where it finds a collision *inside* the
# bat, the ball reverses, and is still inside the bat, so bounces around inside.
# This way, the ball can always escape and bounce away cleanly
if self.rect.colliderect(player1.rect) == 1 and not self.hit:
angle = math.pi - angle
self.hit = not self.hit
elif self.rect.colliderect(player2.rect) == 1 and not self.hit:
angle = math.pi - angle
self.hit = not self.hit
elif self.hit:
self.hit = not self.hit
self.vector = (angle,z)
def calcnewpos(self,rect,vector):
(angle,z) = vector
(dx,dy) = (z*math.cos(angle),z*math.sin(angle))
return rect.move(dx,dy)
class Bat(pygame.sprite.Sprite):
"""Movable tennis 'bat' with which one hits the ball
Returns: bat object
Functions: reinit, update, moveup, movedown
Attributes: which, speed"""
def __init__(self, side):
pygame.sprite.Sprite.__init__(self)
self.image, self.rect = load_png('bat.png')
screen = pygame.display.get_surface()
self.area = screen.get_rect()
self.side = side
self.speed = 10
self.state = "still"
self.reinit()
def reinit(self):
self.state = "still"
self.movepos = [0,0]
if self.side == "left":
self.rect.midleft = self.area.midleft
elif self.side == "right":
self.rect.midright = self.area.midright
def update(self):
newpos = self.rect.move(self.movepos)
if self.area.contains(newpos):
self.rect = newpos
pygame.event.pump()
def moveup(self):
self.movepos[1] = self.movepos[1] - (self.speed)
self.state = "moveup"
def movedown(self):
self.movepos[1] = self.movepos[1] + (self.speed)
self.state = "movedown"
def main():
# Initialise screen
pygame.init()
screen = pygame.display.set_mode((640, 480))
pygame.display.set_caption('Basic Pong')
# Fill background
background = pygame.Surface(screen.get_size())
background = background.convert()
background.fill((0, 0, 0))
# Initialise players
global player1
global player2
player1 = Bat("left")
player2 = Bat("right")
# Initialise ball
speed = 13
rand = ((0.1 * (random.randint(5,8))))
ball = Ball((0,0),(0.47,speed))
# Initialise sprites
playersprites = pygame.sprite.RenderPlain((player1, player2))
ballsprite = pygame.sprite.RenderPlain(ball)
# Blit everything to the screen
screen.blit(background, (0, 0))
pygame.display.flip()
# Initialise clock
clock = pygame.time.Clock()
# Event loop
while 1:
# Make sure game doesn't run at more than 60 frames per second
clock.tick(60)
for event in pygame.event.get():
if event.type == QUIT:
return
elif event.type == KEYDOWN:
if event.key == K_a:
player1.moveup()
if event.key == K_z:
player1.movedown()
if event.key == K_UP:
player2.moveup()
if event.key == K_DOWN:
player2.movedown()
elif event.type == KEYUP:
if event.key == K_a or event.key == K_z:
player1.movepos = [0,0]
player1.state = "still"
if event.key == K_UP or event.key == K_DOWN:
player2.movepos = [0,0]
player2.state = "still"
screen.blit(background, ball.rect, ball.rect)
screen.blit(background, player1.rect, player1.rect)
screen.blit(background, player2.rect, player2.rect)
ballsprite.update()
playersprites.update()
ballsprite.draw(screen)
playersprites.draw(screen)
pygame.display.flip()
if __name__ == '__main__': main()
除了展示最终产品,我还会把你们带回到TomPong上,所有这些都是基于此的。
下载,看看源代码,你会看到一个全面实施pong使用的所有代码。在本教程中,您看到的以及很多其他的代码我已经添加各种版本,比如一些额外的物理旋转,和其他各种错误和故障修复。