Pgzero飞机大战

提示

关于Pgzero更基础的东西可以参考我之前的一篇文章

利用pgzero做一个接球的小游戏 - 菜缤的世界 CairBin's Blog

问题

解决的问题&特点:

  • 在这个项目中我自己手写了gameObject基类代替了pgzero中的Actor类,并且该项目中的所有对象均继承此类
  • 之所以这样写,是因为我发现Actor无法对图片进行缩放,我利用pygame.transform.scale解决了这个问题

待思考的问题:

  • 这个项目最没搞明白的地方就是精确碰撞。原本是想通过pygame的mask.overlap来实现精确碰撞,但无论我怎么改获取的offset坐标差值元组的纵横坐标都为0,如果有什么方法请大佬们在文章下放或私信告诉我一声
  • 我根据坐标判断两个矩形重叠来实现碰撞检测,但图片周围留白的地方也会触发,因此该方式不是一种精确的检测
  • 敌人爆炸没有爆炸特效,原因是我忘写了(doge)。要实现也很简单,我的思路是爆炸时修改为爆炸图片,最好是gif类型(我并不喜欢用pgzero自带的动画类, 感觉用起来都怪怪的),然后一定时间后效果消失

代码

资源

资源来自网络 (非商业使用),我打包上传到百度云、CSDN资源了,或者直接从GitHub上获取

百度云 提取码:6d1r
CSDN资源 需要积分,如果你想“赞助”一下作者,可以通过该方式下载(doge)

define.py

文件define.py定义了一些常量(并非真正的常量),便于代码维护

# 窗口参数
WINDOW_TITLE = 'plane war'
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 500

BG_COLOR = 'white'

# 方块尺寸 长/宽
BLOCK_SIZE = 50

# 飞机血量
PLANE_HP = 3

# 子弹参数
BULLET_SPEED = 10
BULLET_SIZE = (10, 10)
BULLET_MOVE_LENGTH = -20
BULLET_AGRES = 1

# 敌人参数
ENEMY_SPEEED = 5
ENEMY_CREATE_TIME = 30

# 血量大小
HEART_SIZE = 30

gameObject.py

文件gameObject.py用于创建所有对象都具有的共同属性和方法(父类)

from typing import Tuple
from numpy import rec
import pygame
from enum import Enum

# 坐标锚点
class posAnchor(Enum):
    TOP_LEFT = 0        # 左上方
    TOP = 1             # 正上方
    TOP_RIGHT = 2       # 右上方
    LEFT = 3            # 左
    CENTER = 4          # 中心
    RIGHT = 5           # 右
    BOTTOM_LEFT = 6     # 左下方
    BOTTOM = 7          # 正下方
    BOTTOM_RIGHT = 8    # 右下方

# 游戏基类
class gameObject:
    def __init__(self, img, init_pos:tuple, size:tuple, screen = None, pos_anchor=posAnchor.TOP_LEFT) -> None:
        self.img = img          # pgzero图片对象
        self.size = size        # 大小,元组(宽, 高)
        self.screen = screen    # screen
        self.destroy = False    # 用于检测是否被销毁,spiritGroup中的checkDestroy方法检测该属性,为True将从spiritGroup中被移除

        self.__anchor = pos_anchor                                  # 锚
        self.__pos = self.__getTruePos(init_pos, self.__anchor)     # 绘制坐标
        self.__userPos = init_pos                                   # 锚坐标

        # 图片容器对象,需导入pygame
        self.obj = pygame.transform.scale(self.img, self.size)

    # 初始化, 在pgzero update()中调用
    def update(self, screen):
        self.initScreen(screen)
        self.draw()

    # 初始化screen
    def initScreen(self, screen):
        self.screen = screen

    # 绘制
    def draw(self):
        self.screen.blit(self.obj, self.__pos)

    # 更改位置(要求锚点,防止坐标混乱)
    def changePos(self, pos: tuple, pos_anchor):
        self.__pos = self.__getTruePos(pos, pos_anchor)
        self.__userPos = pos

    # 获取宽度
    def width(self):
        return self.size[0]

    # 获取高度
    def height(self):
        return self.size[1]

    # 获取大小尺寸
    def getSize(self):
        return self.size

    # 获取横坐标(锚点坐标)
    def x(self):
        return self.__userPos[0]

    # 获取纵坐标(锚点坐标)
    def y(self):
        return self.__userPos[1]

    # 获取左上角坐标
    def getTopLeftPos(self):
        return self.__pos

    # 获取rect
    def getRect(self):
        return self.obj.get_rect()

    # 获取mask
    def getMask(self):
        return pygame.mask.from_surface(self.obj)

    # 获取锚点
    def getAnchor(self):
        return self.__anchor

    # 更改销毁属性
    def setDestroy(self, choose:bool):
        self.destroy = choose

    # 获得销毁属性
    def getDestroy(self)->bool:
        return self.destroy

    # 碰撞检测
    def collisionDetection(self, other):
        this_pos = self.getTopLeftPos()
        other_pos = other.getTopLeftPos()
        
        this_center_pos = (this_pos[0]+self.width()/2, this_pos[1]+self.height()/2)
        other_center_pos = (other_pos[0]+other.width()/2, other_pos[1]+other.height()/2)

        # 判断矩形相交
        if abs(this_center_pos[0] - other_center_pos[0]) <= (self.width() + other.width())/2 and abs(this_center_pos[1] - other_center_pos[1]) <= (self.height() + other.height())/2:
            return True
        else:
            return False

        def move(*args):
            pass


    # 根据锚和给出坐标,获取左上角坐标
    def __getTruePos(self, pos: tuple, pos_anchor):
        if pos_anchor == posAnchor.TOP_LEFT:
            return pos
        elif pos_anchor == posAnchor.TOP:
            return (pos[0]-self.size[0]/2, pos[1])
        elif pos_anchor == posAnchor.TOP_LEFT:
            return (pos[0]-self.size[0], pos[1])
        elif pos_anchor == posAnchor.LEFT:
            return (pos[0], pos[1]-self.size[1]/2)
        elif pos_anchor == posAnchor.CENTER:
            return (pos[0]-self.size[0]/2, pos[1]-self.size[1]/2)
        elif pos_anchor == posAnchor.RIGHT:
            return (pos[0]-self.size[0], pos[1]-self.size[1]/2)
        elif pos_anchor == posAnchor.BOTTOM_LEFT:
            return (pos[0], pos[1]-self.size[1])
        elif pos_anchor == posAnchor.BOTTOM:
            return (pos[0]-self.size[0]/2, pos[1]-self.size[1])
        elif pos_anchor == posAnchor.BOTTOM_RIGHT:
            return (pos[0]-self.size[0], pos[1]-self.size[1])
        else:
            return None

# 精灵组
class spiritGroup:
    def __init__(self) -> None:
        self.__spiritList = []

    # ----------------------列表操作

    # 追加
    def append(self, spirit: gameObject):
        self.__spiritList.append(spirit)

    # 索引
    def index(self, g_obj: gameObject):
        return self.__spiritList.index(g_obj)

    # 插入
    def insert(self, num):
        self.__spiritList.insert(num)

    # 移除
    def remove(self, g_obj: gameObject):
        self.__spiritList.remove(g_obj)

    # 根据索引移除
    def removeAsIndex(self, num):
        self.remove(self.index(num))

    # 元素个数
    def size(self):
        return len(self.__spiritList)

    # 清空
    def clear(self):
        self.__spiritList.clear()

    # ----------------------运算符重载

    # []
    def __getitem__(self, index) -> gameObject:
        return self.__spiritList[index]

    # in
    def __contains__(self, elem):
        return 

    # +
    def __add__(self, other):
        new_g_obj = spiritGroup()
        for i in self.__spiritList:
            new_g_obj.append(i)
        for i in other:
            new_g_obj.append(i)

        return new_g_obj
    
    # ----------------------基本操作

    # 初始化(draw)
    def update(self, screen):
        for i in self.__spiritList:
            i.update(screen)

    # 绘制
    def draw(self):
        for i in self.__spiritList:
            i.draw()

    # 碰撞检测
    def collisionDetection(self, other, collided = None, args=()):
        for e in self.__spiritList:
            for i in other:
                if e.collisionDetection(i) == True:
                    if collided == None:
                        e.setDestroy(True)
                        i.setDestroy(True)
                    else:
                        collided(e, i, *args)

    # 移动
    def move(self, *args):
        for i in self.__spiritList:
            i.move(*args)

    # 检测元素是否应该被销毁(移除),并执行
    def checkDestroy(self):
        for i in self.__spiritList:
            if i.getDestroy():
                temp = i
                self.remove(i)
                del i

actors.py

文件actors.py定义游戏内具体创建对象的类,例如飞机类,子弹类等

import define
import gameObject

# 子弹类
class bullet(gameObject.gameObject):
    def __init__(self, img, init_pos: tuple, size: tuple, screen=None, pos_anchor=...) -> None:
        super().__init__(img, init_pos, size, screen, pos_anchor)
        self.speed = define.BULLET_MOVE_LENGTH
        self.agres = define.BULLET_AGRES            #子弹威力

    def move(self, *args):
        self.changePos((self.x(), self.y()+self.speed), self.getAnchor())
        pos = self.getTopLeftPos()
        if pos[1]+self.height() < 0:
            self.setDestroy(True)


# 飞机类
class plane(gameObject.gameObject):
    def __init__(self, img, init_pos: tuple, size: tuple, screen=None, pos_anchor=...) -> None:
        super().__init__(img, init_pos, size, screen, pos_anchor)
        self.hp = define.PLANE_HP   # 玩家飞机血量

    def move(self, *args):
        # 边界判断
        pos = list((args[0][0]-self.width()/2, args[0][1]-self.height()/2))

        if pos[0] <0:
            pos[0] = 0
        if pos[0]+self.width() > define.WINDOW_WIDTH:
            pos[0] = define.WINDOW_WIDTH-self.width()
        
        if pos[1]<0:
            pos[1] = 0
        if pos[1]+self.height() > define.WINDOW_HEIGHT:
            pos[1] = define.WINDOW_HEIGHT-self.height()

        self.changePos(tuple(pos), gameObject.posAnchor.TOP_LEFT)

    # 开火,参数为子弹pgzero图片对象
    def fire(self, bullet_img):
        pos = self.getTopLeftPos()
        bullet_pos = (pos[0]+self.width()/2, pos[1])
        bull = bullet(bullet_img, bullet_pos, define.BULLET_SIZE, screen = self.screen, pos_anchor=gameObject.posAnchor.BOTTOM)
        return bull


# 敌人类
class enemy(gameObject.gameObject):
    def __init__(self, img, hp, init_pos: tuple, size: tuple, screen=None, pos_anchor=...) -> None:
        super().__init__(img, init_pos, size, screen, pos_anchor)
        self.hp = hp
        self.speed = define.ENEMY_SPEEED
        self.destroy = False

    def move(self, *args):
        if self.destroy == False:
            self.changePos((self.x(), self.y()+self.speed),
                        gameObject.posAnchor.CENTER)
            pos = self.getTopLeftPos()
            if pos[1] > define.WINDOW_HEIGHT:
                self.setDestroy(True)

# 血量类, 用于显示hp
class heart(gameObject.gameObject):
    def __init__(self, img, init_pos: tuple, size: tuple, screen=None, pos_anchor=...) -> None:
        super().__init__(img, init_pos, size, screen, pos_anchor)

main.py

main.py程序的主要入口在此文件中

import random
import pgzrun
import define
import actors
import pygame, sys

# 窗口基本配置, pgzero会调用此处的变量来设置窗口
TITLE = define.WINDOW_TITLE
WIDTH = define.WINDOW_WIDTH
HEIGHT = define.WINDOW_HEIGHT

# 图片映射字典
images_dict = {
    'plane': images.plane,
    'bullet': images.bullet,
    'enemy': images.enemy,
    'heart': images.heart
}

# 子弹组
bullet_list = actors.gameObject.spiritGroup()
# 敌人组
enemy_list = actors.gameObject.spiritGroup()
# 血量显示组
heart_list = actors.gameObject.spiritGroup()

# 角色飞机
plane = actors.plane(images_dict['plane'], (define.WINDOW_WIDTH/2, define.WINDOW_HEIGHT*3/4),
                     (define.BLOCK_SIZE, define.BLOCK_SIZE), pos_anchor=actors.gameObject.posAnchor.CENTER)

# 生成敌人时间间隔
enemy_time = 0
# 游戏得分
player_scores = 0

# -------------------------------------------------游戏控制----------------------------------- #
# 随机生成敌人
def creatEnemy():
    global enemy_time
    if enemy_time <= 0:
        # 此处血量随机1-2
        enemy_list.append(actors.enemy(images_dict['enemy'], random.randint(1,2),
            (random.randint(5, define.WINDOW_WIDTH-define.BLOCK_SIZE), 0), (define.BLOCK_SIZE, define.BLOCK_SIZE), 
            pos_anchor=actors.gameObject.posAnchor.TOP_LEFT))
        enemy_time = define.ENEMY_CREATE_TIME
    else:
        enemy_time -= 1

# 显示血量
def showHp():
    hp = plane.hp
    i = 0
    heart_list.clear()
    while i<hp:
        he = actors.heart(images_dict['heart'], (i*define.HEART_SIZE, 0), (define.HEART_SIZE,
                          define.HEART_SIZE), pos_anchor=actors.gameObject.posAnchor.TOP_LEFT)
        heart_list.append(he)
        i+=1

# 得分
def getScores():
    global player_scores
    player_scores += 1

# 游戏结束
def gameOver():
    global player_scores
    pygame.quit()
    print('Game Over')
    print('Your scores are {}'.format(player_scores))
    sys.exit()

# 子弹碰撞敌机的回调函数
# 默认的碰撞检测直接将对象删除,如果给敌人设置血量根据子弹威力来扣血则需要外部提供回调函数
def bulletCollidedEnemy(bullet: actors.bullet, enemy: actors.enemy, getScore):
    enemy.hp -= bullet.agres
    bullet.setDestroy(True)         #销毁子弹
    # 根据血量判断敌人是否销毁
    if enemy.hp <= 0:
        enemy.setDestroy(True)
        getScore()                 # 得分

# 敌机与玩家碰撞
# 玩家飞机没有精灵组,所以直接写碰撞事件在Update()中调用
def enemyCollidedPlane():
    # 遍历并检测碰撞
    for i in enemy_list:
        if i.collisionDetection(plane):
            plane.hp -= 1           #扣血
            i.setDestroy(True)
            # 检测是否血量归零以结束游戏
            if plane.hp == 0:
                gameOver()

# -------------------------------------------------pgzero内置----------------------------------- #

def update():
    plane.update(screen)

    bullet_list.update(screen)
    bullet_list.move()

    creatEnemy()
    enemy_list.update(screen)
    enemy_list.move()
    enemy_list.checkDestroy()


    showHp()
    heart_list.update(screen)

    bullet_list.collisionDetection(enemy_list, collided=bulletCollidedEnemy, args=(getScores,))
    bullet_list.checkDestroy()

    enemyCollidedPlane()

def draw():
    screen.fill(define.BG_COLOR)

    plane.draw()
    bullet_list.draw()
    enemy_list.draw()
    heart_list.draw()

def on_mouse_move(pos):
    plane.move(pos)

def on_key_down(key):
    # 空格飞机发射子弹
    if key == keys.SPACE:
        bullet_list.append(plane.fire(images_dict['bullet']))

# -------------------------------------------------运行-----------------------------------#
pgzrun.go()

开源

Github:

CairBin/PlaneWar: A game under python(pgzero) (github.com)

最后修改:2022 年 07 月 27 日
如果觉得我的文章对你有用,请随意赞赏