成品已錄制視頻投稿B站(本文目前實現了基礎的游戲功能),點擊觀看
項目稽忽悠不(github)地址:
https://github.com/BigShuang/From-simple-to-Huaji本文首發於本人簡書
初始准備工作
- 本項目使用的python3版本(如果你用python2,我不知會怎么樣)
- Ide推薦大家選擇pycharm(不同ide應該沒影響)
- 需要安裝第三方庫pygame,
pygame安裝方法(windows電腦,mac系統本人實測與pygame不兼容,強行運行本項目卡成ppt)
電腦打開cmd命令窗口,輸入
pip3 install pygame
補充說明:
由於眾所周知的原因,安裝過程中下載可能十分緩慢,甚至由此導致安裝失敗
此時建議大家嘗試使用鏡像下載
---國內源---
清華:https://pypi.tuna.tsinghua.edu.cn/simple
阿里雲:http://mirrors.aliyun.com/pypi/simple/
中國科技大學: https://pypi.mirrors.ustc.edu.cn/simple/
華中理工大學:http://pypi.hustunique.com/
山東理工大學:http://pypi.sdutlinux.org/
豆瓣:http://pypi.douban.com/simple/
使用辦法 pip install xxxx -i jinxiangurl
具體到pygame,則是:pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple
一、實現基礎窗口
0 - 新建app.py文件,內容如下
import pygame WINWIDTH = 600 # 窗口寬度 WINHEIGHT = 900 # 窗口高度 pygame.init() # pygame初始化,必須有,且必須在開頭 # 創建主窗體 win=pygame.display.set_mode((WINWIDTH,WINHEIGHT))
此時運行app.py,會發現一個一閃而逝的窗口
1 - 進一步,我們自然而然的就要思考這些問題
- 怎么維持住這個窗口?
通過while循環去實現
- 但是簡單的循環只是單純的將界面卡住,怎么實現刷新?
在循環體內使用pygame.display.update()
語句進行界面的更新
- 循環的刷新頻率不做節制的話,界面會飛速刷新導致卡死,怎么辦?
pygame有專門的對象pygame.time.Clock
用於去控制循環刷新的頻率,創建pygame.time.Clock
對象后,調用該對象的tick()
方法,函數參數為每秒刷新次數,就可以設置循環每秒刷新頻率,術語叫做幀率
可前往官方文檔觀看pygame.time.Clock的更多細節,
根據上面的思路,修改app.py后如下
import pygame FPS=60 # 游戲幀率 WINWIDTH = 600 # 窗口寬度 WINHEIGHT = 900 # 窗口高度 pygame.init() # pygame初始化,必須有,且必須在開頭 # 創建主窗體 clock=pygame.time.Clock() # 用於控制循環刷新頻率的對象 win=pygame.display.set_mode((WINWIDTH,WINHEIGHT)) while True: clock.tick(FPS) # 控制循環刷新頻率,每秒刷新FPS對應的值的次數 pygame.display.update()
此時運行app.py,就可以得到一個最最最基礎的窗口了,
2- 優化
最后,還有一個比較重要的問題,此時窗口的關閉按鈕很容易出bug(卡死)
一般需要程序去重新實現這個窗口關閉功能,需要在循環體內添加如下代碼
# 獲取所有事件 for event in pygame.event.get(): if event.type == pygame.QUIT: # 判斷當前事件是否為點擊右上角退出鍵 pygame.quit() sys.exit() # 需要提前 import sys
本階段最后app.py如下
import pygame import sys FPS=60 # 游戲幀率 WINWIDTH = 600 # 窗口寬度 WINHEIGHT = 900 # 窗口高度 pygame.init() # pygame初始化,必須有,且必須在開頭 # 創建主窗體 clock=pygame.time.Clock() # 用於控制循環刷新頻率的對象 win=pygame.display.set_mode((WINWIDTH,WINHEIGHT)) while True: # 獲取所有事件 for event in pygame.event.get(): if event.type == pygame.QUIT: # 判斷當前事件是否為點擊右上角退出鍵 pygame.quit() sys.exit() clock.tick(FPS) # 控制循環刷新頻率,每秒刷新FPS對應的值的次數 pygame.display.update()
到這里,基礎窗口就完成了~
二、玩家飛機實現
本節主要實現一個基本的,可以鍵盤控制方向移動的玩家飛機
0 - 分析與初步實現
- 創建玩家飛機需要知道哪些?
1、父控件(在該控件上繪制飛機)
2、飛機坐標(x,y)
3、飛機的圖像(圖像文件地址) - 玩家飛機需要實現哪些方法?
1、移動到指定坐標
2、繪制玩家飛機
基於上述分析,新建plane.py
文件,內容如下
import pygame PLANEIMG="img/huaplane.png" class Plane(): def __init__(self,master,x,y,img_path): self._master=master # 父控件 self.image=pygame.image.load(img_path) # 飛機圖像 # 飛機位置-坐標 self.x=x self.y=y # 移動飛機到指定位置 def move(self,x,y): self.x+=x self.y+=y # 繪制飛機 def draw(self): self._master.blit(self.image,(self.x,self.y))
在surface對象上繪制圖像方法:
在surface對象(a)上位置為(x,y)的地方繪制另一個surfae對象(img)a.blit(img,(x,y))
1 - 界面繪制飛機
- 要在界面上展示飛機,十分簡單,只需在
app.py
中添加三行代碼
# 在開頭添加 from plane import Plane # 在創建完窗體win實例之后添加(開始循環之前) plane=Plane(win,200,600) # 在循環體中添加(在clock.tick(FPS)語句之前) plane.draw()
- 但是此時界面上展示的只是一個靜態的飛機圖像,無法鍵盤運動,需要添加通過鍵盤控制飛機移動功能
修改app.py
循環體部分代碼如下
while True: # 獲取所有事件 for event in pygame.event.get(): if event.type == pygame.QUIT: # 判斷當前事件是否為點擊右上角退出鍵 pygame.quit() sys.exit() if event.type == pygame.KEYDOWN: if event.key==pygame.K_LEFT or event.key == ord('a'): plane.move(-1,0) if event.key==pygame.K_RIGHT or event.key == ord('d'): plane.move(1,0) if event.key==pygame.K_UP or event.key == ord('w'): plane.move(0,-1) if event.key==pygame.K_DOWN or event.key == ord('s'): plane.move(0,1) plane.draw() clock.tick(FPS) # 控制循環刷新頻率,每秒刷新FPS對應的值的次數 pygame.display.update()
運行
app.py
之前,需要在項目文件夾(app.py
所在文件夾)中新建img文件夾,並把所有需要用到的圖片素材拷貝到img文件夾中,圖片素材可以從本人github下載,鏈接見本文開頭
然后運行app.py
即可
但是此時仍然有幾個問題:
- 1、飛機移動后,移動前的圖像仍然存在在界面上,沒有清除
- 2、飛機移動十分卡頓(體驗感不好,需要不停地點擊方向鍵)
- 3、飛機可以移動出主界面的邊界
2 - 優化
- 解決問題1十分簡單,只需要循環體中每次繪制界面時清空背景(或者說叫重新繪制背景)
- 針對問題二,需要有變量在方向鍵按下時記錄飛機移動方向,方向鍵抬起時重置飛機移動方向(或者說停止移動)
這里面有個細節問題可以思考,如果玩家先按下左方向鍵,然后按下右方向鍵,之后松開左方向鍵,最后松開右方向鍵,該如何處理飛機運動
所以方向鍵抬起時要檢查當前移動方向是否與抬起的方向鍵相吻合,吻合則重置,不吻合說明玩家在其他移動方向狀態上,此時不應該重置
- 針對問題三,先判斷移動后是否在邊界里,再移動飛機
最終優化后
plane.py
代碼如下
import pygame PLANEIMG="img/huaplane.png" PLANESIZE=90 # 飛機對象直徑(近似圓形) class Plane(): def __init__(self,master,x,y,img_path): self._master=master # 父控件 self.image=pygame.image.load(img_path) # 飛機圖像 # 飛機位置-坐標 self.x=x self.y=y # 移動飛機到指定位置 def move(self,x,y): if 0<=self.x+PLANESIZE/2+x<=self._master.get_width(): self.x+=x if 0<=self.y+PLANESIZE/2+y<=self._master.get_height(): self.y+=y # 繪制飛機 def draw(self): self._master.blit(self.image,(self.x,self.y))
app.py
代碼如下
import pygame import sys from plane import Plane COLORS={ "bg":(0, 0, 0) # 背景顏色 } FPS=60 # 游戲幀率 WINWIDTH = 600 # 窗口寬度 WINHEIGHT = 900 # 窗口高度 MOVESTEP=5 # 移動速度 pygame.init() # pygame初始化,必須有,且必須在開頭 # 創建主窗體 clock=pygame.time.Clock() # 用於控制循環刷新頻率的對象 win=pygame.display.set_mode((WINWIDTH,WINHEIGHT)) plane=Plane(win,200,600) mx,my=0,0 # 記錄移動方向 while True: win.fill(COLORS["bg"]) # 獲取所有事件 for event in pygame.event.get(): if event.type == pygame.QUIT: # 判斷當前事件是否為點擊右上角退出鍵 pygame.quit() sys.exit() if event.type == pygame.KEYDOWN: if event.key==pygame.K_LEFT or event.key == ord('a'): mx=-1 if event.key==pygame.K_RIGHT or event.key == ord('d'): mx=1 if event.key==pygame.K_UP or event.key == ord('w'): my=-1 if event.key==pygame.K_DOWN or event.key == ord('s'): my=1 if event.type == pygame.KEYUP: if event.key==pygame.K_LEFT or event.key == ord('a'): if mx==-1: mx=0 if event.key==pygame.K_RIGHT or event.key == ord('d'): if mx==1: mx=0 if event.key==pygame.K_UP or event.key == ord('w'): if my==-1: my=0 if event.key==pygame.K_DOWN or event.key == ord('s'): if my==1: my=0 plane.move(mx*MOVESTEP,my*MOVESTEP) plane.draw() clock.tick(FPS) # 控制循環刷新頻率,每秒刷新FPS對應的值的次數 pygame.display.update()
然后運行app.py
就可以操控我們的戰斗稽了
三、實現發射子彈功能
本節主要實現上一節的基礎飛機發射子彈功能
先需要創建一個子彈類,來實現子彈的基本功能,
0 - 子彈功能分析
- 子彈的方法與玩家飛機基本一樣,不過子彈不需要圖像,只需要在其坐標上畫圓
子彈類的代碼如下(在plane.py
中,最下面添加如下代碼)
class Bullet(): speed=2 # 速度 color=(255,0,0) # 顏色 radius=5 # 半徑 def __init__(self,master,x,y): self._master=master # 父控件 self.x=x self.y=y # 記錄子彈狀態,初始為True,子彈失效(超出邊界或者碰到敵機)時為False self.on=True # 更新子彈位置,移動子彈 def update(self): self.y-=self.speed if self.y<=0: #超出邊界 self.on=False # 繪制飛機 def draw(self): pygame.draw.circle(self._master, self.color, (self.x,self.y), self.radius)
在Surface對象上畫圓的方法:pygame.draw.circle(Surface, color, pos, radius, width=0)
Draws a circular shape on the Surface.
The pos argument is the center of the circle, and radius is the size.
The width argument is the thickness to draw the outer edge.
If width is zero then the circle will be filled.
翻譯(附加了本人補充):
在Surface對象上繪制圓形。
pos參數(可以是個二元組)是圓心坐標,radius參數是半徑大小,color參數是指定的顏色。
width參數是繪制外緣的厚度。
如果width為零,則圓將被填充成指定顏色。
如果width不為零,則會繪制出一個指定顏色的圓環(圓環內部無顏色填充)。
更多繪制形狀方法可前往官方文檔觀看,https://www.pygame.org/docs/ref/draw.html#pygame.draw.circle
1 - 玩家發射子彈
- 玩家飛機需要實現功能
控制發射的頻率(多久發一顆)
管理發射出去的子彈(更新位置與繪制)
子彈失效時清除
添加完這些功能之后,Plane類的代碼如下
class Plane(): firedelay=15 # 發射子彈時間間隔 def __init__(self,master,x,y,img_path=PLANEIMG): self._master=master # 父控件 self.image=pygame.image.load(img_path) # 飛機圖像 # 飛機位置-坐標 self.x=x self.y=y self.t=0 self.bullets=[] # 發射的子彈 # 移動飛機 def move(self,x,y): if 0<=self.x+PLANESIZE/2+x<=self._master.get_width(): self.x+=x if 0<=self.y+PLANESIZE/2+y<=self._master.get_height(): self.y+=y # 繪制飛機 def draw(self): self._master.blit(self.image,(self.x,self.y)) # 發射子彈 def fire(self): self.t+=1 if self.t>=self.firedelay: self.t=0 # 子彈初始坐標 bx=self.x+int(self.image.get_width()/2) by=self.y bullet=Bullet(self._master,bx,by) self.bullets.append(bullet) # 更新子彈位置,清除失效的子彈 def update_bullets(self): survive=[] for b in self.bullets: b.update() if b.on: survive.append(b) self.bullets=survive # 繪制子彈 def draw_bullets(self): for b in self.bullets: b.draw()
2 - 主界面中調用飛機發射子彈方法
最后還需要在app.py
中添加這樣幾行代碼
# 在while循環中,繪制飛機-plane.draw()后面添加 plane.fire() plane.update_bullets() plane.draw_bullets()
運行app.py,可以看到界面上的飛機能夠發射子彈了
本階段最后代碼如下app.py
import pygame import sys from plane import Plane COLORS={ "bg":(0, 0, 0) # 背景顏色 } FPS=60 # 游戲幀率 WINWIDTH = 600 # 窗口寬度 WINHEIGHT = 900 # 窗口高度 MOVESTEP=5 # 移動速度 pygame.init() # pygame初始化,必須有,且必須在開頭 # 創建主窗體 clock=pygame.time.Clock() # 用於控制循環刷新頻率的對象 win=pygame.display.set_mode((WINWIDTH,WINHEIGHT)) plane=Plane(win,200,600) mx,my=0,0 # 記錄移動方向 while True: win.fill(COLORS["bg"]) # 獲取所有事件 for event in pygame.event.get(): if event.type == pygame.QUIT: # 判斷當前事件是否為點擊右上角退出鍵 pygame.quit() sys.exit() if event.type == pygame.KEYDOWN: if event.key==pygame.K_LEFT or event.key == ord('a'): mx=-1 if event.key==pygame.K_RIGHT or event.key == ord('d'): mx=1 if event.key==pygame.K_UP or event.key == ord('w'): my=-1 if event.key==pygame.K_DOWN or event.key == ord('s'): my=1 if event.type == pygame.KEYUP: if event.key==pygame.K_LEFT or event.key == ord('a'): if mx==-1: mx=0 if event.key==pygame.K_RIGHT or event.key == ord('d'): if mx==1: mx=0 if event.key==pygame.K_UP or event.key == ord('w'): if my==-1: my=0 if event.key==pygame.K_DOWN or event.key == ord('s'): if my==1: my=0 plane.move(mx*MOVESTEP,my*MOVESTEP) plane.draw() plane.fire() plane.update_bullets() plane.draw_bullets() clock.tick(FPS) # 控制循環刷新頻率,每秒刷新FPS對應的值的次數 pygame.display.update()
plane.py
#usr/bin/env python #-*- coding:utf-8- -*- import pygame PLANEIMG="img/huaplane.png" PLANESIZE=90 # 飛機對象直徑(近似圓形) class Plane(): firedelay=15 # 發射子彈時間間隔 def __init__(self,master,x,y,img_path=PLANEIMG): self._master=master # 父控件 self.image=pygame.image.load(img_path) # 飛機圖像 # 飛機位置-坐標 self.x=x self.y=y self.t=0 self.bullets=[] # 發射的子彈 # 移動飛機 def move(self,x,y): if 0<=self.x+PLANESIZE/2+x<=self._master.get_width(): self.x+=x if 0<=self.y+PLANESIZE/2+y<=self._master.get_height(): self.y+=y # 繪制飛機 def draw(self): self._master.blit(self.image,(self.x,self.y)) # 發射子彈 def fire(self): self.t+=1 if self.t>=self.firedelay: self.t=0 # 子彈初始坐標 bx=self.x+int(self.image.get_width()/2) by=self.y bullet=Bullet(self._master,bx,by) self.bullets.append(bullet) # 更新子彈位置,清除失效的子彈 def update_bullets(self): survive=[] for b in self.bullets: b.update() if b.on: survive.append(b) self.bullets=survive # 繪制子彈 def draw_bullets(self): for b in self.bullets: b.draw() class Bullet(): speed=2 # 速度 color=(255,0,0) # 顏色 radius=5 # 半徑 def __init__(self,master,x,y): self._master=master # 父控件 self.x=x self.y=y # 記錄子彈狀態,初始為True,子彈失效(超出邊界或者碰到敵機)時為False self.on=True # 更新子彈位置,移動子彈 def update(self): self.y-=self.speed if self.y<=0: self.on=False # 繪制飛機 def draw(self): pygame.draw.circle(self._master, self.color, (self.x,self.y), self.radius)
四、簡單敵機(基礎滑稽)實現
0 - 敵機功能分析
- 創建敵機的方法與玩家飛機基本一樣
1、父控件
2、飛機坐標(一般的,x坐標隨機,y坐標初始為0)
3、飛機的圖像(在類變量中寫死,不讓玩家傳遞參數) - 敵機需要實現的方法也與玩家飛機大體相似
1、更新位置(默認下移)
2、繪制敵機
不同:敵機類需要速度變量speed,以控制下移速度。
基於上述分析,可以新建huaji.py
文件,內容如下
import pygame # 敵機 - 滑稽 class Huaji(): imgpath="img/smallhuaji.png" speed=2 def __init__(self,master,x,y=0): self._master=master # 父控件 self.image=pygame.image.load(self.imgpath) self.x=x self.y=y # 移動敵機,更新敵機位置 def update(self): self.y+=self.speed def draw(self): self._master.blit(self.image,(self.x,self.y))
為了能夠在界面上展示這個滑稽,我們還需要在app.py
中添加這樣幾行代碼
(本段代碼在下一小節有敵機管理類中,需要刪掉或者注釋掉)
# 在開頭添加 from huaji import Huaji # 在創建完plane實例-plane=Plane(win,100,100)之后,while循環之前添加 huaji=Huaji(win,100) # 在while循環中,飛機移動plane.move(mx*MOVESTEP,my*MOVESTEP)-之前 huaji.update() huaji.draw()
然后運行app.py
,就可以在界面中,看到我們的戰斗稽和敵軍滑稽。
但是使用這個方法,此時界面上只會有一個敵軍滑稽,當我們需要比較多
敵機的時候(能夠指定數量和位置),本方法就顯得很不方便了。
1 - 管理敵機
為了實現對敵機的管理,我們需要新建一個管理類
這個管理類應該實現這些方法:
- 按照一定的規則生成滑稽
- 當滑稽失效時清理掉(超出邊界或者被子彈擊中時)
- 更新滑稽位置,繪制滑稽
管理類代碼如下(下方代碼粘貼到huaji.py里面就可以)
SMALLSIZE=50 # 本行添加到文件開頭 class HuajiManager(): cd=15 # 生成滑稽的時間間隔 def __init__(self,master): self._master=master self.t=0 self.huajilist=[] def generate(self): self.t+=1 if self.t%self.cd==0: x=random.randint(0,self._master.get_width()-SMALLSIZE) # SMALLSIZE 為Huaji對象的直徑 ji=Huaji(self._master,x,0) self.huajilist.append(ji) def update(self): survive=[] for huaji in self.huajilist: huaji.update() if huaji.inWindow(): survive.append(huaji) self.huajilist=survive def draw(self): for huaji in self.huajilist: huaji.draw()
為了能夠在界面上展示滑稽,我們還需要在app.py
中添加這樣幾行代碼
# 在開頭添加 from huaji import HuajiManager # 在創建完plane實例之后,while循環之前添加 hm=HuajiManager(win) # 在while循環中,飛機移動plane.move(mx*MOVESTEP,my*MOVESTEP)-之前 hm.generate() hm.update() hm.draw()
2 - 碰撞檢測與處理
碰撞分兩種
- 一種是子彈與敵機碰撞
- 一種是自己的戰斗稽與敵機碰撞
我們首先來處理第一種 - 子彈與敵機碰撞
原理:子彈圓心與敵機圓心之間的距離小於等於子彈的半徑(為了方便,可忽略不計)+敵機半徑時
子彈與敵機相撞
實現步驟:
1,敵機需要添加一個狀態變量(lives),用於判斷是否被擊中或者死亡。
在Huaji構造器中添加一行代碼self.lives=1
Huaji對象添加獲取圓心坐標和獲取半徑方法
def get_center_XY(self): # 獲取圓心坐標 return (self.x+SMALLSIZE/2,self.y+SMALLSIZE/2) def get_radius(self): # 獲取半徑 return SMALLSIZE/2
2 碰撞檢查與處理
Bullet對象添加檢查是否擊中敵機的方法,並更改狀態
def get_distance(self,xy): x,y=xy return math.sqrt(math.pow(self.x-x,2)+math.pow(self.y-y,2)) def check_hit(self,huajilist): for huaji in huajilist: if huaji.lives>0 and huaji.inWindow(): d=self.get_distance(huaji.get_center_XY()) if d<=huaji.get_radius(): # 擊中,更新狀態 self.on=False huaji.lives-=1
Plane對象增加一個統一處理的方法,並清空已擊中敵機的子彈
def check_all_hit(self,huajilist): survive=[] for b in self.bullets: b.check_hit(huajilist) if b.on: survive.append(b) self.bullets=survive
最后,修改一下huaji.py中HuajiManager的update方法,用於清空已被擊中的飛機
def update(self): survive=[] for huaji in self.huajilist: huaji.update() # if huaji.inWindow():修改前 if huaji.inWindow() and huaji.lives>0: #修改之后 survive.append(huaji) self.huajilist=survive
3主界面中調用這些新加的方法
在app.py中添加一行代碼(while循環體中,hm.generate()語句之前)plane.check_all_hit(hm.huajilist)
戰斗稽與敵機碰撞邏輯與上面相似
1 戰斗稽需要添加一個狀態變量(lives),用於判斷是否被敵機撞到。
在Plane構造器中添加一行代碼self.lives=1
(也可以設置多一點)
添加檢查碰撞方法
def get_distance(self,xy): x,y=xy cx=self.x+PLANESIZE/2 cy=self.y+PLANESIZE/2 return math.sqrt(math.pow(cx-x,2)+math.pow(cy-y,2)) def check_crash(self,huajilist): for huaji in huajilist: if huaji.lives>0 and huaji.inWindow(): d=self.get_distance(huaji.get_center_XY()) if d<=PLANESIZE/2+huaji.get_radius(): # hit self.lives-=1 huaji.lives-=1
2主界面中調用這些新加的方法並在飛機生命值為0時退出游戲
在app.py中添加一行代碼(while循環體中,plane.check_all_hit(hm.huajilist)語句之后)
plane.check_crash(hm.huajilist) if plane.lives<=0: break
到這里,滑稽大戰游戲基礎版就實現了
代碼可以在github上下載
https://github.com/BigShuang/From-simple-to-Huaji/tree/master/huaji%20game
運行截圖如下

其實還有很多可以優化的地方,且功能與我在b站的投稿成品還有很大差距,他日有緣再更新。