python 实现微信打飞机

微信打飞机 python 实现

所用技术和软件

python 2.7

pygame 1.9.3

pyCharm

准备工作

  1. 安装好 pygame 在第一次使用 pygame 的时候,pyCharm 会自动 install pygame。
  2. 下载好使用的素材。

技术实现

首先要初始化 pygame ,之后设定一些基本的要点,比如窗口大小(尽量避免魔法数字),窗口标题以及背景图像。pygame 通过加载图片,最后返回一个 surface 对象,我们不需要关系图片的格式。但是通过 convert() 这个函数,会使我们的图片转换效率提高。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# coding=utf8

import pygame

WIDTH = 480
HEIGHT = 800

pygame.init()

screen = pygame.display.set_mode((WIDTH, HEIGHT))

pygame.display.set_caption('飞机大战')

background = pygame.image.load('resources/image/background.png').convert()

screen.fill(0)
screen.blit(background, (0, 0))

默认图片左上角为原点 (0,0)。

如果我们这样设定,当我们运行的时候,窗口会一闪而过,并不会出现我们想象的画面。因为窗口只是运行一下就会关闭,所以我们要写一个循环,使窗口一直保持出现。当然如果我们简单的写一个 while True那么我们的程序就出现了死循环,卡死。

https://island-hexo.oss-cn-beijing.aliyuncs.com/flyerroe.png

所以还需要写个退出。

1
2
3
4
5
6
7
8
while True:
    screen.fill(0)
    screen.blit(background, (0, 0))

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

首先我们要初始化我们的主角飞机

仍旧需要加载我们需要的资源,我们的资源文件里已经准备好各种各样的飞机,但是他们都在一张切图上。

https://island-hexo.oss-cn-beijing.aliyuncs.com/shoot.png

同时我们的资源文件里还有一个叫做 shoot.pack 的文件,里面记录了每个图片所在的位置。

我们通过下面的代码加载资源图片,并且获得我们需要的主角飞机。

1
2
3
plane_img = pygame.image.load('resources/image/shoot.png')

player = plane_img.subsurface(pygame.Rect(0, 99, 102, 126))

将 player 显示在屏幕上,并且刷新屏幕

1
2
3
screen.blit(player, [100, 400])

pygame.display.update()

效果如下

https://island-hexo.oss-cn-beijing.aliyuncs.com/flyplayer.png

飞机已经出现在我们的屏幕上了,现在需要让飞机动起来让他可以上下左右的移动。

首先要获取键盘事件,获取键盘上什么按键被按下。

1
key_pressed = pygame.key.get_pressed()

通过 key_pressed 获取当前的键盘按键。并进行判断,这里写了四个函数进行对 player 移动。

1
2
3
4
5
6
7
8
if key_pressed[pygame.K_w] or key_pressed[pygame.K_UP]:
    player.moveUp()
if key_pressed[pygame.K_s] or key_pressed[pygame.K_DOWN]:
    player.moveDown()
if key_pressed[pygame.K_a] or key_pressed[pygame.K_LEFT]:
    player.moveLeft()
if key_pressed[pygame.K_d] or key_pressed[pygame.K_RIGHT]:
    player.moveRight()

下一步就是完善这四个方法。

简单的说就是按下方向键的时候(w,a,s,d)飞机向四周移动,但是不能移动离开屏幕。

此时我们就应该把我们的飞机形成一个类,类里面有控制飞机的方法。

这里写类比较麻烦一点

首先要明确一点,这个类需要什么。

我们之前对 player 有什么操作?定义了他的图片和他出现的位置。所以我们的构造方法就要初始化这些值。 所有的这些对象,我们在 pygame 里叫做精灵(sprite),这个概念也在其他游戏开发中使用。

1
2
3
4
5
6
class Player(pygame.sprite.Sprite):
    def __init__(self, plane_img, player_rect, player_position):
        pygame.sprite.Sprite.__init__(self)
        self.img = plane_img.subsurface(player_rect)
        self.rect = player_rect
        self.rect.topleft = player_position

简单的说就是获取飞机的图片,初始化飞机的矩形区域。rect 该属性会获得四个值。分别是左上角 x ,y 坐标,矩形的宽度。topleft 初始化飞机的左上角坐标,也就是飞机出现的位置。如下图所示。

https://island-hexo.oss-cn-beijing.aliyuncs.com/surfaceposition.jpg

当飞机出现了,我们就应该实现我们在循环里写的方法。我们首先要判断它还在不在屏幕内,不能让飞机飞出屏幕。可以通过 rect.top,rect.bottom,rect.left,rect.right四个方法获取飞机图片的上下左右四个边界值。

这样我们就能对飞机进行判断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    def moveUp(self):
        if self.rect.top <= 0:
            self.rect.top = 0
        else:
            self.rect.top -= self.move

    def moveDown(self):
        if self.rect.bottom >= HEIGHT:
            self.rect.bottom = HEIGHT
        else:
            self.rect.bottom += self.move

    def moveLeft(self):
        if self.rect.left <= 0:
            self.rect.left = 0
        else:
            self.rect.left -= self.move

    def moveRight(self):
        if self.rect.right >= WIDTH:
            self.rect.right = WIDTH
        else:
            self.rect.right += self.move

这里的 move 是我们对飞机的移动的位移定义的常量。

子弹要沿着发射方向射出去。可以在屏幕上一直移动,直到移出屏幕。 我们只要有定义一个子弹对象,让这个对象显示在屏幕上就可以。 先定义飞机子弹类,基本和定义 player 一样,获得图片,裁剪图片,设置图片初始位置,在屏幕上显示图片

 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
class Bullet(pygame.sprite.Sprite):
    def __init__(self, bullet_image, bullet_position):
        pygame.sprite.Sprite.__init__(self)
        self.image = bullet_image
        self.rect = self.image.get_rect()
        self.rect.midbottom = bullet_position


        
# 省略其他代码

# 加载子弹图片
bullet_rect = pygame.Rect(69, 78, 9, 21)
bullet_img = plane_img.subsurface(bullet_rect)

# 省略其他代码

while True:

    # 省略其他代码
    
    screen.blit(bullet.img, bullet.rect)
    
    # 省略其他代码
    

运行结果 https://island-hexo.oss-cn-beijing.aliyuncs.com/bulletShow.jpg

下一步就是让飞机的子弹跟随飞机。 我们需要在 Player 类里面添加方法。 首先我们规定,按下空格发射子弹。

1
2
    if key_pressed[pygame.K_SPACE]:
        player.shoot()

完善shoot方法。子弹类已经有了,我们每次只要在按下空格的时候创建一个对象就好。 首先要每次传入一个子弹的图像,然后还有出现位置,这样子弹才能跟随飞机。 定义一个pygame.sprite.Group() 来存放精灵组。这样我们就能把我们的子弹都放进去。

1
2
3
     def shoot(self, bullet_img):
        bullet = Bullet(bullet_img, self.rect.midtop)
        self.bullets.add(bullet)

每次按下空格的时候传入一个子弹图片

1
2
    if key_pressed[pygame.K_SPACE]:
        player.shoot(bullet_img)

最后我们只需要在屏幕上进行子弹的绘制即可。

1
player.bullets.draw(screen)

这样我们的子弹就会跟随飞机出现。

https://island-hexo.oss-cn-beijing.aliyuncs.com/bulletShow2.jpg

下一步就是让子弹在屏幕上移动。

创建移动的方法。

1
2
    def move(self):
        self.rect.top -= self.move

因为我们的子弹在 bullets 里,所以我们仅需要一个循环,遍历每个子弹,之后移动即可。当子弹移出屏幕的时候我们只要在 bullets 中移出就可以。

1
2
3
4
5

    for bullet in player.bullets:
        bullet.bulletMove()
        if bullet.rect.bottom < 0:
            player.bullets.remove(bullet)

结果 https://island-hexo.oss-cn-beijing.aliyuncs.com/playerflyshoot.gif

这个和我们的预期还是有差别的,频率太快了。

关于pygame 的键盘重复事件 官方好像并没有这个设置。那么我们只能在添加一个计数器,通过计算器的数值来判断子弹是否发射。这里的数值是多次测试后,自己感觉一个比较满意的频率。可以自己调整。

1
2
3
4
# 省略其他代码

# 子弹频率
SHOOT_PC = 0

在键盘事件中我们需要判断频率。

1
2
3
4
    if key_pressed[pygame.K_SPACE]:
        SHOOT_PC = SHOOT_PC + 1
        if SHOOT_PC % 400 == 0:
            player.shoot(bullet_img)

player 的飞机就算基本绘制好了

https://island-hexo.oss-cn-beijing.aliyuncs.com/playerflyshoot2.gif

下一步就是绘制敌机。敌机是从屏幕上方移动到屏幕下方。我们任就需要一个类来设置敌机。设置类任就和我们前面的差不多,加载资源,设置 rect,设置位置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

# 加载敌机图片

enemy_rect = pygame.Rect(267, 347, 57, 51)
enemy_img = plane_img.subsurface(enemy_rect)
enemy_position = [200, 200]
enemy = Enemy(enemy_img, enemy_position)


# 敌机类

class Enemy(pygame.sprite.Sprite):
    def __init__(self, enemy_img, enemy_position):
        pygame.sprite.Sprite.__init__(self)
        self.image = enemy_img
        self.rect = self.image.get_rect()
        self.rect.topleft = enemy_position

最后在屏幕显示出来

1
screen.blit(enemy_img, enemy_rect)

https://island-hexo.oss-cn-beijing.aliyuncs.com/enemy.jpg

现在我们就应该想想敌机的特点了,其实他和子弹的特点基本一直,只不过方向不一样而已。还有一点是敌机是随机生成的。

1
2
3
4
5
6
# 敌机计数器
EnEMY_PC = 0
# 省略代码
enemy_position = [random.randint(0, WIDTH - enemy_rect.width), 0]
        enemy = Enemy(enemy_img, enemy_position)
        enemies.add(enemy)

我们随机在顶部生成飞机。

https://island-hexo.oss-cn-beijing.aliyuncs.com/enemyshow.gif

这个方式的情况和子弹其实差不多,我们应该给出现敌机确定一个频率。

1
2
3
4
5
6
    if EnEMY_PC % 500 == 0:
        enemy_position = [random.randint(0, WIDTH - enemy_rect.width), 0]
        enemy = Enemy(enemy_img, enemy_position)
        enemies.add(enemy)

    EnEMY_PC = EnEMY_PC + 1

这样的话出现情况就变得缓慢。下一步实现敌机的移动。敌机的移动原理和子弹的移动其实也是一样的。不多解释

移动方法

1
2
    def enemyMove(self):
        self.rect.top += self.move

移动实现

1
2
3
4
5
6
 for enemy in enemies:
        enemy.enemyMove()
        if enemy.rect.top > HEIGHT:
            enemies.remove(enemy)

    enemies.draw(screen)

https://island-hexo.oss-cn-beijing.aliyuncs.com/enemyMove.gif

飞机和敌机还有子弹都有了,我们现在需要进行完成碰撞检测。有下面几种场景。

  1. 敌机和玩家碰撞在一起
  2. 子弹和敌机碰撞在一起

无论是那种情况的碰撞,其实就是两张图片有了交集。 如图 https://island-hexo.oss-cn-beijing.aliyuncs.com/penz.jpg pygame 给我们提供了碰撞检测的方法。首先两个对象必须是 sprite 。通过 pygame.sprite.collide_rect() 进行碰撞检测。

我们先进行一个测试

1
2
    if pygame.sprite.collide_rect(enemy, player):
        print '检测成功'

结果

检测成功

此时我们就可以完成,当玩家和敌机发生碰撞,游戏结束,当子弹和敌机碰撞,敌机消失。

同样的 pygame 给我们提供了一个 pygame.sprite.groupcollide() 用于 Group 之间的碰撞检测.当发生碰撞的时候这两个对象都会在 Group 中移出。

用于检测敌机和子弹

1
pygame.sprite.groupcollide(enemies, player.bullets, 1, 1)

敌机和子弹的关系已经和好的处理。 处理敌机和玩家飞机的关系。

我们需要在 Player 里添加一个属性判断当前玩家是否被击中的 boolean 值.当集中的时候把属性改为 True.当为 True 的时候游戏结束.也就是我们一开始设置的循环就会结束.所以我们需要更改之前写的一些代码,使它更加完善。

在 Player 类里面添加是否击中属性。

1
self.is_hit = False

修改循环

1
2
3
4
5
6
7
8
RUN = True

while RUN:
    # 省略代码
        if pygame.sprite.collide_rect(enemy, player):
        player.is_hit = True
        RUN = False
    # 省略代码

执行结果

https://island-hexo.oss-cn-beijing.aliyuncs.com/gameover2.gif

当玩家被击中的时候,在显示一张 GameOver 图片提示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
gameOver = pygame.image.load('resources/image/gameover.png')

while GAMEOVER:
    screen.fill(0)
    screen.blit(gameOver, (0, 0))

    pygame.display.update()

    # 退出程序
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            exit()

做到这里基本算是实现了飞机大战.但是还有很多细节处理。

http://7xt81u.com1.z0.glb.clouddn.com/finish1.gif

细节处理

从图上看,当敌机看似还没有和我们接触时,但是已经 GameOver 了。 实际情况是这样的,所有的图片都是矩形,当两张图片的矩形边框线碰撞的时候,就算两个对象碰撞,所以我们要更加精细的使用碰撞检测。

http://7xt81u.com1.z0.glb.clouddn.com/coll.jpg

我们可以按着图片中心的某个长度为半径,在这个半径内发生碰撞才是碰撞。

http://7xt81u.com1.z0.glb.clouddn.com/coll2.jpg

pygame 给我们提供了这样的方法。pygame.sprite.collide_circle_ratio() 可以自己算出一个半径,作为检测半径。并且可以做出一个有效检测的百分比。

1
2
3
    if pygame.sprite.collide_circle_ratio(0.6)(player, enemy):
        player.is_hit = True
        RUN = False

同样,子弹和敌机也可以修改,让碰撞检测更加精细。 修改后面的两个参数,使得碰撞检测更加精细。

1
pygame.sprite.groupcollide(enemies, player.bullets, 0.6, 0.8)

做了怎么就,感觉它没有一点动效,感觉死气沉沉的。无论是飞机飞行,还是飞机被击中,都没有一个明确的反馈。 对于2d游戏,动画其实就是一张一张的图片不停的变化。就和电影的原理类似。要想让我们的飞机动起来,我们需要定义一个列表来存放这些图片,然后写个循环,让他一直不停的更换图片就好。

首先我们更改我们的主角 Player 任就是老套路,加载图片。把加载的图片放到list 里。

1
2
3
4
5
6
7
8
9
player_rect = [pygame.Rect(0, 99, 102, 126),
               pygame.Rect(165, 360, 102, 126),
               pygame.Rect(165, 234, 102, 126),
               pygame.Rect(330, 624, 102, 126), 
               pygame.Rect(330, 498, 102, 126), 
               pygame.Rect(432, 624, 102, 126)]

player_position = [100, 400]
player = Player(plane_img, player_rect, player_position)

之后在 Player 添加循环的方法。获取图片。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Player(pygame.sprite.Sprite):
    def __init__(self, plane_img, player_rect, player_position):
        pygame.sprite.Sprite.__init__(self)
        self.image = []
        for i in range(len(player_rect)):
            self.image.append(plane_img.subsurface(player_rect[i]).convert_alpha())
        self.rect = player_rect[0]
        self.rect.topleft = player_position
        self.img_index = 0
        self.move = 1
        self.bullets = pygame.sprite.Group()
        self.is_hit = False

飞机正常飞行的图片只有两张。所以我们要循环变化这两张图片。所以每发射一个子弹,图片变化两张。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

    screen.blit(player.image[player.img_index], player.rect)
    player.img_index = SHOOT_PC / 248
    # 省略代码
    if key_pressed[pygame.K_SPACE]:
        if SHOOT_PC % 495 == 0:
            player.shoot(bullet_img)
        SHOOT_PC = SHOOT_PC + 1
        if SHOOT_PC >= 495:
            SHOOT_PC = 0

正常发射子弹的动画效果已经做完。我们还需要进行被击中爆炸的动画效果。

击中的原理和正常也一样。只不过先要判断当前飞机状态,是否被击中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    if not player.is_hit:
        screen.blit(player.image[player.img_index], player.rect)
        player.img_index = SHOOT_PC / 248
    else:
        player.img_index = player_shoot / 248
        screen.blit(player.image[player.img_index], player.rect)
        player_shoot += 30
        if player_shoot > 495:
            RUN = False

    # 省略代码
    if pygame.sprite.collide_circle_ratio(0.6)(player, enemy):
        player.is_hit = True

248304951457 这些数字是什么?如何计算出来的。先说 495 这个数字。 495 这个数字很随便,只是控制子弹的发射间隔。完全可以自定义。但是495这个数字一旦确定,其他三个数字基本确定。 248 为 495 的一半,因为发射一个子弹,图片要变化两张。 30 这个数字基本也是自定义的,只要比1大就好,他影响了结束动画出现的时间。

1488 这个数字是通过 248 确定的,是 248 的 6倍,因为飞机被射击后会有四张图片的显示。 同理,把敌机接触子弹的动画写出来。

加载图片

1
2
3
4
enemies_shoot_img = [plane_img.subsurface(pygame.Rect(267, 347, 57, 43)),
                     plane_img.subsurface(pygame.Rect(873, 697, 57, 43)),
                     plane_img.subsurface(pygame.Rect(267, 296, 57, 43)),
                     plane_img.subsurface(pygame.Rect(930, 697, 57, 43))]

同样我们需要创建 Group() 来存放被击中的敌机。

1
enemies_shoot = pygame.sprite.Group()

之后的处理逻辑基本相似,不多介绍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    for enemy in enemies:
        enemy.enemyMove()
        if pygame.sprite.collide_circle_ratio(0.6)(player, enemy):
            enemies_shoot.add(enemy)
            enemies.remove(enemy)
            player.is_hit = True
            break
        if enemy.rect.top > HEIGHT:
            enemies.remove(enemy)

    for enemy_shoots in enemies_shoot:
        if enemy_shoots.shoot_index == 0:
            pass
        if enemy_shoots.shoot_index > 70:
            enemies_shoot.remove(enemy_shoots)
            continue
        screen.blit(enemy_shoots.shoot_imgs[enemy_shoots.shoot_index / 20], enemy_shoots.rect)
        enemy_shoots.shoot_index += 1

这样的话基本完成了动画效果。

有了动画还的有音乐。

音乐的处理只要在特定的地方播放音乐就好,比如子弹发射的时候,背景音乐,被击中的时候,游戏结束的时候,等等。他们的处理逻辑都一样。先加载资源,然后在播放。

背景音乐的播放。

pygame 在处理背景音乐的时候都在 pygame.mixer 方法中。其中播放音乐的play中的参数,第一个为播放几次,-1 为循环播放,后面的浮点表示 从第几秒开始播放。

1
2
backgroundMusic = pygame.mixer.music.load('resources/sound/game_music.mp3')
pygame.mixer.music.play(-1, 0.0)

其他的音乐先加载资源,在需要的地方播放。

发射子弹

1
2
3
4
5
 def shoot(self, bullet_img):
        shootMusic = pygame.mixer.Sound('resources/sound/bullet.mp3')
        bullet = Bullet(bullet_img, self.rect.midtop)
        self.bullets.add(bullet)
        shootMusic.play()

其他音乐处理一样,不多解释。

首先绘制得分情况,在屏幕上显示多少分。

绘制字体基本和绘制精灵是差不多的。首先要生成字体 两个参数分别是字体和字号

1
score_font = pygame.font.Font(None, 36)

有了字体那么需要写点字。

1
score_font.render("分数",True,(0,0,0),(255,255,255))

第一个参数是写的文字;第二个参数是个布尔值,以为这是否开启抗锯齿,就是说True的话字体会比较平滑,不过相应的速度有一点点影响;第三个参数是字体的颜色;第四个是背景色,如果你想没有背景色(也就是透明),那么可以不加这第四个参数 字体也有了,文本也有了,下一步就是绘制。通过 get_rect() 获得矩形,之后绘制和精灵绘制方法一样

1
2
3
4
5
    score_font = pygame.font.Font(None, 36)
    score_text = score_font.render('分数:0', True, (128, 128, 128))
    text_rect = score_text.get_rect()
    text_rect.topleft = [10, 10]
    screen.blit(score_text, text_rect)

https://island-hexo.oss-cn-beijing.aliyuncs.com/scoreShow.jpg

分数已经显示了,就可以积分。我们每击落一个飞机增加 100 分。

1
2
3
4
 if enemy_shoots.shoot_index > 70:
            enemies_shoot.remove(enemy_shoots)
            score += 100
            continue

我们还需要在文本的地方强制转换为 str 。

1
score_text = score_font.render(str(score), True, (128, 128, 128))

有了分数,那么再加点等级会使游戏更加有趣味性。

同样的先绘制等级。

1
2
3
4
5
    level_font = pygame.font.Font(None, 42)
    level_text = level_font.render('Level '+str(level), True, (128, 128, 128, 128))
    level_rect = level_text.get_rect()
    level_rect.midtop = [240, 10]
    screen.blit(level_text, level_rect)

下一步就是写等级函数。 随着分数的增加,等级增加,飞机变多,等等。 首先写分数和等级的关系。 随便瞎写的函数

1
2
 if score == 100 * (level ** 2 + level):
        level += 1

这个是控制敌机数量的,我们可以设定一个变量,使敌机越来越多。

1
if ENEMY_PC % 500 == 0:

每增加一级,就添加敌机数量。等级也不能一直增加,所以当等级是摸个值的时候,就算最高级别了。

1
2
3
4
    if score == 100 * (level ** 2 +  level):
        level += 1
        if level != 10:
            enemy_add -= 20

基本到这里算是写了一个相对完整的游戏。

https://island-hexo.oss-cn-beijing.aliyuncs.com/end.jpg

代码

Github地址

相关内容