> 技术文档 > 解读超级马里奥pygame游戏设计_马里奥python代码

解读超级马里奥pygame游戏设计_马里奥python代码


开源代码 

mx0c/super-mario-python: super mario in python and pygame

一、初步运行

1.终端搭建conda
conda create -n pygame python=3.8 -yconda activate iotpip install pygame scipy

补充:介绍两个包+json

pygame

Pygame Front Page — pygame v2.6.0 documentation

scipy

SciPy

为什么用到scipy:主要是用于实现高斯模糊效果,这在 super-mario-python/classes/GaussianBlur.py 文件,在游戏开发里,高斯模糊常被用于营造特定的视觉效果,像模糊背景以突出前景元素,或者模拟一些特殊场景,比如梦境、雾气等。在这个超级马里奥游戏项目中,高斯模糊或许是用于实现暂停界面的背景模糊效果,增强游戏的视觉表现力。

JSON(JavaScript Object Notation)

是一种轻量级的数据交换格式,它基于 JavaScript 的一个子集,但现在已经成为一种独立于编程语言的数据格式。JSON 以人类可读的文本形式来存储和传输数据,易于人们阅读和编写,同时也易于机器解析和生成。JSON 数据由键值对组成,使用大括号 {} 表示对象,方括号 [] 表示数组。

在这个超级马里奥游戏程序中,JSON 文件起到了非常重要的作用,主要用于存储和配置游戏的各种数据。

  • scale 表示该帧图像的缩放因子,这里 scale 为 2 意味着图像会被放大到原来的 2 倍。

  • deltaTime 字段指定了动画每帧之间的时间间隔,单位可能是游戏中的时间单位(例如帧或毫秒)。这里设置为 10,表示每 10 个时间单位切换一帧动画。

  • colorKey 字段用于指定图像的透明颜色。在某些图形处理中,可以通过设置颜色键来使特定颜色变为透明。这里 colorKeynull,表示不使用颜色键,即图像没有特定的透明颜色。

2.配置pycharm中解析解释器

二、代码详细讲解

1.文件结构

super-mario-python├── compile.py #将超级马里奥游戏的主程序 main.py 以及相关的配置文件、图片、声音等资源打包成一个 Windows 可执行文件├── main.py├── img #存放游戏的图像资源├── traits #包含各种角色行为特性的实现├── classes #包含游戏的各种类,如动画类、相机类、碰撞检测类等├── sprites #游戏精灵(sprites)相关属性的 JSON 文件├── entities #定义了游戏中的各种实体,如金币、蘑菇怪、马里奥等。├── sfx #存放游戏的音效└── levels #关卡    ├── Level1-1.json    └── Level1-2.json

游戏编程中的精灵(sprite)是什么意思呢?有什么作用呢? - laoshoucun - 博客园

2.main.py

    while not menu.start:        menu.update() mario = Mario(0, 0, level, screen, dashboard, sound)    clock = pygame.time.Clock()    while not mario.restart:        pygame.display.set_caption(\"Super Mario running with {:d} FPS\".format(int(clock.get_fps())))         if mario.pause:            mario.pauseObj.update()        else:            level.drawLevel(mario.camera)            dashboard.update()            mario.update()        pygame.display.update()        clock.tick(max_frame_rate)    return \'restart\'
  • while 循环,只要菜单的 start 属性为 False,就不断调用 menu.update() 方法来更新菜单界面。

  • mario:创建一个 Mario 对象,代表游戏中的马里奥角色,初始位置为 (0, 0),并传入关卡、游戏窗口、仪表盘和音效对象。

  • clock = pygame.time.Clock():创建一个 pygame 的时钟对象,用于控制游戏的帧率。

  • while not mario.restart:开始一个主游戏循环,只要马里奥的 restart 属性为 False,就不断更新游戏状态。

  • pygame.display.set_caption():设置游戏窗口的标题,显示当前游戏的帧率。

  • 如果马里奥的 pause 属性为 True,则调用 mario.pauseObj.update() 方法来更新暂停界面。

  • 否则,调用 level.drawLevel(mario.camera) 方法绘制当前关卡的画面,调用 dashboard.update() 方法更新仪表盘信息,调用 mario.update() 方法更新马里奥角色的状态。

3.class类文件

1)animation.py

Animation 类在超级马里奥游戏中主要负责管理和更新游戏角色或物体的动画效果。它可以让角色或物体在不同状态下展示出不同的动画帧,使游戏画面更加生动。

  • idleSprite:角色处于闲置状态时显示的图像。

  • airSprite:角色在空中时显示的图像。

2)camera.py

模拟游戏中的相机,控制玩家在游戏世界中看到的视野范围。

简单的相机跟随系统,相机可以根据指定实体的位置进行移动。相机的位置在初始化时设置,并且在 move 方法中根据实体的位置进行更新。相机的位置会被转换为实际的像素坐标,以便在游戏中进行渲染。

  • move 方法用于根据关联实体的位置来更新相机的位置。

  • xPosFloat = self.entity.getPosIndexAsFloat().x:调用 entity 对象的 getPosIndexAsFloat 方法获取实体在 x 轴上的浮点位置。

  • if 10 < xPosFloat < 50::判断实体的 x 轴位置是否在 10 到 50 之间。如果满足条件,则执行下面的操作。

  • self.pos.x = -xPosFloat + 10:当实体的 x 轴位置在 10 到 50 之间时,更新相机的 x 轴位置。这里使用 -xPosFloat + 10 来计算新的位置,目的是让相机随着实体的移动而反向移动,保证实体处于屏幕的合适位置。

  • self.x = self.pos.x * 32self.y = self.pos.y * 32:根据更新后的 self.pos 重新计算相机在游戏世界中的实际坐标。

class Camera:    def __init__(self, pos, entity):        self.pos = Vec2D(pos.x, pos.y)        self.entity = entity        self.x = self.pos.x * 32        self.y = self.pos.y * 32​    def move(self):        xPosFloat = self.entity.getPosIndexAsFloat().x        if 10 < xPosFloat < 50:            self.pos.x = -xPosFloat + 10        self.x = self.pos.x * 32        self.y = self.pos.y * 32class Mario(EntityBase):#马里奥文件中;EntityBase.py-def getPosIndexAsFloat(self):    def __init__(self, x, y, level, screen, dashboard, sound, gravity=0.8):        super(Mario, self).__init__(x, y, gravity)

3)等级设置 level.py

4)front、dashboard——文字面板

Font 类的主要功能是从精灵表(spritesheet)中加载字体字符的图像,并将每个字符与其对应的图像关联起来,方便在游戏中使用这些字符图像来显示文本。

class Font(Spritesheet):    def __init__(self, filePath, size):        Spritesheet.__init__(self, filename=filePath)        self.chars = \" !\\\"#$%&\'()*+,-./0123456789:;? @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\"        self.charSprites = self.loadFont() ​    def loadFont(self):        font = {}        row = 0        charAt = 0        for char in self.chars:            if charAt == 16: #if charAt == 16::当列号达到 16 时,将列号重置为 0                charAt = 0                row += 1            font.update(               {                    char: self.image_at(                        charAt,                        row,                        2,                        colorkey=pygame.color.Color(0, 0, 0),                        xTileSize=8,                        yTileSize=8                   )               }           )            charAt += 1 #列号加 1,处理下一个字符图像        return font

dashboard

继承自 Font 类,用于在游戏界面上绘制和更新仪表盘信息,如玩家得分、金币数量、关卡名称和游戏时间等。

5)碰撞检测

Collider 的类

其主要功能是处理游戏中实体(如角色)与游戏关卡中瓷砖(tile)之间的碰撞检测,同时也处理实体到达关卡边界的情况。

EntityCollider

这个类的主要功能是检查一个实体(self.entity)与另一个目标实体(target)是否发生碰撞,并确定碰撞发生的具体位置(顶部碰撞还是其他位置)

    def determineSide(self, rect1, rect2):        if (            rect1.collidepoint(rect2.bottomleft)            or rect1.collidepoint(rect2.bottomright)            or rect1.collidepoint(rect2.midbottom)       ):            if rect2.collidepoint(               (rect1.midleft[0] / 2, rect1.midleft[1] / 2)           ) or rect2.collidepoint((rect1.midright[0] / 2, rect1.midright[1] / 2)):                return CollisionState(True, False)            else:                if self.entity.vel.y > 0:                    return CollisionState(True, True)        return CollisionState(True, False)
  • 该方法接收两个矩形参数 rect1rect2,用于确定它们之间的碰撞位置。

  • 首先检查 rect2 的左下角、右下角或底边中点是否在 rect1 内部。如果满足条件,则继续进行下一步判断。

  • 接着检查 rect2 是否包含 rect1 左中位置或右中位置的一半坐标。如果满足条件,则返回一个 CollisionState 对象,其中 isCollidingTrueisTopFalse

  • 如果不满足上述条件,并且 self.entity 在垂直方向上的速度 vel.y 大于 0(表示向下移动),则返回一个 CollisionState 对象,其中 isCollidingTrueisTopTrue,表示是顶部碰撞。

  • 如果以上条件都不满足,则返回一个 CollisionState 对象,其中 isCollidingTrueisTopFalse

6)input.py

处理游戏中的输入事件,包括键盘输入、鼠标输入,以及处理退出和重启游戏的相关事件。

7)高斯模糊 pause暂停键后使用scipy库的高斯函数进行模糊处理

8)sprite、sprites、spritesheet ——图像的处理

超级玛丽是如何做到占用空间如此之小的呢?

img介绍

从提供的代码和配置文件来看,程序用到了以下6个img文件夹下的图像文件,它们各自的用途如下:

1. ./img/tiles.png

在Animations.json里用于创建如CoinBoxcoin等动画精灵的图像。在BackgroundSprites.json里用于创建背景精灵,像skybricksground等。

2. ./img/characters.gif

角色精灵的精灵表

3. ./img/Items.png

在RedMushroom.json里用于创建红色蘑菇(mushroom)这个物品的图像,在ItemAnimations.json里用于创建coin-item的动画图像。

4. ./img/koopas.png

乌龟角色精灵的精灵表。在sprites/Koopa.json中被引用,用于创建koopa-1koopa-2koopa-hiding等库巴角色不同状态的图像。

5. ./img/title_screen.png

用于游戏的菜单和暂停界面。在Pause.py里用于创建暂停界面的相关元素,如点(dot)和灰色点(gray_dot)。在Menu.py里用于创建菜单界面的横幅(menu_banner)、点(menu_dot)等元素。

6. ./img/font.png

显示游戏中的文字,作为字体图像资源。

三者关系

  • Sprite 类封装了单个精灵的基本属性和绘制方法。__init__ 方法用于初始化精灵的图像、碰撞属性、动画和背景重绘标志,

  • Sprites 类用于管理游戏中所有的精灵。它负责从多个 JSON 文件中加载精灵信息,并将这些精灵存储在一个字典中,方便统一管理和访问。

  • Spritesheet 类,用于处理精灵表。精灵表是一种将多个精灵图像合并到一个大图像文件中的技术,Spritesheet 类负责加载精灵表图像,并从精灵表中提取指定位置和大小的精灵图像。

Sprites 类创建并管理 Sprite 对象

Sprites 类在加载精灵信息时,会根据提取的精灵图像和其他属性创建 Sprite 对象,并将这些对象存储在 spriteCollection 字典中。

Sprites 类依赖 Spritesheet

Sprites 类的 loadSprites 方法中,会根据 JSON 文件中指定的精灵表路径创建 Spritesheet 对象,然后使用该对象的 image_at 方法从精灵表中提取精灵图像

class Sprite:    def __init__(self, image, colliding, animation=None, redrawBackground=False):        self.image = image        self.colliding = colliding        self.animation = animation        self.redrawBackground = redrawBackground​    def drawSprite(self, x, y, screen):        dimensions = (x * 32, y * 32)        if self.animation is None:            screen.blit(self.image, dimensions)        else:            self.animation.update()            screen.blit(self.animation.image, dimensions)
  • colliding:一个布尔值,用于指示该精灵是否参与碰撞检测。

  • animation:可选参数,默认为 None,表示精灵的动画对象。如果不为 None,则精灵会有动画效果。

  • dimensions = (x * 32, y * 32) 将网格坐标 (x, y) 转换为像素坐标,每个网格单元的大小为 32x32 像素。

  • 如果 self.animationNone,则直接将 self.image 绘制到屏幕的 dimensions 位置,使用 screen.blit(self.image, dimensions)

  • 如果 self.animation 不为 None,则先调用 self.animation.update() 更新动画状态,然后将动画当前帧的图像 self.animation.image 绘制到屏幕的 dimensions 位置。

    class Spritesheet(object):    def __init__(self, filename):        try:            self.sheet = pygame.image.load(filename)            self.sheet = pygame.image.load(filename)            if not self.sheet.get_alpha(): #self.sheet.get_alpha() 检查图像是否有透明度                self.sheet.set_colorkey((0, 0, 0)) #黑色(RGB 值为 (0, 0, 0))设置为透明色        except pygame.error:            print(\"Unable to load spritesheet image:\", filename)            raise SystemExit#scalingfactor:表示提取的精灵图像的缩放 //网格坐标或像素坐标,取决于 ignoreTileSize 的值    def image_at(self, x, y, scalingfactor, colorkey=None, ignoreTileSize=False,                 xTileSize=16, yTileSize=16):        if ignoreTileSize:            rect = pygame.Rect((x, y, xTileSize, yTileSize))        else:            rect = pygame.Rect((x * xTileSize, y * yTileSize, xTileSize, yTileSize))        image = pygame.Surface(rect.size)        image.blit(self.sheet, (0, 0), rect)        #blit(source,dest=None,special_flags=0)将source参数指定的Surface对象绘制到该对象上。        #dest参数指定绘制的位置。        #dest的值可以是source的左上角坐标,如果传入一个rect对象给dest,那么blit()会使用它的左上角坐标        if colorkey is not None:            if colorkey == -1:                colorkey = image.get_at((0, 0))            image.set_colorkey(colorkey, pygame.RLEACCEL)        return pygame.transform.scale(            image, (xTileSize * scalingfactor, yTileSize * scalingfactor)       )

    class Sprites:    def __init__(self):        self.spriteCollection = self.loadSprites(           [                \"./sprites/Mario.json\",                \"./sprites/Goomba.json\",                \"./sprites/Koopa.json\",                \"./sprites/Animations.json\",                \"./sprites/BackgroundSprites.json\",                \"./sprites/ItemAnimations.json\",                \"./sprites/RedMushroom.json\"           ]       ) #字典​    def loadSprites(self, urlList):#包含多个 JSON 文件路径的列表        resDict = {}#字典        for url in urlList:            with open(url) as jsonData:                data = json.load(jsonData)                mySpritesheet = Spritesheet(data[\"spriteSheetURL\"])                dic = {}                if data[\"type\"] == \"background\":                    for sprite in data[\"sprites\"]:                        try:                            colorkey = sprite[\"colorKey\"]                        except KeyError:                            colorkey = None                        dic[sprite[\"name\"]] = Sprite(                            mySpritesheet.image_at(                                sprite[\"x\"],                                sprite[\"y\"],                                sprite[\"scalefactor\"],                                colorkey,                           ),                            sprite[\"collision\"],                            None,                            sprite[\"redrawBg\"],                       )                    resDict.update(dic)                    continue                elif data[\"type\"] == \"animation\":                    for sprite in data[\"sprites\"]:                        images = []                        for image in sprite[\"images\"]:                            images.append(                                mySpritesheet.image_at(                                    image[\"x\"],                                    image[\"y\"],                                    image[\"scale\"],                                    colorkey=sprite[\"colorKey\"],                               )                           )                        dic[sprite[\"name\"]] = Sprite(                            None,                            None,                            animation=Animation(images, deltaTime=sprite[\"deltaTime\"]),                       )                    resDict.update(dic)                    continue                elif data[\"type\"] == \"character\" or data[\"type\"] == \"item\":                    for sprite in data[\"sprites\"]:                        try:                            colorkey = sprite[\"colorKey\"]                        except KeyError:                            colorkey = None                        try:                            xSize = sprite[\'xsize\']                            ySize = sprite[\'ysize\']                        except KeyError:                            xSize, ySize = data[\'size\']                        dic[sprite[\"name\"]] = Sprite(                            mySpritesheet.image_at(                                sprite[\"x\"],                                sprite[\"y\"],                                sprite[\"scalefactor\"],                                colorkey,                                True,                                xTileSize=xSize,                                yTileSize=ySize,                           ),                            sprite[\"collision\"],                       )                    resDict.update(dic)                    continue        return resDict

try-except Python教学 | 有备无患!详解 Python 异常处理(try-except)_try except函数-CSDN博客

4.traits——马里奥的动作

bounceTrait-弹跳 和 jump-跳

jump:由玩家输入触发,当玩家按下特定按键(如空格键、上方向键等),且实体在地面上(self.entity.onGroundTrue)时,跳跃行为才会启动。

bounce:通常是在实体与其他物体碰撞时触发,特别是当实体从上方碰撞到敌人触发

class bounceTrait:    def __init__(self, entity):        self.vel = 5 #初始化弹跳的垂直速度为 5,这个值决定了实体在弹跳时向上移动的速度        self.jump = False        self.entity = entity​    def update(self):        if self.jump:            self.entity.vel.y = 0            self.entity.vel.y -= self.vel            self.jump = False            self.entity.inAir = True​    def reset(self):        self.entity.inAir = False
  • self.entity.vel.y = 0:将实体的垂直速度 vel.y 重置为 0,这样可以避免之前的垂直速度对弹跳产生影响。

  • self.entity.vel.y -= self.vel:将实体的垂直速度减去 self.vel 的值,因为 self.vel 是正值,所以减去它会使实体获得一个向上的速度,从而实现弹跳效果。

go

管理角色的移动速度、动画更新和绘制

动作和键盘交互
class Input:    def __init__(self, entity):        self.mouseX = 0        self.mouseY = 0        self.entity = entity​    def checkForInput(self):        events = pygame.event.get()        self.checkForKeyboardInput()        self.checkForMouseInput(events)        self.checkForQuitAndRestartInputEvents(events)​    def checkForKeyboardInput(self):        pressedKeys = pygame.key.get_pressed()​        # 检测左右移动按键        if pressedKeys[pygame.K_LEFT] or pressedKeys[pygame.K_h] and not pressedKeys[pygame.K_RIGHT]:            self.entity.traits[\"goTrait\"].direction = -1        elif pressedKeys[pygame.K_RIGHT] or pressedKeys[pygame.K_l] and not pressedKeys[pygame.K_LEFT]:            self.entity.traits[\"goTrait\"].direction = 1        else:            self.entity.traits[\'goTrait\'].direction = 0​        # 检测跳跃按键        isJumping = pressedKeys[pygame.K_SPACE] or pressedKeys[pygame.K_UP] or pressedKeys[pygame.K_k]        self.entity.traits[\'jumpTrait\'].jump(isJumping)​        # 检测加速按键        self.entity.traits[\'goTrait\'].boost = pressedKeys[pygame.K_LSHIFT]

self.entity.traits[\'goTrait\'].direction

class Mario(EntityBase):    def __init__(self, x, y, level, screen, dashboard, sound, gravity=0.8):        # ... 其他代码 ...        self.traits = {            \"jumpTrait\": JumpTrait(self),            \"goTrait\": GoTrait(smallAnimation, screen, self.camera, self),            \"bounceTrait\": bounceTrait(self),       }        # ... 其他代码 ...class GoTrait:    def __init__(self, animation, screen, camera, ent):        self.animation = animation        self.direction = 0        # ... 其他代码 ...