Pygame游戏飞机大战《星野守望》
程序地址:網頁鏈接
注意:目前只支持win系統。
游戲做的不完整,只能用來體驗體驗。
最近接觸了Pygame游戲編程,十分感興趣,學習了一本相關書籍以及查閱了Pygame的官網https://www.pygame.org的資料,花了一周的時間寫出了這個飛機大戰的游戲。主體玩法相對完整,模擬的是逝去的手游帝王《雷霆戰機》,算是對它多舛命運的哀悼吧。
話不多說,先看效果
什么,你問我素材哪里找的?萬能的淘寶在這時候總不會讓你失望!所有音頻和圖片素材均來自于淘寶。
這里跟大家分享幾個制作過程中遇到的幾個技術細節的解決方法,以及優化措施:
1.幀率不同步問題。
我用pygame.time.Clock對象來控制整個游戲的最大幀率為60。為什么說是最大呢?不是因為性能原因導致游戲幀率真的低于60(雖然在某些過程優化之前的確出現過卡的低于60幀的情況),是因為某些動畫過程的幀率本就不是60,比如飛機爆炸的動畫,就那么9幀,若60Hz播放的話就顯得太快了,起初,我想到用重復的圖片Surface對象填充這些應該重復的幾幀,使得切換到下一幀的速度沒那么快,但重復的圖片對象會導致成倍數的內存占用,這是我不能忍受的。于是我用了一個FramNum對象,將重復的次數用正整數num表示,并增加計數點屬性,當運行到下一次就增加一個計數點,若計數點達到了num,則運行下一幀。于是一個完整的動畫幀Frame對象被設計成由FramNum對象組成,能夠成倍數地減少在動畫幀方面內存使用。
以下實現代碼看不懂沒關系,因為注釋不清楚,而且有一些額外的設計沒有被提及。
import pygameclass FrameNum:def __init__(self, frame:pygame.Surface, num:int):self.frame = frameself.num = numdef __getframe(self):return self.__framedef __setframe(self, other):self.__frame = otherframe = property(__getframe, __setframe)def __getnum(self):return self.__numdef __setnum(self, other):self.__num = othernum = property(__getnum, __setnum)def __mul__(self, other):self.num *= otherreturn selfdef __imul__(self, other):self.num *= otherreturn selfdef __getitem__(self, item):if item == 0:return self.frameelif item == 1:return self.numelse:raise ValueError('indexvalue not 0 or 1!')class Frame:'like [[(frame1,num1),(frame2,num2)],[...]]'LASTCIRCLE= b'\x01'LASTSTICK = b'\x00'def __init__(self, framelist, leisureframe=0):self.framelist = framelist# this conditionself.this_code = leisureframeself.thiscondition = framelist[self.this_code]# this condition lenself.thiscdlen = len(self.thiscondition)# this frameself.thisframe = 0# this frame numself.thisfnum = self.thiscondition[self.thisframe][1]self.thisgone = 0self.image = self.thiscondition[self.thisframe][0]self.next_code = self.LASTCIRCLEself.sticked = False# 得到本狀態的虛擬幀數目self.leisureframe = leisureframedef set_stick(self):self.next_code = self.LASTSTICKdef unsetonce(self):self.next_code = self.LASTCIRCLEdef update(self):# 若為循環播放if not self.sticked:if self.next_code == self.LASTCIRCLE:self.circleupdate()elif self.next_code == self.LASTSTICK:self.stickupdate()else:self.onceupdate()def changecondition(self, condition_code):self.thiscondition = self.framelist[condition_code]self.thisgone = 0self.thisframe = 0self.this_code = condition_codeself.thiscdlen = len(self.thiscondition)self.updateframe()# 跳轉condition代碼def set_next(self, next_code):self.next_code = next_codedef set_condition(self, this_code):self.this_code = this_codedef circleupdate(self):# 若達到這一幀的最后一個if self.thisgone >= self.thisfnum:# 進入下一幀self.nextframe()# 若超過最后一幀if self.thisframe >= self.thiscdlen:self.thisframe = 0self.thisgone = 0# 圖像變化self.updateframe()# 下一個虛幀self.go()def stickupdate(self):if self.thisframe < self.thiscdlen - 1:if self.thisgone >= self.thisfnum - 1:self.nextframe()self.updateframe()else:self.go()elif self.thisframe == self.thiscdlen - 1:if self.thisgone < self.thisfnum - 1:self.go()else:if not self.sticked:self.sticked = Truedef onceupdate(self):if not self.thisframe == self.next_code:if self.thisframe < self.thiscdlen - 1:if self.thisgone >= self.thisfnum:self.nextframe()self.updateframe()self.go()else:if self.thisgone >= self.thisfnum:self.changecondition(self.next_code)else:self.go()def set_certain(self, new_thisframe, new_condition_code, sticked=False):self.this_code = new_condition_codeself.thisframe = new_thisframeself.thiscondition = self.framelist[self.this_code]self.updateframe()self.thiscdlen = len(self.thiscondition)self.thisgone = 0self.next_code = self.LASTSTICK if sticked else self.LASTCIRCLEself.sticked = Falsedef nextframe(self):self.thisgone = 0self.thisframe += 1def updateframe(self):self.image = self.thiscondition[self.thisframe][0]self.thisfnum = self.thiscondition[self.thisframe][1]def go(self):self.thisgone += 1def copy(self):return Frame(self.framelist, self.leisureframe)2.按鈕制作
按鈕是和鼠標放上和鼠標點擊時間交互的精靈對象,我用兩個旗幟onCovered和onClicked來保存鼠標放上和點擊按鈕的狀態,并且鼠標放上將使按鈕變得更亮,移開后按鈕又恢復原狀。點擊按鈕又是另一種動畫幀。有了以上Frame對象管理幀,這個整體變得不難實現。
3.游戲開始前的背景
游戲開始前的背景是用29幀動畫重復播放實現的
4.游戲背景循環滾動設計
正式進入游戲,希望能模擬飛機戰場的推進,背景向下方不斷滾動,并循環播放,此代碼保證一次最多只渲染兩個背景圖片,當滾動出界時回到原處重復過程,能做到無縫銜接
最終游戲背景為如下VerticalUSBG對象
import pygame from myGametools import mySprite from myGametools import mypointclass BackGround(mySprite.MyMoveSprite):def draw(self, screen:pygame.Surface):screen.blit(self.image, self.rect)self.update()class UnStopBG(BackGround):def __init__(self, frame, position:mypoint.Mypoint, speed=0, accelerate=0):self.frame = frameself.image = self.frame.imageself.rect = pygame.Rect(*position.position, *self.image.get_size())self.speed = speedself.accelerate = accelerateclass LevelUSBG(UnStopBG):def draw(self, screen:pygame.Surface):lastrect = self.rectfor i in range(screen.get_width()//self.rect.width + 1):screen.blit(self.image, lastrect)lastrect.topleft = lastrect.toprightif self.speed > 0:if 0 < self.rect.left < self.rect.width:screen.blit(self.image, (self.rect.left-self.rect.width, self.rect.top))elif self.rect.left >= self.rect.width:self.rect.left = 0elif self.speed < 0:if lastrect.left <= screen.get_width():screen.blit(self.image, lastrect)if self.rect.right <= 0:self.rect.left = 0else:returnself.rect = self.rect.move(self.speed, 0)self.speed += self.accelerateclass VerticalUSBG(UnStopBG):def draw(self, screen: pygame.Surface):lastrect = self.rect.copy()for i in range(screen.get_height() // self.rect.height + 1):screen.blit(self.image, lastrect)lastrect.topleft = lastrect.bottomleftif self.speed > 0:if 0 < self.rect.top < self.rect.height:screen.blit(self.image, (self.rect.left, self.rect.top-self.rect.height, self.rect.width, self.rect.height))elif self.rect.top >= self.rect.height:self.rect.top = 0elif self.speed < 0:if lastrect.bottom <= screen.get_height():screen.blit(self.image, lastrect)if self.rect.bottom <= 0:self.rect.top = 0else:returnself.rect = self.rect.move(0, self.speed)self.speed += self.accelerate5.玩家飛機操縱邏輯和飛機姿態轉換邏輯
玩家飛機控制的最開始版本為飛機只有一個正面的姿態,沒有側翻的姿態,并且飛機不能加速,只能勻速運動,控制鍵為"wasd"系(w向前,a向左,s向后,d向右)
但這樣玩起來十分枯燥,手感單一。于是想到模擬飛機控制的實際情景,為飛機添加加速度。如當玩家按下a時,飛機就像左加速,當玩家松開時,飛機就減速直到速度為0。而加速有一個最大值。并且要保證飛機不飛出窗口,由于編寫飛機的過程中多次涉及到“限制”這個概念,于是想到寫Bound類系:
import pygameimport sys # bound族都有limit協議class BoundUnit:NEGATIVE_INFINITY = b'\x11'POSITIVE_INFINITY = b'\x01'def __init__(self, floor=NEGATIVE_INFINITY, ceiling=POSITIVE_INFINITY):self.__floor = floorself.__ceiling = ceilingself.__room = (floor, ceiling)def __getf(self):return self.__floordef __getc(self):return self.__ceilingdef __getroom(self):return self.__roomfloor = property(__getf)ceiling = property(__getc)room = property(__getroom)def floorlimit(self, other, eq=True):if self.floor == self.NEGATIVE_INFINITY:return Trueif eq:return self.floor <= otherelse:return self.floor < otherdef ceilinglimit(self, other, eq=True):if self.ceiling == self.POSITIVE_INFINITY:return Trueif eq:return other <= self.ceilingelse:return other < self.ceilingdef limit(self, other, eqf=True, eqc=True):return self.floorlimit(other, eqf) and self.ceilinglimit(other, eqc)def setin(self, other, eqf=True, eqc=True):if not self.floorlimit(other, eqf):other = self.floorelif not self.ceilinglimit(other,eqc):other = self.ceilingreturn otherdef __getitem__(self, item):return self.room[item]class BoundGroup:# 限制群def __init__(self, *groups):self.bounds = set(groups)def __add__(self, other):return self.bounds + other.boundsdef __radd__(self, other):return self + otherdef __iadd__(self, other):self.bounds += set(other)return selfdef limit(self, other):for i in self.bounds:if not i.limit(other):return Falsereturn Trueclass BoundLine:# 切割數軸的有序bound# 必須為升序def __init__(self, boundpoints):bounds = []self.boundlen = len(boundpoints) + 1if self.boundlen == 1:bounds.append(BoundUnit(BoundUnit.NEGATIVE_INFINITY, BoundUnit.POSITIVE_INFINITY))else:bounds.append(BoundUnit(BoundUnit.NEGATIVE_INFINITY, boundpoints[0]))for i in range(self.boundlen-2):if not boundpoints[i] < boundpoints[i+1]:raise ValueError('please garantee its ascending order')bounds.append(BoundUnit(boundpoints[i], boundpoints[i+1]))bounds.append(BoundUnit(boundpoints[-1], BoundUnit.POSITIVE_INFINITY))self.bounds = boundsdef detect_section(self, other, righteq=True):for i in range(self.boundlen):if self.bounds[i].limit(other, not righteq, righteq):return iclass BoundLineSymmetry(BoundLine):def __init__(self, boundpoints):bounds = []boundpoints.reverse()negboundpoints = [-i for i in boundpoints]self.lefthalfbounds = BoundLine(negboundpoints)for i in negboundpoints:bounds.append(i)boundpoints.reverse()self.righthalfbounds = BoundLine(boundpoints)if boundpoints[0]:bounds.append(boundpoints[0])for i in boundpoints[1:]:bounds.append(i)super().__init__(bounds)def detect_section(self, other, righteq=True):leftdetect = self.lefthalfbounds.detect_section(other, not righteq)rightdetect = self.righthalfbounds.detect_section(other, righteq)return leftdetect + rightdetectclass Boundlftb:# 目前不支持改變def __init__(self, bound_1: bound.BoundUnit, bound_2: bound.BoundUnit):self.__left = bound_1[0]self.__right = bound_1[1]self.__top = bound_2[0]self.__bottom = bound_2[1]self.__leftright = bound_1.roomself.__topbottom = bound_2.roomself.bound_1 = bound_1self.bound_2 = bound_2self.__bound = (self.bound_1, self.bound_2)def __getl(self):return self.__leftdef __getr(self):return self.__rightdef __gett(self):return self.__topdef __getb(self):return self.__bottomdef __getlr(self):return self.__leftrightdef __gettb(self):return self.__topbottomleft = property(__getl)right = property(__getr)top = property(__gett)bottom = property(__getb)leftright = property(__getlr)topbottom = property(__gettb)def leftlimit(self, other, eq=True):return self.bound_1.floorlimit(other, eq)def rightlimit(self, other, eq=True):return self.bound_1.ceilinglimit(other, eq)def xlimit(self, other, eql=True, eqr=True):return self.bound_1.limit(other, eql, eqr)def toplimit(self, other, eq=True):return self.bound_2.floorlimit(other, eq)def bottomlimit(self, other, eq=True):return self.bound_2.ceilinglimit(other, eq)def ylimit(self, other, eqt=True, eqb=True):return self.bound_2.limit(other, eqt, eqb)def limit(self, x, y, xeqf=True, xeqc=True, yeqf=True, yeqc=True):return self.xlimit(x, xeqf, xeqc) and self.ylimit(y, yeqf, yeqc)def __getitem__(self, item):return self.__bound[item]總之它們封裝了可用于限制速度和飛機位置的代碼,并在飛機的實現中用到
對于飛機的姿態問題也就迎刃而解了,就根據其向左或向右的速度區間,利用以上的數軸對稱區間劃分BuondLineSemmetry類來判斷飛機速度所在區間,并確定將信息返回給飛機,使得飛機具備相應的狀態stage屬性,在根據該屬性調整顯示圖片
6.飛機發射武器
武器可以看作子彈的群組,于是從pygame.sprite.Sprite繼承了Bullet子彈類,從pygame.sprite.Group繼承了Gun武器類。武器能間隔一段時間在飛機的固定的一些相對位置生成子彈,因此武器也需要和飛機綁定配置在一起。于是飛機和武器又是has-a關系。注意到飛機的組成如此復雜,為了避免寫出有極多屬性的飛機類,于是用ControlCenter控制中心類來封裝飛機的運動,并用Engine類封裝飛機運動的控制:
# controlcenter.py import random import pygame from . import engine from classes import bound, boundlrtb from init import constants from init import globalsInitclass ControlCenter:'''manage move and location'''def __init__(self, engine:engine.Engine):# engine only manage speed changing and accelerate changingself.engine = engine# 預加載screen_bound 免得每次都重復生成self.space_b = globalsInit.screen_bounddef configure(self, plane):# 初始位置self.plane = planeself.rect = pygame.Rect(int(constants.SCREEN_SIZE[0] / 2 - plane.rect.width),int(constants.BeginLocCEnter - plane.rect.height / 2),plane.rect.width,plane.rect.height)plane.rect = self.rectdef control(self):self.engine.drive()self.updatelocation()self.plane.rect = self.rectdef updatelocation(self):if not self.space_b.leftlimit(self.rect.left, True):self.rect.left = self.space_b.leftelif not self.space_b.rightlimit(self.rect.right, True):self.rect.right = self.space_b.rightif not self.space_b.toplimit(self.rect.top, True):self.rect.top = self.space_b.topelif not self.space_b.bottomlimit(self.rect.bottom, True):self.rect.bottom = self.space_b.bottomself.rect.left += self.engine.speed[0]self.rect.top += self.engine.speed[1]def copy(self):return ControlCenter(self.engine.copy())class Enemy_Controlcenter(ControlCenter):def configure(self, plane):self.plane = planeself.rect = plane.rectclass Sa_1_Controlcenter(Enemy_Controlcenter): # 敵人的中央控制器def control(self):if self.rect.bottom < 0:self.engine.a_acce()else:super().control()# engine.py import pygame, random from pygame.locals import * from myGametools import mymove from classes import boundlrtb, bound from classes.planes import planeclass Engine(mymove.Mymove):# only for speed and accelerate(manally, just for player)# 目前在Engine類中硬編碼所有飛船都應遵守的協議ACCE_x = 1ACCE_y = 0.2ACCE_F = 0.5LEFT = 0LEISURE = 1RIGHT = 2STAGE_P = bound.BoundLineSymmetry([1, 4, 6, 10])(STAGE_LEFT3, STAGE_LEFT2, STAGE_LEFT1,STAGE_LEISURE,STAGE_RIGHT1, STAGE_RIGHT2, STAGE_RIGHT3) = (1, 2, 3,4,5, 6, 7)def __init__(self, speedbound:boundlrtb.Boundlftb, speed=None, accelerate=None):super().__init__(speed, accelerate)self.speed_b = speedboundself.stage = self.STAGE_P.detect_section(self.speed[0])def w_acce(self):if self.speed_b.ylimit(self.speed[1]):if self.speed[1] > 0:self.setyAccelerate(-(self.ACCE_y + self.ACCE_F))else:self.setyAccelerate(-self.ACCE_y)def a_acce(self):if self.speed_b.xlimit(self.speed[0]):if self.speed[0] > 0:self.setxAccelerate(-(self.ACCE_x + self.ACCE_F))else:self.setxAccelerate(-self.ACCE_x)def s_acce(self):if self.speed_b.ylimit(self.speed[1]):if self.speed[1] < 0:self.setyAccelerate(self.ACCE_y + self.ACCE_F)else:self.setyAccelerate(self.ACCE_y)def d_acce(self):if self.speed_b.xlimit(self.speed[0]):if self.speed[0] < 0:self.setxAccelerate(self.ACCE_x + self.ACCE_F)else:self.setxAccelerate(self.ACCE_x)def drive(self):keys = pygame.key.get_pressed()if keys[K_w] and not keys[K_s]:self.w_acce()elif keys[K_s] and not keys[K_w]:self.s_acce()else:if self.accelerate[1]:self.accelerate[1] = 0if self.speed[1] > 0:self.speed[1] -= self.ACCE_Fif self.speed[1] < 0:self.speed[1] = 0elif self.speed[1] < 0:self.speed[1] += self.ACCE_Fif self.speed[1] > 0:self.speed[1] = 0if keys[K_a] and not keys[K_d]:self.a_acce()elif keys[K_d] and not keys[K_a]:self.d_acce()else:if self.accelerate[0]:self.accelerate[0] = 0if self.speed[0] > 0:self.speed[0] -= self.ACCE_Fif self.speed[0] < 0:self.speed[0] = 0elif self.speed[0] < 0:self.speed[0] += self.ACCE_Fif self.speed[0] > 0:self.speed[0] = 0self.updatespeed()self.updatestage()def updatestage(self):self.stage = self.STAGE_P.detect_section(self.speed[0])def updatespeed(self):self.speed[0] += self.accelerate[0]self.speed[1] += self.accelerate[1]self.speed[0] = self.speed_b.bound_1.setin(self.speed[0])self.speed[1] = self.speed_b.bound_2.setin(self.speed[1])def copy(self):return Engine(self.speed_b, self.speed[:], self.accelerate[:])class Enemy_Engine(mymove.Mymove): # 敵人的引擎def __init__(self):super().__init__()self.last_acce = pygame.time.get_ticks()self.acce_time = self.ACCE_TIMEself.sleep_time = self.SLEEP_TIMEself.sleeping = Falseself.acceing = Falsedef copy(self):return Enemy_Engine()class Sa_1_Engine(Enemy_Engine):ACCE_X = 1ACCE_Y = 0.1ACCE_F = 0.5ACCE_TIME = 300SLEEP_TIME = 100def drive(self):# 睡眠結束super().updatespeed()if not self.acceing and not self.sleeping:d = random.randint(1, 4)if d == 1:self.w_acce()if d == 2:self.a_acce()if d == 3:self.s_acce()if d == 4:self.d_acce()self.acceing = True# 加速中elif self.acceing and not self.sleeping:now = pygame.time.get_ticks()if now - self.last_acce >= self.ACCE_TIME:self.sleeping = Trueself.acceing = Falseself.accelerate = [0,0]self.last_acce = now# 睡眠中elif self.sleeping:if self.speed[0] > 0:self.speed[0] -= self.ACCE_Fif self.speed[0] < 0:self.speed[0] = 0elif self.speed[0] < 0:self.speed[0] += self.ACCE_Fif self.speed[0] > 0:self.speed[0] = 0if self.speed[1] > 0:self.speed[1] -= self.ACCE_Fif self.speed[1] < 0:self.speed[1] = 0elif self.speed[1] < 0:self.speed[1] += self.ACCE_Fif self.speed[1] > 0:self.speed[1] = 0now = pygame.time.get_ticks()if now - self.last_acce >= self.SLEEP_TIME:self.sleeping = Falseself.last_acce = nowdef w_acce(self):self.accelerate[1] = -self.ACCE_Ydef a_acce(self):self.accelerate[0] = -self.ACCE_Xdef s_acce(self):# can use to drive showself.accelerate[1] = self.ACCE_Ydef d_acce(self):self.accelerate[0] = self.ACCE_Xdef copy(self):return Sa_1_Engine()這樣,飛機就只有controlcenter屬性和weapon屬性了
7.敵人飛機生成
敵人飛機生成涉及到實例對象產生的問題,和炮彈生成一樣,用一個群組Group類的子類封裝敵人的每隔一段時間隨機生成的邏輯。為保證每個敵人完全獨立,為每個組件都增加了一個copy函數,使之在內存中復制,從而不相互干擾
8.生命值的計算和顯示邏輯。
如果沒有生命值外框,又怎么知道扣了多少生命值呢?于是我設計了HpFrameBar(繼承自pygame.sprite.Group)來封裝外框和實條。
用Hp類來封裝數值的運算代碼(很簡單,只有加和減)
玩家生命值條和外框的顯示位置可以硬編碼在左下角,但敵人的生命顯示只能隨敵人位置變化。于是也增加了HpFrameBar于一個敵人實例的綁定函數configure,且隨敵人位置移動而移動
由于篇幅限制,沒有討論子彈和飛機爆炸播放邏輯和音效播放邏輯的實現。
技術介紹完。
目前游戲沒有實現的是游戲結束邏輯,更豐富的戰斗體驗以及游戲進度保存。因為最近要學JavaScript/HTML/CSS了,所以這個游戲做到這樣能玩就收手了,各位還請海涵。
源代碼:
鏈接: https://pan.baidu.com/s/1SwBsF-bjoBbHkN_LY4Ddqw
提取碼: mnfc?
。
鏈接永遠有效,請放心食用!
總結
以上是生活随笔為你收集整理的Pygame游戏飞机大战《星野守望》的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一加7 nfc 无限重启
- 下一篇: 自动发送寄件通知教程