第五章:打砖块游戏编程
在第四章我们学习了如何以面向对象的方法来编写贪吃蛇游戏。在本章,我们将学习编写另一个经典游戏,打砖块。这个游戏的整体结构和贪吃蛇区别不大,仍然会定义多个游戏元素的类。只不过这个游戏会涉及到一些三角函数的计算,以及鼠标控制。此外,我们还会在此基础上编写一个双人对战游戏。
一、打砖块游戏介绍
游戏规则
在打砖块游戏中,存在三种游戏元素,分别是玩家控制的球板,飞行的球,以及一组静止的砖块。游戏规则规定球可以自由飞行,如果碰撞到了场景中左侧、上侧和右侧的边界,球会反弹,改变其方向。如果飞出了下侧边界,则游戏会失败。玩家控制的球板,在场景下侧边界附近,可以控制其左右移动,以挡住球的行进路线。场景中的上方还会有一组砖块,如果球击中砖块,则会增加一分。游戏的目标就是用球板来挡击球,让球击中更多的砖块得分,避免失败。
游戏开始时,球会随机出现在窗口中部的位置,以静止状态,等待玩家的指令。玩家通过鼠标按键让球开始运行。如果玩家控制的球板未挡住球,则游戏重新开始。如果所有砖块被打掉或玩家关闭窗口,则游戏结束。
本章编写的打砖块游戏,还要求使用球板来控制球的反弹方向。具体来讲,就是当球和球板中间部分接触时,球会以较垂直的方向反弹回去。如果球和球板的偏边缘部分接触时,球会更向两则边界处反弹。此外,如果球落在左半部分球板时,球会偏左反弹,落在右半部分球板时,球会偏右反弹。这种碰撞机制赋予玩家更大的控制能力,可以更好的控制球反弹的方向,以准确击中目标砖块。
游戏画面如下所示。
游戏资源
游戏资源包括三种,放在了配套代码的相应目录中。资源对应了游戏中的三种游戏元素。分别是球板、球和砖块的图片资源。大家下载后,可以打开这些图片观察其图片像素大小。
二、游戏功能和程序设计
游戏功能
本游戏要实现的主要功能包括:
- 处理用户输入,根据用户的鼠标点击控制球开始运动,根据鼠标位置位置来移动球板。
- 控制每帧下球的移动,并判断球和边界的碰撞情况。
- 判断球有没有越过下侧边界,如果发生则游戏失败。
- 判断球和球板有没有发生碰撞。计算碰撞角度,将球进行反弹。为了增加难度,每次碰撞后增加球的速度。
- 判断球和砖块有没有发生碰撞。碰撞后将球进行反弹,并计分。
- 在游戏场景上方显示分数。
程序设计
游戏场景中需要处理三种角色,即球板、球和砖块。所以我们会设计三个类来封装三种角色对应的数据和函数。然后用一个游戏类来组装他们。设计的类图简化显示如下。
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文件。大家可以通过阅读并运行完整的代码,来理解这个游戏的实现逻辑。顺便看看,你能打掉所有的砖块吗?
四、双人对战游戏
前面编写的打转块是一个单人游戏,我们再来编写一个类似的双人对战游戏,其游戏图示如下。
游戏玩法,就是两个人类玩家分别控制左右球板,来互相击球。球和球板的碰撞机制和单人游戏类似。如果某一方玩家出现漏球,则对方玩家得一分,看谁的分数更高。因为双人游戏和单人游戏很相似,我们对游戏设计和机制解释会简略一些,更多来看代码是如何实现的。
首先会定义球板类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部分知识和第十三章的例子。在后续的第六章,我们会学习另一个经典游戏,笨鸟先飞。这个游戏不论从代码难度,还是游戏难度,都会比打砖块要更上一个台阶。让我们更上层楼吧。