跳转至

第五章:打砖块游戏编程

在第四章我们学习了如何以面向对象的方法来编写贪吃蛇游戏。在本章,我们将学习编写另一个经典游戏,打砖块。这个游戏的整体结构和贪吃蛇区别不大,仍然会定义多个游戏元素的类。只不过这个游戏会涉及到一些三角函数的计算,以及鼠标控制。此外,我们还会在此基础上编写一个双人对战游戏。

一、打砖块游戏介绍

游戏规则

在打砖块游戏中,存在三种游戏元素,分别是玩家控制的球板,飞行的球,以及一组静止的砖块。游戏规则规定球可以自由飞行,如果碰撞到了场景中左侧、上侧和右侧的边界,球会反弹,改变其方向。如果飞出了下侧边界,则游戏会失败。玩家控制的球板,在场景下侧边界附近,可以控制其左右移动,以挡住球的行进路线。场景中的上方还会有一组砖块,如果球击中砖块,则会增加一分。游戏的目标就是用球板来挡击球,让球击中更多的砖块得分,避免失败。

游戏开始时,球会随机出现在窗口中部的位置,以静止状态,等待玩家的指令。玩家通过鼠标按键让球开始运行。如果玩家控制的球板未挡住球,则游戏重新开始。如果所有砖块被打掉或玩家关闭窗口,则游戏结束。

本章编写的打砖块游戏,还要求使用球板来控制球的反弹方向。具体来讲,就是当球和球板中间部分接触时,球会以较垂直的方向反弹回去。如果球和球板的偏边缘部分接触时,球会更向两则边界处反弹。此外,如果球落在左半部分球板时,球会偏左反弹,落在右半部分球板时,球会偏右反弹。这种碰撞机制赋予玩家更大的控制能力,可以更好的控制球反弹的方向,以准确击中目标砖块。

游戏画面如下所示。

ch05-01

游戏资源

游戏资源包括三种,放在了配套代码的相应目录中。资源对应了游戏中的三种游戏元素。分别是球板、球和砖块的图片资源。大家下载后,可以打开这些图片观察其图片像素大小。

二、游戏功能和程序设计

游戏功能

本游戏要实现的主要功能包括:

  • 处理用户输入,根据用户的鼠标点击控制球开始运动,根据鼠标位置位置来移动球板。
  • 控制每帧下球的移动,并判断球和边界的碰撞情况。
  • 判断球有没有越过下侧边界,如果发生则游戏失败。
  • 判断球和球板有没有发生碰撞。计算碰撞角度,将球进行反弹。为了增加难度,每次碰撞后增加球的速度。
  • 判断球和砖块有没有发生碰撞。碰撞后将球进行反弹,并计分。
  • 在游戏场景上方显示分数。

程序设计

游戏场景中需要处理三种角色,即球板、球和砖块。所以我们会设计三个类来封装三种角色对应的数据和函数。然后用一个游戏类来组装他们。设计的类图简化显示如下。

classDiagram

    Game *-- Ball : Composition
    Game *-- Bat :Composition
    Game *-- Brick : Composition

    class Game {
    ball
    bat
    brick
    ...
    }

    class Ball {
    }

    class Bat {
    }

    class Brick {
    }

Ball类中,包括球的图片对象,控制球移动的方法,绘图方法,以及游戏重启后的重置函数。

Bat类中,包括了球板的图片对象,用户交互函数,以及绘图方法。

Brick类中,包括了砖块的图片对象,和绘图函数。

Game类中,定义了场景初始化,以及定义窗口,初始化上述三种对象。定义分值、时钟等属性。在方法中需要定义碰撞检测、绘图和游戏主循环函数。

三、打砖块游戏代码实现

Bat类

首先定义球板Bat类。在这个类的初始化函数中加载了图片资源,读取图片对应的边框矩形rect。这类边框矩形对象就是一个包括了四个元素的元组,分别是图片左上角坐标和图片尺寸。然后定义mousey变量,这个值后面会用于设置球板在窗口中的纵坐标位置。draw函数是显而易见的。

class Bat:
    def __init__(self,playerY=540):
        self.image = pygame.image.load('bat.png')
        self.rect = self.image.get_rect()
        self.mousey = playerY

    def draw(self, surface):
        surface.blit(self.image, self.rect)

update函数是用于接收输入以更新球板数据的,使用mouse.get_pos函数来获取鼠标坐标,返回坐标包含两个值,不过我们只需要保留第一个值即可,也就是x坐标。因为我们只需要左右横向移动。对mousex需要加条件判断,是为了让球板不要移到窗口外面去了。最后我们来设置边框矩形rect的位置,这里是让rect左上角的坐标等于鼠标的x坐标。当然其y坐标一直是一个常量。

    def update(self,win_width):
        mousex, _ = pygame.mouse.get_pos()
        if (mousex > win_width - self.rect.width):
            mousex = win_width - self.rect.width
        self.rect.topleft = (mousex, self.mousey)

Ball类

下面来定义球的类Ball,初始化中依然是加载图片和获取边框矩形。reset函数则是让每次游戏重新开始时,重新定义球的位置。

在reset函数中,served表示是否要让球运动起来,如果还没有发球,球还处于静止状态,则served为逻辑假,如果球处于运动中,则是已经发球了,served为逻辑真。也就意味着游戏真正开始了。

球的初始位置坐标通过positionX和positionY确定,positionX坐标是一个随机值,而positionY坐标是一个常量,之所以它不设置为随机值是为了不让球混入砖块。然后让球的边框矩形的左上角和这两个坐标对齐。这样就让球的出生点随机的位于场景中某个合适地方了。

有一点需要注意,就是球的移动方向和速度。每次球和球板碰撞时,会以不同的角度进行反弹。不同的角度意味着在横轴x和纵轴y两个方向下的速度是不一样的。有时候x方向的速度大,y方向的速度小,有时会反过来。但球整体的移动速度是需要保持不变的。这里需要基于三角函数对速度进行分解和控制。我们设置球一开始是向右下角方向运动,所以x方向和y方向的分量是一样的,所以我们使用45度,以及两个三角函数定义了速度分量speedX和speedY。

class Ball:

    def __init__(self,win_width):
        self.image = pygame.image.load('ball.png')
        self.rect = self.image.get_rect()
        self.reset(win_width)

    def reset(self,win_width,startY=220,speed=5, degree=45):
        self.served = False
        self.positionX = random.randint(0,win_width)
        self.positionY = startY
        self.rect.topleft = (self.positionX, self.positionY)
        self.speed = speed
        self.speedX = self.speed * sin(radians(degree))
        self.speedY = self.speed * cos(radians(degree))

    def draw(self, surface):
        surface.blit(self.image, self.rect)

Ball类的移动函数是update,只有当served为真,也就是球开始运行后,才会运行后续代码,前面的几行代码是常规的位置移动,将速度分量增加到坐标上,并重新赋值边框矩形的左上角坐标,以使球移动起来。后面的三组代码分别是判断球和上边界,左边界,右边界碰撞后的情况。

    def update(self,win_width):
        if self.served:    
            self.positionX += self.speedX
            self.positionY += self.speedY
            self.rect.topleft = (self.positionX, self.positionY)

            if (self.positionY <= 0):
                self.positionY = 0
                self.speedY *= -1

            if (self.positionX <= 0):
                self.positionX = 0
                self.speedX *= -1

            if (self.positionX >=win_width - self.rect.width):
                self.positionX = win_width - self.rect.width
                self.speedX *= -1

Bricks类

砖块类Bricks是最简单的,其核心属性contains是一个list,用双重循环向其中写入元素,它保存的是若干个Rect对象,也就是边框矩形,每个Rect中保存着一个砖块的坐标地址和长宽尺寸这四个数字。

此处的数字需要说明一下,因为每个砖块图片的宽高数据是31个像素宽,16个像素高,整个砖块集合是5行12列组成,那么12*31会占用372个像素,将窗口宽度800减去372,再除以2,是到两边应该空出的空间宽度,这就是214,横向砖块是紧密排列在一起的,纵向看每行砖块间可以保留一些空间,所以这里用24作为坐标值,是为了留出24-16,即8个像素单位的间隔。

class Bricks:

    def __init__(self,row=5, col=12):
        self.image = pygame.image.load('brick.png')
        self.rect = self.image.get_rect()
        self.contains = []
        for y in range(row):
            brickY = (y * 24) + 100
            for x in range(col):
                brickX = (x * 31) + 214
                rect = Rect(brickX, brickY, self.rect.width, self.rect.height)
                self.contains.append(rect)

    def draw(self, surface):
        for rect in self.contains:
            surface.blit(self.image, rect)

Game类

然后我们来定义最重要的Game类,和之前类似,初始化方法中定义了窗口参数和一个绘图窗口对象surface。将三种游戏角色类进行了初始化。这里用SysFont来定义操作系统内的字体。如果不清楚自己系统内字体,可以使用get_fonts函数来查看。此外就是一些常见的变量,和之前贪吃蛇游戏是类似的。

class Game:
    WHITE = (255, 255, 255)
    BLACK = (0, 0, 0)

    def __init__(self, Width=800,Height=600):
        pygame.init()
        self.Win_width , self.Win_height = (Width, Height)
        self.surface = pygame.display.set_mode((self.Win_width, self.Win_height))
        self.bat = Bat()
        self.ball = Ball(self.Win_width) 
        self.bricks = Bricks() 
        self.font = pygame.font.SysFont('microsoftyahei',26)
        self.score = 0
        self.Clock = pygame.time.Clock()
        self.fps = 60
        self.running = True
        pygame.display.set_caption('Bricks')

碰撞检测

有两个碰撞检测函数需要编写,一个是球和球板之间的碰撞。bat_collision函数实现这此功能,这里使用了Rect的特有方法colliderect来实现碰撞检测,它可以在两个Rect对象间进行判断。只要球的边框矩形和球板的边框矩形产生重叠,就会满足条件。当条件满足时,首先会将球的底部坐标设置在球板上部位置,以准备向上反弹。这种处理是为了避免二者发生侧面碰撞的罕见情况。每次碰撞后,为了增加游戏难度,将球的速度增加一点点。

随后代码的计算逻辑是为了计算反弹角度和方向,较为复杂。首先是计算球的中心位置和球板的中心位置在x轴方向的距离,如果diff_x大于0,则球和球板撞击在右半部分,反之则是在左半部分。然后将diff_x的绝对值除以球板的一半数值,用于计算一个比例,看是更靠近球板的中间,还是靠近球板的边缘。我们将这个比例和0.95之间取最小值。这是因为在某些极端情况下,二者发生侧面碰撞,那么比例值会算出大于1的情况。此时我们用0.95来代替这种极端值。然后将比例值diff_ratio用asin函数转换成一个角度值theta,asin函数就是反正弦函数。角度值theta用于后续对速度分量的分解。

当球和球板中间部分接触时,diff_x会比较小,diff_ratio也会比较小,然后theta计算出的角度为比较小,自然sin(theta)也会比较小,得到的x方向的速度分量speedX也会较小,那么speedY分量会分配到更多的速度,所以球的回弹角度会比较垂直。反之,如果球和球板的边缘部分接触时,diff_x会比较大,x方向的速度分量speedX也会较大。球会更向两边反弹。

当speedX和speedY速度分量都计算完毕后,将speedY反转方向,让球反弹向上。最后的条件判断是为了控制反弹方向,如果球落在左半部分球板时,球会偏左反弹,落在右半部分球板时,球会偏右反弹。当speedX大于0时,球向右运动,如果落在球板左侧,则将speedX进行反转,以反转方向。当球向左运动,而落在球板右侧时,也会反转方向。

    def bat_collision(self):
        if self.ball.rect.colliderect(self.bat.rect):
            self.ball.rect.bottom = self.bat.mousey
            self.ball.speed += 0.1
            # 控制反弹角度
            diff_x = self.ball.rect.centerx - self.bat.rect.centerx
            diff_ratio = min(0.95,abs(diff_x)/(0.5*self.bat.rect.width))
            theta = asin(diff_ratio)
            self.ball.speedX = self.ball.speed * sin(theta)
            self.ball.speedY = self.ball.speed * cos(theta)
            self.ball.speedY *= -1
            # 控制反弹方向
            if (diff_x < 0 and self.ball.speedX > 0) or (diff_x > 0 and self.ball.speedX < 0):
                self.ball.speedX *= -1

bricks_collision函数用来检测球和砖块之间的碰撞,使用collidelist函数用来判断一个Rect对象和一组Rect对象之间的碰撞,返回的是检测结果存放在brickHitIndex中,如果没有发生碰撞,返回值会是-1,如果有碰撞会返回砖块list中的编号索引。当碰撞发生时,我们用索引将这个被撞到的砖块取出来。再分析球和砖块的碰撞是纵向碰撞还是横向碰撞。如果碰撞时,球的中心坐标小于砖块左侧位置或大于砖块右侧位置,说明球撞击了砖块的左边或右边的侧面。那么将球的速度分量speedX进行反转。另一种情况自然就是球撞击了砖块的上边或下边。就将球的speedY速度分量进行反转。

然后将被撞击的砖块从contains中删除,分值增加1分,如果contains的长度为0,意味着所有的砖块都被打破了,那么结束游戏。

    def bricks_collision(self):
        brickHitIndex = self.ball.rect.collidelist(self.bricks.contains)
        if brickHitIndex >= 0:
            brick = self.bricks.contains[brickHitIndex]
            if (self.ball.rect.centerx > brick.right or 
                self.ball.rect.centerx < brick.left):
                self.ball.speedX *= -1
            else:
                self.ball.speedY *= -1
            del (self.bricks.contains[brickHitIndex])
            self.score +=1
            if len(self.bricks.contains)==0:
                self.running = False 

另一个和碰撞有关的函数是check_failed,判断球的底部是否大于窗口高度,条件满足意味着球将飞出窗口下界,游戏失败,此时我们重置球的状态,并将分值设为0,游戏将准备重新开始。

    def check_failed(self):
        if self.ball.rect.bottom >= self.Win_height:
            self.ball.reset(self.Win_width)
            self.score = 0

绘图输出

绘图输出函数比较容易,这里的方法和之前贪吃蛇例子类似。先定义draw_data来输出当前得分,再将所有对象中的绘图函数组合在一起,用于更新所有窗口显示。

    def draw_data(self):
        score_text = "得分:{score}".format(score=self.score)
        score_img = self.font.render(score_text, 1, Game.WHITE)
        score_rect = score_img.get_rect(centerx=self.Win_width//2, top=5)
        self.surface.blit(score_img, score_rect)

    def draw(self):
        self.surface.fill(Game.BLACK)
        self.draw_data()
        self.bricks.draw(self.surface)
        self.bat.draw(self.surface)
        self.ball.draw(self.surface)
        pygame.display.update()

游戏主循环

游戏主循环我们也并不陌生,play函数中定义了常见的窗口关闭逻辑,然后在事件监听循环中,监听鼠标按键操作,即MOUSEBUTTONUP事件。如果鼠标按下去了,而且球还没开始移动,则将served设置为真,后续代码会基于此条件判断,让球运动起来。之后则是bat更新函数,根据玩家输入控制球板,ball的更新函数update让球进行移动。检测球是否出界,检测球是否和球板碰撞,是否和砖块碰撞。最后绘图并控制游戏帧数。

    def play(self):
        while self.running:
            for event in pygame.event.get():
                if event.type == QUIT:
                    self.running = False

                if event.type == MOUSEBUTTONUP and not self.ball.served:
                    self.ball.served = True

            self.bat.update(self.Win_width)        
            self.ball.update(self.Win_width)
            self.check_failed()
            self.bat_collision()
            self.bricks_collision()
            self.draw()
            self.Clock.tick(self.fps)

        pygame.quit()
        print('Good Job! Final Score:', self.score)

main入口运行游戏的代码和贪吃蛇没什么区别。整体代码可以参见brick.py文件。大家可以通过阅读并运行完整的代码,来理解这个游戏的实现逻辑。顺便看看,你能打掉所有的砖块吗?

四、双人对战游戏

前面编写的打转块是一个单人游戏,我们再来编写一个类似的双人对战游戏,其游戏图示如下。

ch05-02

游戏玩法,就是两个人类玩家分别控制左右球板,来互相击球。球和球板的碰撞机制和单人游戏类似。如果某一方玩家出现漏球,则对方玩家得一分,看谁的分数更高。因为双人游戏和单人游戏很相似,我们对游戏设计和机制解释会简略一些,更多来看代码是如何实现的。

首先会定义球板类Paddle,因为不需要图片资源,所以只需要绘制一个矩形就可以了。draw.rect函数会返回一个Rect对象,它和加载图片返回的边框矩形是一样的类型,它用于后续的坐标控制。其它函数包括了绘图、移动和重置,这些函数很简单,其代码含义是显而易见的。

class Paddle:
    COLOR = (255, 255, 255)
    VEL = 4

    def __init__(self, surface, x, y, width, height):
        self.original_x = x
        self.original_y = y
        self.rect = pygame.draw.rect(surface, self.COLOR, (x, y, width, height))

    def draw(self, surface):
        pygame.draw.rect(surface, self.COLOR, self.rect)

    def move(self, up=True):
        if up:
            self.rect.y -= self.VEL
        else:
            self.rect.y += self.VEL

    def reset(self):
        self.rect.x = self.original_x
        self.rect.y = self.original_y

之后定义球Ball类,同样是不需要图片资源,而是用draw.circle函数来绘制一个圆形。其它函数的代码含义也是显而易见的。

class Ball:
    MAX_VEL = 8
    COLOR = (255, 255, 255)

    def __init__(self, surface, x, y, radius):
        self.original_x = x
        self.original_y = y
        self.radius = radius
        self.x_vel = self.MAX_VEL
        self.y_vel = 0
        self.rect = pygame.draw.circle(surface, self.COLOR, (x, y), radius)

    def draw(self, surface):
        pygame.draw.circle(surface, self.COLOR, (self.rect.centerx, self.rect.centery), self.radius)

    def move(self):
        self.rect.x += self.x_vel
        self.rect.y += self.y_vel

    def reset(self):
        self.rect.centerx = self.original_x
        self.rect.centery = self.original_y
        self.y_vel = 0
        self.x_vel *= -1

定义Game类,初始化中包括了生成窗口,也实例化了Ball对象,注意因为是双人游戏,所以实例化了两个球板,也定义了双方的分值。

class Game:
    WHITE = (255, 255, 255)
    BLACK = (0, 0, 0)
    def __init__(self, Width=800,Height=600):
        pygame.init()
        self.Win_width , self.Win_height = (Width, Height)
        self.surface = pygame.display.set_mode((self.Win_width, self.Win_height))
        self.paddle_width = 10
        self.paddle_height = 100
        self.ball_radius = 7
        self.win_score = 10
        self.ball = Ball(self.surface,self.Win_width // 2, self.Win_height // 2, self.ball_radius )
        self.left_paddle = Paddle(self.surface,10, self.Win_height//2 - self.paddle_height//2, 
                                    self.paddle_width, self.paddle_height)
        self.right_paddle = Paddle(self.surface,self.Win_width - 10 - self.paddle_width, 
                                    self.Win_height//2 - self.paddle_height//2, 
                                    self.paddle_width, self.paddle_height)
        self.left_score = 0
        self.right_score = 0
        self.fpsClock = pygame.time.Clock()
        self.font = pygame.font.SysFont("comicsans", 40)
        pygame.display.set_caption('Pong')

绘图函数主要分几部分,先显示两边的分值,再显示两边球板,中间画一条中线表示分界线,最后是显示球。

    def draw(self):
        self.surface.fill(self.BLACK)
        left_score_text = self.font.render(f"{self.left_score}", 1, self.WHITE)
        right_score_text = self.font.render(f"{self.right_score}", 1, self.WHITE)
        self.surface.blit(left_score_text, (self.Win_width//4 - left_score_text.get_width()//2, 20))
        self.surface.blit(right_score_text, (self.Win_width * (3/4) -
                                    right_score_text.get_width()//2, 20))

        self.left_paddle.draw(self.surface)
        self.right_paddle.draw(self.surface)
        pygame.draw.line(self.surface, self.WHITE,(self.Win_width//2,0),
                        (self.Win_width//2,self.Win_height),width=4)
        self.ball.draw(self.surface)
        pygame.display.update()

然后paddle_collision函数用于处理球和球板碰撞后情况。条件判断球中心是否处于球板顶部和底部之间,这就是为了确认没有发生意外的侧面碰撞。然后用colliderect函数确认二者发生碰撞,将球的x速度分量进行反转。最后几行代码是需要计算球和球板的相对位置,主要是为了丰富双方的进攻手段,当球越靠近球板中间时,y方向的速度分量越低,反之,当球越靠近球板边缘时,y方向的速度分量越高,那么一方面增加了反弹后的整体球速,另一方面也增加了反弹角度,让对方更难以招架。这里并没有保持球的整体速度在反弹前后一致。当然这样更好玩了不是吗?

    def paddle_collision(self,paddle):
        if (self.ball.rect.centery >= paddle.rect.top and 
            self.ball.rect.centery <= paddle.rect.bottom):
            if self.ball.rect.colliderect(paddle.rect):
                self.ball.x_vel *= -1

                difference_in_y = paddle.rect.centery - self.ball.rect.centery
                reduction_factor = (paddle.rect.height / 2) / self.ball.MAX_VEL
                y_vel = difference_in_y / reduction_factor
                self.ball.y_vel = -1 * y_vel

handle_collision函数用于处理所有的碰撞场景,如果球和上下边界碰撞,则发生反弹,如果和左右边界发生碰撞,意味着没有接到球,分值减少,重置球的位置。最后是调用上面paddle_collision函数,分别处理左侧球板和右侧球板的接球情况。

    def handle_collision(self):
        if self.ball.rect.bottom >= self.Win_height:
            self.ball.y_vel *= -1
        elif self.ball.rect.top <= 0:
            self.ball.y_vel *= -1

        if self.ball.rect.left < 0:
            self.right_score += 1
            self.ball.reset()
        elif self.ball.rect.right > self.Win_width:
            self.left_score += 1
            self.ball.reset()

        if self.ball.x_vel < 0:
            self.paddle_collision(self.left_paddle)
        else:
            self.paddle_collision(self.right_paddle)

handle_paddle_movement函数用于处理玩家输入,我们用w和s按键来处理左侧球板的上下移动,用上下按键来处理右侧球板的上下移动。这样两位玩家通过两组不同的键盘输入控制各自的球板角色。

    def handle_paddle_movement(self):
        keys = pygame.key.get_pressed()
        if (keys[pygame.K_w] and 
            self.left_paddle.rect.top - self.left_paddle.VEL >= 0):
            self.left_paddle.move(up=True)
        if (keys[pygame.K_s] and 
            self.left_paddle.rect.bottom + self.left_paddle.VEL <= self.Win_height):
            self.left_paddle.move(up=False)
        if (keys[pygame.K_UP] and 
            self.right_paddle.rect.top - self.right_paddle.VEL >= 0):
            self.right_paddle.move(up=True)
        if (keys[pygame.K_DOWN] and 
            self.right_paddle.rect.bottom  + self.right_paddle.VEL <= self.Win_height):
            self.right_paddle.move(up=False)

game_is_win函数用于判断两边选手谁最终赢得比赛。只要某位玩家得分超过win_score,就会显示提示信息,并将窗口冻结5秒钟,再重置游戏数据。

    def game_is_win(self):
        won = False 
        if self.left_score >= self.win_score:
            won = True
            win_text = "Left Player Won!"
        if self.right_score >= self.win_score:
            won = True
            win_text = "Right Player Won!"
        if won:
            text = self.font.render(win_text, 1, self.WHITE)
            self.surface.blit(text, (self.Win_width//2 - text.get_width() //
                            2, self.Win_height//2 - text.get_height()//2))
            pygame.display.update()
            pygame.time.delay(5000)
            self.ball.reset()
            self.left_paddle.reset()
            self.right_paddle.reset()
            self.left_score = 0
            self.right_score = 0

最后是将所有游戏逻辑组装在一起,构成了play_step函数。但这里并没有游戏主循环所必须的while循环,因为这里只是处理游戏的一帧,或者说一步。

    def play_step(self):
        game_over = False
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game_over = True

        self.handle_paddle_movement()
        self.ball.move()
        self.handle_collision()
        self.game_is_win()
        self.draw()
        self.fpsClock.tick(60)
        return game_over

真正的主循环我们放在模块外层,由main入口代码处理,也就是Game类里面不包括循环,而是在类的外面定义while循环,调用play_step以运行游戏的每一步。这两种主循环的处理效果是等价的的,不过我们先了解一下。因为在人工智能的部分我们会遇到这种主循环处理方式。

if __name__ == '__main__':
    game = Game()
    while True:
        game_over = game.play_step()
        if game_over:
            break
    pygame.quit()

整体代码可以参见pong.py文件。大家可以下载完整文件以方便阅读和运行。你可以试试看,能否打败你的朋友呢?

本章小结

本章我们基于PyGame模块完成了又一个经典游戏打砖块。设计并实现了四个游戏元素类。这是我们基于OOP实现的第二个游戏,整体思路和上一章的贪吃蛇并没有很大区别。这样才方便我们学习掌握面向对象编程,并进一步熟悉PyGame。毕竟学习的关键是重复。

本章的游戏编程代码也有不一样的地方。其一,单机游戏中我们是用鼠标来控制用户交互的。其二,它涉及到一些三角函数等数学计算的任务。其三,我们学习了如何来编写双人对战的模式。

如果你完全理解了本章的游戏代码,你也可以尝试对这个游戏做一些符合自己品味的修改。例如在单机游戏中,当球击中砖块时,能否增加一点音效,或者增加一点动画。双人对战游戏中,能否把简单的形状改成你喜欢的图片,或者新增有趣的对抗玩法。

在第十四章,我们会引入AI玩家,AI会根据游戏场景信息来自动移动球板。当然你需要耐心,先学完AI部分知识和第十三章的例子。在后续的第六章,我们会学习另一个经典游戏,笨鸟先飞。这个游戏不论从代码难度,还是游戏难度,都会比打砖块要更上一个台阶。让我们更上层楼吧。