如何用 Python 实现超级玛丽的界面和状态机?-深蓝源码网


时间: 2020-09-03 00:08:26 人气: 2270 评论: 0

云栖号:https://www.aliyun.com/#module-yedOfott8
第一手的上云资讯,不同行业精选的上云企业案例库,基于众多成功案例萃取而成的最佳实践,助力您上云决策!

image

小时候的经典游戏,代码参考了github上的项目Mario-Level-1(https://github.com/justinmeister/Mario-Level-1),使用pygame来实现,从中学习到了横版过关游戏实现中的一些处理方法。原项目实现了超级玛丽的第一个小关。
在原项目的基础上,游戏使用json文件来保存每一个关卡的数据,将数据和代码解耦合,目前已开发4个小关,后续关卡的扩展也很方便,只需要添加json文件和地图图片,支持新的怪物就行。游戏还支持进入水管,到新的子地图。
这篇文章是要介绍下游戏中的几个界面显示和界面之前如何转换,所以特意写了一个demo程序,完整的游戏代码在下面的github链接(https://github.com/marblexu/PythonSuperMario)中下载。

image

状态机介绍

游戏中的状态机一般都是有限状态机,简写为FSM(Finite State Machine),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
状态机的每一个状态至少需要有下面三个操作:
Startup:当从其他状态进入这个状态时,需要进行的初始化操作;

Update :在这个状态运行时进行的更新操作;

Cleanup:当从这个状态退出时,需要进行的清除操作。

状态需要的变量:
next: 表示这个状态退出后要转到的下一个状态;

persist:在状态间转换时需要传递的数据;

done:表示这个状态是否结束,状态机会根据这个值来决定转换状态。

游戏界面状态机的状态转换图如下,箭头表示可能的状态转换方向:(注意有个转换不太好画出来:Time Out状态可以转换到Game Over状态。)

image

这几个状态的意思比较简单,下面把游戏界面的截图发一下。
Main Menu:主菜单,启动程序就进入这个状态,可以用UP和DOWN键选择player 1或player 2,按回车键开启游戏。

image

Load Screen:游戏开始前的加载界面。

image

Game Run:游戏运行时的状态,在代码实现中是Level类。
image

Game Over:人物死亡且生命数目为0时到这个状态。
image

Time Out:在游戏中时间超时会到这个状态,这个和Game Over类似,就不截图了。

image

状态机代码实现

因为这篇文章的目的是游戏界面的状态机实现,所以专门写了一个state_demo.py文件,让大家可以更加方便的看代码。

游戏启动代码

开始是 pygame的初始化,设置屏幕大小为c.SCREEN_SIZE(800, 600)。所有的常量都保存在单独的constants.py中。

image

load_all_gfx函数查找指定目录下所有符合后缀名的图片,使用pg.image.load函数加载,保存在graphics set中。

GFX 保存在resources/graphics目录找到的所有图片,后面获取各种图形时会用到。
image

下面是demo的入口函数,先创建了一个保存所有状态的state_dict set,调用setup_states函数设置起始状态是 MAIN_MENU。

image

状态类

先定义一个State 基类, 按照上面说的状态需要的三个操作分别定义函数(startup, update, cleanup)。在 init 函数中定义了上面说的三个变量(next,persist,done),还有start_time 和 current_time 用于记录时间。

image

看一个状态类LoadScreen的具体实现,这个状态的显示效果如图3。

startup 函数保存了传入的persist,设置 next 为Level 状态类,start_time保存进入这个状态的开始时间。初始化一个Info类,这个就是专门用来显示界面信息的。
update 函数根据在这个状态已运行的时间(current_time - self.start_time),决定显示内容和是否结束状态(self.done = True)。

image

Info类

下面介绍Info类,界面的显示大部分都是由它来完成,init函数中create_info_labels函数创建通用的信息,create_state_labels函数对于不同的状态,会初始化不同的信息。

image

create_font_image_dict函数从之前加载的图片GFX[‘text_images’]中,截取字母和数字对应的图形,保存在一个set中,在后面创建文字时会用到。

image

get_image函数从一个大的Surface sheet 中按照 area(x, y, width, height)截取出部分图片 放入Surface image对应的起始位置(0,0),并按照scale参数调整大小。

pygame的 blit 函数介绍如下:

image

看一下create_info_labels函数中其中一个字符串’MARIO’是如何在界面上显示的。

create_label函数参数 (x, y) 表示字符串在界面上的起始位置,从self.image_dict中根据字符获取对应的Surface 对象。
set_label_rects函数会设置字符串中每一个Surface 对象 rect 的(x, y)值。

image

下面的坐标图可以看到,在左上角是整个屏幕的原点(0,0), 图中标识了长方形rect的四个顶点的坐标。

image

image

Control类

Control 是状态机类,main函数是游戏的主循环,setup_states函数设置游戏启动时运行的状态。

image

event_loop函数负责监听输入(键盘输入和退出按钮),slef.keys 保存键盘输入。

update函数会检测状态的done值,调用状态的更新函数。如果检测到当前状态结束,就调用flip_state函数进行旧状态的清理操作,并转换到下一个状态。

image

image

完整代码

有两个文件constants.py 和 state_demo.py,constants.py 保存了所有的字符串定义和常量。

constants.py

GAME_TIME_OUT 表示游戏的超时时间,这边为了demo演示,设成了5秒,实际是300秒。

image

state_demo.py

上面讲的状态类,状态机类都放在这里。


import os
import pygame as pg
from abc import ABC, abstractmethod
import constants as c


class State():
  def __init__(self):
      self.start_time = 0.0
      self.current_time = 0.0
      self.done = False
      self.next = None
      self.persist = {}

  @abstractmethod
  def startup(self, current_time, persist):
      '''abstract method'''

  def cleanup(self):
      self.done = False
      return self.persist

  @abstractmethod
  def update(sefl, surface, keys, current_time):
      '''abstract method'''

class Menu(State):
  def __init__(self):
      State.__init__(self)
      persist = {c.COIN_TOTAL: 0,
                 c.SCORE: 0,
                 c.LIVES: 3,
                 c.TOP_SCORE: 0,
                 c.CURRENT_TIME: 0.0,
                 c.LEVEL_NUM: 1,
                 c.PLAYER_NAME: c.PLAYER_MARIO}
      self.startup(0.0, persist)

  def startup(self, current_time, persist):
      self.next = c.LOAD_SCREEN
      self.persist = persist
      self.game_info = persist
      self.overhead_info = Info(self.game_info, c.MAIN_MENU)

      self.setup_background()
      self.setup_player()
      self.setup_cursor()

  def setup_background(self):
      self.background = GFX['level_1']
      self.background_rect = self.background.get_rect()
      self.background = pg.transform.scale(self.background,
                                  (int(self.background_rect.width*c.BACKGROUND_MULTIPLER),
                                  int(self.background_rect.height*c.BACKGROUND_MULTIPLER)))

      self.viewport = SCREEN.get_rect(bottom=SCREEN_RECT.bottom)
      self.image_dict = {}
      image = get_image(GFX['title_screen'], 1, 60, 176, 88,
                          (255, 0, 220), c.SIZE_MULTIPLIER)
      rect = image.get_rect()
      rect.x, rect.y = (170, 100)
      self.image_dict['GAME_NAME_BOX'] = (image, rect)

  def setup_player(self):
      self.player_list = []
      player_rect_info = [(178, 32, 12, 16), (178, 128, 12, 16)]
      for rect in player_rect_info:
          image = get_image(GFX['mario_bros'],
                              *rect, c.BLACK, 2.9)
          rect = image.get_rect()
          rect.x, rect.bottom = 110, c.GROUND_HEIGHT
          self.player_list.append((image, rect))
      self.player_index = 0

  def setup_cursor(self):
      self.cursor = pg.sprite.Sprite()
      self.cursor.image = get_image(GFX[c.ITEM_SHEET], 24, 160, 8, 8, c.BLACK, 3)
      rect = self.cursor.image.get_rect()
      rect.x, rect.y = (220, 358)
      self.cursor.rect = rect
      self.cursor.state = c.PLAYER1

  def update(self, surface, keys, current_time):
      self.current_time = current_time
      self.game_info[c.CURRENT_TIME] = self.current_time
      self.player_image = self.player_list[self.player_index][0]
      self.player_rect = self.player_list[self.player_index][1]
      self.update_cursor(keys)
      self.overhead_info.update(self.game_info)

      surface.blit(self.background, self.viewport, self.viewport)
      surface.blit(self.image_dict['GAME_NAME_BOX'][0],
                   self.image_dict['GAME_NAME_BOX'][1])
      surface.blit(self.player_image, self.player_rect)
      surface.blit(self.cursor.image, self.cursor.rect)
      self.overhead_info.draw(surface)

  def update_cursor(self, keys):
      if self.cursor.state == c.PLAYER1:
          self.cursor.rect.y = 358
          if keys[pg.K_DOWN]:
              self.cursor.state = c.PLAYER2
              self.player_index = 1
              self.game_info[c.PLAYER_NAME] = c.PLAYER_LUIGI
      elif self.cursor.state == c.PLAYER2:
          self.cursor.rect.y = 403
          if keys[pg.K_UP]:
              self.cursor.state = c.PLAYER1
              self.player_index = 0
              self.game_info[c.PLAYER_NAME] = c.PLAYER_MARIO
      if keys[pg.K_RETURN]:
          self.done = True

class LoadScreen(State):
  def __init__(self):
      State.__init__(self)
      self.time_list = [2400, 2600, 2635]

  def startup(self, current_time, persist):
      self.start_time = current_time
      self.persist = persist
      self.game_info = self.persist
      self.next = self.set_next_state()

      info_state = self.set_info_state()
      self.overhead_info = Info(self.game_info, info_state)

  def set_next_state(self):
      return c.LEVEL

  def set_info_state(self):
      return c.LOAD_SCREEN

  def update(self, surface, keys, current_time):
      if (current_time - self.start_time) < self.time_list[0]:
          surface.fill(c.BLACK)
          self.overhead_info.update(self.game_info)
          self.overhead_info.draw(surface)
      elif (current_time - self.start_time) < self.time_list[1]:
          surface.fill(c.BLACK)
      elif (current_time - self.start_time) < self.time_list[2]:
          surface.fill((106, 150, 252))
      else:
          self.done = True

class GameOver(LoadScreen):
  def __init__(self):
      LoadScreen.__init__(self)
      self.time_list = [3000, 3200, 3235]

  def set_next_state(self):
      return c.MAIN_MENU

  def set_info_state(self):
      return c.GAME_OVER

class TimeOut(LoadScreen):
  def __init__(self):
      LoadScreen.__init__(self)
      self.time_list = [2400, 2600, 2635]

  def set_next_state(self):
      if self.persist[c.LIVES] == 0:
          return c.GAME_OVER
      else:
          return c.LOAD_SCREEN

  def set_info_state(self):
      return c.TIME_OUT

class Level(State):
  def __init__(self):
      State.__init__(self)

  def startup(self, current_time, persist):
      self.game_info = persist
      self.persist = self.game_info
      self.player = None
      self.overhead_info = Info(self.game_info, c.LEVEL)
      self.setup_background()

  def setup_background(self):
      self.background = GFX['level_1']
      self.bg_rect = self.background.get_rect()
      self.background = pg.transform.scale(self.background, 
                                  (int(self.bg_rect.width*c.BACKGROUND_MULTIPLER),
                                  int(self.bg_rect.height*c.BACKGROUND_MULTIPLER)))
      self.bg_rect = self.background.get_rect()

      self.level = pg.Surface((self.bg_rect.w, self.bg_rect.h)).convert()
      self.viewport = SCREEN.get_rect(bottom=self.bg_rect.bottom)

  def update(self, surface, keys, current_time):
      self.game_info[c.CURRENT_TIME] = self.current_time = current_time
      self.overhead_info.update(self.game_info, self.player)
      if self.overhead_info.time <= 0:
          self.update_game_info()
          self.done = True
      self.draw(surface)

  def update_game_info(self):
      self.persist[c.LIVES] -= 1

      if self.persist[c.LIVES] == 0:
          self.next = c.GAME_OVER
      elif self.overhead_info.time == 0:
          self.next = c.TIME_OUT
      else:
          self.next = c.LOAD_SCREEN

  def draw(self, surface):
      self.level.blit(self.background, self.viewport, self.viewport)
      surface.blit(self.level, (0,0), self.viewport)
      self.overhead_info.draw(surface)

class Character(pg.sprite.Sprite):
  def __init__(self, image):
      pg.sprite.Sprite.__init__(self)
      self.image = image
      self.rect = self.image.get_rect()

class Info():
  def __init__(self, game_info, state):
      self.coin_total = game_info[c.COIN_TOTAL]
      self.total_lives = game_info[c.LIVES]
      self.state = state
      self.game_info = game_info

      self.create_font_image_dict()
      self.create_info_labels()
      self.create_state_labels()
      self.flashing_coin = FlashCoin(280, 53)

  def create_font_image_dict(self):
      self.image_dict = {}
      image_list = []

      image_rect_list = [# 0 - 9
                         (3, 230, 7, 7), (12, 230, 7, 7), (19, 230, 7, 7),
                         (27, 230, 7, 7), (35, 230, 7, 7), (43, 230, 7, 7),
                         (51, 230, 7, 7), (59, 230, 7, 7), (67, 230, 7, 7),
                         (75, 230, 7, 7), 
                         # A - Z
                         (83, 230, 7, 7), (91, 230, 7, 7), (99, 230, 7, 7),
                         (107, 230, 7, 7), (115, 230, 7, 7), (123, 230, 7, 7),
                         (3, 238, 7, 7), (11, 238, 7, 7), (20, 238, 7, 7),
                         (27, 238, 7, 7), (35, 238, 7, 7), (44, 238, 7, 7),
                         (51, 238, 7, 7), (59, 238, 7, 7), (67, 238, 7, 7),
                         (75, 238, 7, 7), (83, 238, 7, 7), (91, 238, 7, 7),
                         (99, 238, 7, 7), (108, 238, 7, 7), (115, 238, 7, 7),
                         (123, 238, 7, 7), (3, 246, 7, 7), (11, 246, 7, 7),
                         (20, 246, 7, 7), (27, 246, 7, 7), (48, 246, 7, 7),
                         # -*
                         (68, 249, 6, 2), (75, 247, 6, 6)]

      character_string = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ -*'

      for character, image_rect in zip(character_string, image_rect_list):
          self.image_dict[character] = get_image(GFX['text_images'], 
                                          *image_rect, (92, 148, 252), 2.9)

  def create_info_labels(self):
      self.score_text = []
      self.coin_count_text = []
      self.mario_label = []
      self.world_label = []
      self.time_label = []
      self.stage_label = []

      self.create_label(self.score_text, '000000', 75, 55)
      self.create_label(self.coin_count_text, '*00', 300, 55)
      self.create_label(self.mario_label, 'MARIO', 75, 30)
      self.create_label(self.world_label, 'WORLD', 450, 30)
      self.create_label(self.time_label, 'TIME', 625, 30)
      self.create_label(self.stage_label, '1-1', 472, 55)

      self.info_labels = [self.score_text, self.coin_count_text, self.mario_label,
                  self.world_label, self.time_label, self.stage_label]

  def create_state_labels(self):
      if self.state == c.MAIN_MENU:
          self.create_main_menu_labels()
      elif self.state == c.LOAD_SCREEN:										

技术沙龙 教程文章 热点综合

评论