本文作者: 楊曉輝
本文鏈接: http://youngxhui.github.io/2017/06/22/python-實現微信打飛機/
版權聲明: 本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 3.0 許可協議。
微信打飛機 python 實現
所用技術和軟件
python 2.7
pygame 1.9.3
pyCharm
準備工作
- 安裝好 pygame 在第一次使用 pygame 的時候,pyCharm 會自動 install pygame。
- 下載好使用的素材。
技術實現
初始化 pygame
首先要初始化 pygame ,之后設定一些基本的要點,比如窗口大小(盡量避免魔法數字),窗口標題以及背景圖像。pygame 通過加載圖片,最后返回一個 surface 對象,我們不需要關系圖片的格式。但是通過 convert()
這個函數,會使我們的圖片轉換效率提高。
# coding=utf8
import pygame
WIDTH = 480
HEIGHT = 800
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('飛機大戰(zhàn)')
background = pygame.image.load('resources/image/background.png').convert()
screen.fill(0)
screen.blit(background, (0, 0))
默認圖片左上角為原點 (0,0)。
顯示窗口
如果我們這樣設定,當我們運行的時候,窗口會一閃而過,并不會出現我們想象的畫面。因為窗口只是運行一下就會關閉,所以我們要寫一個循環(huán),使窗口一直保持出現。當然如果我們簡單的寫一個 while True那么我們的程序就出現了死循環(huán),卡死。
所以還需要寫個退出。
while True:
screen.fill(0)
screen.blit(background, (0, 0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
顯示飛機
首先我們要初始化我們的主角飛機
仍舊需要加載我們需要的資源,我們的資源文件里已經準備好各種各樣的飛機,但是他們都在一張切圖上。
同時我們的資源文件里還有一個叫做 shoot.pack
的文件,里面記錄了每個圖片所在的位置。
我們通過下面的代碼加載資源圖片,并且獲得我們需要的主角飛機。
plane_img = pygame.image.load('resources/image/shoot.png')
player = plane_img.subsurface(pygame.Rect(0, 99, 102, 126))
將 player 顯示在屏幕上,并且刷新屏幕
screen.blit(player, [100, 400])
pygame.display.update()
效果如下
讓飛機 “飛” 起來
飛機已經出現在我們的屏幕上了,現在需要讓飛機動起來讓他可以上下左右的移動。
首先要獲取鍵盤事件,獲取鍵盤上什么按鍵被按下。
key_pressed = pygame.key.get_pressed()
通過 key_pressed
獲取當前的鍵盤按鍵。并進行判斷,這里寫了四個函數進行對 player
移動。
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的出現
首先要明確一點,這個類需要什么。
我們之前對 player
有什么操作?定義了他的圖片和他出現的位置。所以我們的構造方法就要初始化這些值。
所有的這些對象,我們在 pygame
里叫做精靈(sprite),這個概念也在其他游戲開發(fā)中使用。
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
簡單的說就是獲取飛機的圖片,初始化飛機的矩形區(qū)域。rect
該屬性會獲得四個值。分別是左上角 x
,y
坐標,矩形的寬度。topleft
初始化飛機的左上角坐標,也就是飛機出現的位置。如下圖所示。
player的控制
當飛機出現了,我們就應該實現我們在循環(huán)里寫的方法。我們首先要判斷它還在不在屏幕內,不能讓飛機飛出屏幕。可以通過 rect.top
,rect.bottom
,rect.left
,rect.right
四個方法獲取飛機圖片的上下左右四個邊界值。
這樣我們就能對飛機進行判斷
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
是我們對飛機的移動的位移定義的常量。
讓子彈飛
子彈要沿著發(fā)射方向射出去。可以在屏幕上一直移動,直到移出屏幕。
我們只要有定義一個子彈對象,讓這個對象顯示在屏幕上就可以。
先定義飛機子彈類,基本和定義 player
一樣,獲得圖片,裁剪圖片,設置圖片初始位置,在屏幕上顯示圖片
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)
# 省略其他代碼
運行結果
下一步就是讓飛機的子彈跟隨飛機。
我們需要在 Player 類里面添加方法。
首先我們規(guī)定,按下空格發(fā)射子彈。
if key_pressed[pygame.K_SPACE]:
player.shoot()
完善shoot方法。子彈類已經有了,我們每次只要在按下空格的時候創(chuàng)建一個對象就好。
首先要每次傳入一個子彈的圖像,然后還有出現位置,這樣子彈才能跟隨飛機。
定義一個pygame.sprite.Group()
來存放精靈組。這樣我們就能把我們的子彈都放進去。
def shoot(self, bullet_img):
bullet = Bullet(bullet_img, self.rect.midtop)
self.bullets.add(bullet)
每次按下空格的時候傳入一個子彈圖片
if key_pressed[pygame.K_SPACE]:
player.shoot(bullet_img)
最后我們只需要在屏幕上進行子彈的繪制即可。
player.bullets.draw(screen)
這樣我們的子彈就會跟隨飛機出現。
下一步就是讓子彈在屏幕上移動。
創(chuàng)建移動的方法。
def move(self):
self.rect.top -= self.move
因為我們的子彈在 bullets 里,所以我們僅需要一個循環(huán),遍歷每個子彈,之后移動即可。當子彈移出屏幕的時候我們只要在 bullets
中移出就可以。
for bullet in player.bullets:
bullet.bulletMove()
if bullet.rect.bottom < 0:
player.bullets.remove(bullet)
結果
這個和我們的預期還是有差別的,頻率太快了。
關于pygame 的鍵盤重復事件 官方好像并沒有這個設置。那么我們只能在添加一個計數器,通過計算器的數值來判斷子彈是否發(fā)射。這里的數值是多次測試后,自己感覺一個比較滿意的頻率。可以自己調整。
# 省略其他代碼
# 子彈頻率
SHOOT_PC = 0
在鍵盤事件中我們需要判斷頻率。
if key_pressed[pygame.K_SPACE]:
SHOOT_PC = SHOOT_PC + 1
if SHOOT_PC % 400 == 0:
player.shoot(bullet_img)
player 的飛機就算基本繪制好了
繪制敵機
下一步就是繪制敵機。敵機是從屏幕上方移動到屏幕下方。我們任就需要一個類來設置敵機。設置類任就和我們前面的差不多,加載資源,設置 rect
,設置位置。
# 加載敵機圖片
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
最后在屏幕顯示出來
screen.blit(enemy_img, enemy_rect)
現在我們就應該想想敵機的特點了,其實他和子彈的特點基本一致,只不過方向不一樣而已。還有一點是敵機是隨機生成的。
# 敵機計數器
EnEMY_PC = 0
# 省略代碼
enemy_position = [random.randint(0, WIDTH - enemy_rect.width), 0]
enemy = Enemy(enemy_img, enemy_position)
enemies.add(enemy)
我們隨機在頂部生成飛機。
這個方式的情況和子彈其實差不多,我們應該給出現敵機確定一個頻率。
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
這樣的話出現情況就變得緩慢。下一步實現敵機的移動。敵機的移動原理和子彈的移動其實也是一樣的。不多解釋
移動方法
def enemyMove(self):
self.rect.top += self.move
移動實現
for enemy in enemies:
enemy.enemyMove()
if enemy.rect.top > HEIGHT:
enemies.remove(enemy)
enemies.draw(screen)
碰撞檢測
飛機和敵機還有子彈都有了,我們現在需要進行完成碰撞檢測。有下面幾種場景。
- 敵機和玩家碰撞在一起
- 子彈和敵機碰撞在一起
無論是那種情況的碰撞,其實就是兩張圖片有了交集。
如圖
pygame 給我們提供了碰撞檢測的方法。首先兩個對象必須是
sprite
。通過 pygame.sprite.collide_rect()
進行碰撞檢測。
我們先進行一個測試
if pygame.sprite.collide_rect(enemy, player):
print '檢測成功'
結果
檢測成功
此時我們就可以完成,當玩家和敵機發(fā)生碰撞,游戲結束,當子彈和敵機碰撞,敵機消失。
同樣的 pygame 給我們提供了一個 pygame.sprite.groupcollide()
用于 Group
之間的碰撞檢測.當發(fā)生碰撞的時候這兩個對象都會在 Group
中移出。
用于檢測敵機和子彈
pygame.sprite.groupcollide(enemies, player.bullets, 1, 1)
敵機和子彈的關系已經和好的處理。
處理敵機和玩家飛機的關系。
我們需要在 Player
里添加一個屬性判斷當前玩家是否被擊中的 boolean
值.當集中的時候把屬性改為 True
.當為 True
的時候游戲結束.也就是我們一開始設置的循環(huán)就會結束.所以我們需要更改之前寫的一些代碼,使它更加完善。
在 Player 類里面添加是否擊中屬性。
self.is_hit = False
修改循環(huán)
RUN = True
while RUN:
# 省略代碼
if pygame.sprite.collide_rect(enemy, player):
player.is_hit = True
RUN = False
# 省略代碼
執(zhí)行結果
當玩家被擊中的時候,在顯示一張 GameOver 圖片提示
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()
做到這里基本算是實現了飛機大戰(zhàn).但是還有很多細節(jié)處理。
細節(jié)處理
精細的碰撞檢測
從圖上看,當敵機看似還沒有和我們接觸時,但是已經 GameOver 了。
實際情況是這樣的,所有的圖片都是矩形,當兩張圖片的矩形邊框線碰撞的時候,就算兩個對象碰撞,所以我們要更加精細的使用碰撞檢測。
我們可以按著圖片中心的某個長度為半徑,在這個半徑內發(fā)生碰撞才是碰撞。
pygame 給我們提供了這樣的方法。pygame.sprite.collide_circle_ratio()
可以自己算出一個半徑,作為檢測半徑。并且可以做出一個有效檢測的百分比。
if pygame.sprite.collide_circle_ratio(0.6)(player, enemy):
player.is_hit = True
RUN = False
同樣,子彈和敵機也可以修改,讓碰撞檢測更加精細。
修改后面的兩個參數,使得碰撞檢測更加精細。
pygame.sprite.groupcollide(enemies, player.bullets, 0.6, 0.8)
動畫
做了這么久,感覺它沒有一點動效,感覺死氣沉沉的。無論是飛機飛行,還是飛機被擊中,都沒有一個明確的反饋。
對于2d游戲,動畫其實就是一張一張的圖片不停的變化。就和電影的原理類似。要想讓我們的飛機動起來,我們需要定義一個列表來存放這些圖片,然后寫個循環(huán),讓他一直不停的更換圖片就好。
首先我們更改我們的主角 Player
任就是老套路,加載圖片。把加載的圖片放到list 里。
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
添加循環(huán)的方法。獲取圖片。
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
飛機正常飛行的圖片只有兩張。所以我們要循環(huán)變化這兩張圖片。所以每發(fā)射一個子彈,圖片變化兩張。
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
正常發(fā)射子彈的動畫效果已經做完。我們還需要進行被擊中爆炸的動畫效果。
擊中的原理和正常也一樣。只不過先要判斷當前飛機狀態(tài),是否被擊中。
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
248
,30
,495
,1457
這些數字是什么?如何計算出來的。先說 495
這個數字。
495 這個數字很隨便,只是控制子彈的發(fā)射間隔。完全可以自定義。但是495這個數字一旦確定,其他三個數字基本確定。
248 為 495 的一半,因為發(fā)射一個子彈,圖片要變化兩張。
30 這個數字基本也是自定義的,只要比1大就好,他影響了結束動畫出現的時間。
1488 這個數字是通過 248 確定的,是 248 的 6倍,因為飛機被射擊后會有四張圖片的顯示。
同理,把敵機接觸子彈的動畫寫出來。
加載圖片
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))]
同樣我們需要創(chuàng)建 Group() 來存放被擊中的敵機。
enemies_shoot = pygame.sprite.Group()
之后的處理邏輯基本相似,不多介紹
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
這樣的話基本完成了動畫效果。
音樂
有了動畫還的有音樂。
音樂的處理只要在特定的地方播放音樂就好,比如子彈發(fā)射的時候,背景音樂,被擊中的時候,游戲結束的時候,等等。他們的處理邏輯都一樣。先加載資源,然后在播放。
背景音樂的播放。
pygame
在處理背景音樂的時候都在 pygame.mixer
方法中。其中播放音樂的play中的參數,第一個為播放幾次,-1 為循環(huán)播放,后面的浮點表示 從第幾秒開始播放。
backgroundMusic = pygame.mixer.music.load('resources/sound/game_music.mp3')
pygame.mixer.music.play(-1, 0.0)
其他的音樂先加載資源,在需要的地方播放。
發(fā)射子彈
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()
其他音樂處理一樣,不多解釋。
分數&等級
分數
首先繪制得分情況,在屏幕上顯示多少分。
繪制字體基本和繪制精靈是差不多的。首先要生成字體
兩個參數分別是字體和字號
score_font = pygame.font.Font(None, 36)
有了字體那么需要寫點字。
score_font.render("分數",True,(0,0,0),(255,255,255))
第一個參數是寫的文字;第二個參數是個布爾值,以為這是否開啟抗鋸齒,就是說True的話字體會比較平滑,不過相應的速度有一點點影響;第三個參數是字體的顏色;第四個是背景色,如果你想沒有背景色(也就是透明),那么可以不加這第四個參數
字體也有了,文本也有了,下一步就是繪制。通過 get_rect()
獲得矩形,之后繪制和精靈繪制方法一樣
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)
分數已經顯示了,就可以積分。我們每擊落一個飛機增加 100 分。
if enemy_shoots.shoot_index > 70:
enemies_shoot.remove(enemy_shoots)
score += 100
continue
我們還需要在文本的地方強制轉換為 str 。
score_text = score_font.render(str(score), True, (128, 128, 128))
等級
有了分數,那么再加點等級會使游戲更加有趣味性。
同樣的先繪制等級。
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)
下一步就是寫等級函數。
隨著分數的增加,等級增加,飛機變多,等等。
首先寫分數和等級的關系。
隨便瞎寫的函數
if score == 100 * (level ** 2 + level):
level += 1
這個是控制敵機數量的,我們可以設定一個變量,使敵機越來越多。
if ENEMY_PC % 500 == 0:
每增加一級,就添加敵機數量。等級也不能一直增加,所以當等級是摸個值的時候,就算最高級別了。
if score == 100 * (level ** 2 + level):
level += 1
if level != 10:
enemy_add -= 20
基本到這里算是寫了一個相對完整的游戲。