想体验一下的话,文末给出百度网盘网址和密码供大家获取完整源码(没有打包,请大家运行main.py这个脚本,也是第一次分享代码,出了问题请私信我,唯一的外部依赖库是pygame,用pip install pygame即可安装,python版本建议3.8,因为我就是3.8)
大家好,我是CSDN新人,也是一名在校大一学生,第一次写博客,有什么写的不好的地方,或者做的不对的地方,希望大家能指出来,十分感谢!
最近接触了Pygame游戏编程,十分感兴趣,学习了一本相关书籍以及查阅了Pygame的官网https://www.pygame.org的资料,花了一周的时间写出了这个飞机大战的游戏。主体玩法相对完整,模拟的是逝去的手游帝王《雷霆战机》,算是对它多舛命运的哀悼吧。
话不多说,先看效果
什么,你问我素材哪里找的?万能的淘宝在这时候总不会让你失望!所有音频和图片素材均来自于淘宝。
这里跟大家分享几个制作过程中遇到的几个技术细节的解决方法,以及优化措施:
1.帧率不同步问题。
我用pygame.time.Clock对象来控制整个游戏的最大帧率为60。为什么说是最大呢?不是因为性能原因导致游戏帧率真的低于60(虽然在某些过程优化之前的确出现过卡的低于60帧的情况),是因为某些动画过程的帧率本就不是60,比如飞机爆炸的动画,就那么9帧,若60Hz播放的话就显得太快了,起初,我想到用重复的图片Surface对象填充这些应该重复的几帧,使得切换到下一帧的速度没那么快,但重复的图片对象会导致成倍数的内存占用,这是我不能忍受的。于是我用了一个FramNum对象,将重复的次数用正整数num表示,并增加计数点属性,当运行到下一次就增加一个计数点,若计数点达到了num,则运行下一帧。于是一个完整的动画帧Frame对象被设计成由FramNum对象组成,能够成倍数地减少在动画帧方面内存使用。
以下实现代码看不懂没关系,因为注释不清楚,而且有一些额外的设计没有被提及。
import pygame
class FrameNum:
def __init__(self, frame:pygame.Surface, num:int):
self.frame = frame
self.num = num
def __getframe(self):
return self.__frame
def __setframe(self, other):
self.__frame = other
frame = property(__getframe, __setframe)
def __getnum(self):
return self.__num
def __setnum(self, other):
self.__num = other
num = property(__getnum, __setnum)
def __mul__(self, other):
self.num *= other
return self
def __imul__(self, other):
self.num *= other
return self
def __getitem__(self, item):
if item == 0:
return self.frame
elif item == 1:
return self.num
else:
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 condition
self.this_code = leisureframe
self.thiscondition = framelist[self.this_code]
# this condition len
self.thiscdlen = len(self.thiscondition)
# this frame
self.thisframe = 0
# this frame num
self.thisfnum = self.thiscondition[self.thisframe][1]
self.thisgone = 0
self.image = self.thiscondition[self.thisframe][0]
self.next_code = self.LASTCIRCLE
self.sticked = False
# 得到本状态的虚拟帧数目
self.leisureframe = leisureframe
def set_stick(self):
self.next_code = self.LASTSTICK
def unsetonce(self):
self.next_code = self.LASTCIRCLE
def 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 = 0
self.thisframe = 0
self.this_code = condition_code
self.thiscdlen = len(self.thiscondition)
self.updateframe()
# 跳转condition代码
def set_next(self, next_code):
self.next_code = next_code
def set_condition(self, this_code):
self.this_code = this_code
def circleupdate(self):
# 若达到这一帧的最后一个
if self.thisgone >= self.thisfnum:
# 进入下一帧
self.nextframe()
# 若超过最后一帧
if self.thisframe >= self.thiscdlen:
self.thisframe = 0
self.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 = True
def 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_code
self.thisframe = new_thisframe
self.thiscondition = self.framelist[self.this_code]
self.updateframe()
self.thiscdlen = len(self.thiscondition)
self.thisgone = 0
self.next_code = self.LASTSTICK if sticked else self.LASTCIRCLE
self.sticked = False
def nextframe(self):
self.thisgone = 0
self.thisframe += 1
def updateframe(self):
self.image = self.thiscondition[self.thisframe][0]
self.thisfnum = self.thiscondition[self.thisframe][1]
def go(self):
self.thisgone += 1
def 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 mypoint
class 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 = frame
self.image = self.frame.image
self.rect = pygame.Rect(*position.position, *self.image.get_size())
self.speed = speed
self.accelerate = accelerate
class LevelUSBG(UnStopBG):
def draw(self, screen:pygame.Surface):
lastrect = self.rect
for i in range(screen.get_width()//self.rect.width + 1):
screen.blit(self.image, lastrect)
lastrect.topleft = lastrect.topright
if 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 = 0
elif self.speed < 0:
if lastrect.left <= screen.get_width():
screen.blit(self.image, lastrect)
if self.rect.right <= 0:
self.rect.left = 0
else:
return
self.rect = self.rect.move(self.speed, 0)
self.speed += self.accelerate
class 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.bottomleft
if 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 = 0
elif self.speed < 0:
if lastrect.bottom <= screen.get_height():
screen.blit(self.image, lastrect)
if self.rect.bottom <= 0:
self.rect.top = 0
else:
return
self.rect = self.rect.move(0, self.speed)
self.speed += self.accelerate
5.玩家飞机操纵逻辑和飞机姿态转换逻辑
玩家飞机控制的最开始版本为飞机只有一个正面的姿态,没有侧翻的姿态,并且飞机不能加速,只能匀速运动,控制键为"wasd"系(w向前,a向左,s向后,d向右)
但这样玩起来十分枯燥,手感单一。于是想到模拟飞机控制的实际情景,为飞机添加加速度。如当玩家按下a时,飞机就像左加速,当玩家松开时,飞机就减速直到速度为0。而加速有一个最大值。并且要保证飞机不飞出窗口,由于编写飞机的过程中多次涉及到“限制”这个概念,于是想到写Bound类系:
import pygame
import sys
# bound族都有limit协议
class BoundUnit:
NEGATIVE_INFINITY = b'\x11'
POSITIVE_INFINITY = b'\x01'
def __init__(self, floor=NEGATIVE_INFINITY, ceiling=POSITIVE_INFINITY):
self.__floor = floor
self.__ceiling = ceiling
self.__room = (floor, ceiling)
def __getf(self):
return self.__floor
def __getc(self):
return self.__ceiling
def __getroom(self):
return self.__room
floor = property(__getf)
ceiling = property(__getc)
room = property(__getroom)
def floorlimit(self, other, eq=True):
if self.floor == self.NEGATIVE_INFINITY:
return True
if eq:
return self.floor <= other
else:
return self.floor < other
def ceilinglimit(self, other, eq=True):
if self.ceiling == self.POSITIVE_INFINITY:
return True
if eq:
return other <= self.ceiling
else:
return other < self.ceiling
def 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.floor
elif not self.ceilinglimit(other,eqc):
other = self.ceiling
return other
def __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.bounds
def __radd__(self, other):
return self + other
def __iadd__(self, other):
self.bounds += set(other)
return self
def limit(self, other):
for i in self.bounds:
if not i.limit(other):
return False
return True
class BoundLine:
# 切割数轴的有序bound
# 必须为升序
def __init__(self, boundpoints):
bounds = []
self.boundlen = len(boundpoints) + 1
if 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 = bounds
def detect_section(self, other, righteq=True):
for i in range(self.boundlen):
if self.bounds[i].limit(other, not righteq, righteq):
return i
class 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 + rightdetect
class 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.room
self.__topbottom = bound_2.room
self.bound_1 = bound_1
self.bound_2 = bound_2
self.__bound = (self.bound_1, self.bound_2)
def __getl(self):
return self.__left
def __getr(self):
return self.__right
def __gett(self):
return self.__top
def __getb(self):
return self.__bottom
def __getlr(self):
return self.__leftright
def __gettb(self):
return self.__topbottom
left = 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 globalsInit
class ControlCenter:
'''
manage move and location
'''
def __init__(self, engine:engine.Engine):
# engine only manage speed changing and accelerate changing
self.engine = engine
# 预加载screen_bound 免得每次都重复生成
self.space_b = globalsInit.screen_bound
def configure(self, plane):
# 初始位置
self.plane = plane
self.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.rect
def control(self):
self.engine.drive()
self.updatelocation()
self.plane.rect = self.rect
def updatelocation(self):
if not self.space_b.leftlimit(self.rect.left, True):
self.rect.left = self.space_b.left
elif not self.space_b.rightlimit(self.rect.right, True):
self.rect.right = self.space_b.right
if not self.space_b.toplimit(self.rect.top, True):
self.rect.top = self.space_b.top
elif not self.space_b.bottomlimit(self.rect.bottom, True):
self.rect.bottom = self.space_b.bottom
self.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 = plane
self.rect = plane.rect
class 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 plane
class Engine(mymove.Mymove):
# only for speed and accelerate(manally, just for player)
# 目前在Engine类中硬编码所有飞船都应遵守的协议
ACCE_x = 1
ACCE_y = 0.2
ACCE_F = 0.5
LEFT = 0
LEISURE = 1
RIGHT = 2
STAGE_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 = speedbound
self.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] = 0
if self.speed[1] > 0:
self.speed[1] -= self.ACCE_F
if self.speed[1] < 0:
self.speed[1] = 0
elif self.speed[1] < 0:
self.speed[1] += self.ACCE_F
if self.speed[1] > 0:
self.speed[1] = 0
if 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] = 0
if self.speed[0] > 0:
self.speed[0] -= self.ACCE_F
if self.speed[0] < 0:
self.speed[0] = 0
elif self.speed[0] < 0:
self.speed[0] += self.ACCE_F
if self.speed[0] > 0:
self.speed[0] = 0
self.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_TIME
self.sleep_time = self.SLEEP_TIME
self.sleeping = False
self.acceing = False
def copy(self):
return Enemy_Engine()
class Sa_1_Engine(Enemy_Engine):
ACCE_X = 1
ACCE_Y = 0.1
ACCE_F = 0.5
ACCE_TIME = 300
SLEEP_TIME = 100
def 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 = True
self.acceing = False
self.accelerate = [0,0]
self.last_acce = now
# 睡眠中
elif self.sleeping:
if self.speed[0] > 0:
self.speed[0] -= self.ACCE_F
if self.speed[0] < 0:
self.speed[0] = 0
elif self.speed[0] < 0:
self.speed[0] += self.ACCE_F
if self.speed[0] > 0:
self.speed[0] = 0
if self.speed[1] > 0:
self.speed[1] -= self.ACCE_F
if self.speed[1] < 0:
self.speed[1] = 0
elif self.speed[1] < 0:
self.speed[1] += self.ACCE_F
if self.speed[1] > 0:
self.speed[1] = 0
now = pygame.time.get_ticks()
if now - self.last_acce >= self.SLEEP_TIME:
self.sleeping = False
self.last_acce = now
def w_acce(self):
self.accelerate[1] = -self.ACCE_Y
def a_acce(self):
self.accelerate[0] = -self.ACCE_X
def s_acce(self):
# can use to drive show
self.accelerate[1] = self.ACCE_Y
def d_acce(self):
self.accelerate[0] = self.ACCE_X
def 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? --来自百度网盘超级会员v1的分享
使用注意:仅包含自己手写python代码和doc文档,依赖库pygame请用pip install pygame安装。也建议各位和我一样用Pycharm跑。因为技术有限,不好意思放在github,也没打包,就是纯纯的剁手代码。
链接永远有效,请放心食用!
|