作者 | marble_xu
责编 | 郭芮
出品 | CSDN博客
小时候的经典游戏,代码参考了github上的项目Mario-Level-1(https://github.com/justinmeister/Mario-Level-1),使用pygame来实现,从中学习到了横版过关游戏实现中的一些处理方法。原项目实现了超级玛丽的第一个小关。在原项目的基础上,游戏使用json文件来保存每一个关卡的数据,将数据和代码解耦合,目前已开发4个小关,后续关卡的扩展也很方便,只需要添加json文件和地图图片,支持新的怪物就行。游戏还支持进入水管,到新的子地图。这篇文章是要介绍下游戏中的几个界面显示和界面之前如何转换,所以特意写了一个demo程序,完整的游戏代码在下面的github链接(https://github.com/marblexu/PythonSuperMario)中下载。
状态机介绍
游戏中的状态机一般都是有限状态机,简写为FSM(Finite State Machine),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。状态机的每一个状态至少需要有下面三个操作:Startup:当从其他状态进入这个状态时,需要进行的初始化操作;
Update :在这个状态运行时进行的更新操作;
Cleanup:当从这个状态退出时,需要进行的清除操作。
状态需要的变量:next: 表示这个状态退出后要转到的下一个状态;
persist:在状态间转换时需要传递的数据;
done:表示这个状态是否结束,状态机会根据这个值来决定转换状态。
游戏界面状态机的状态转换图如下,箭头表示可能的状态转换方向:(注意有个转换不太好画出来:Time Out状态可以转换到Game Over状态。)
图1
这几个状态的意思比较简单,下面把游戏界面的截图发一下。Main Menu:主菜单,启动程序就进入这个状态,可以用UP和DOWN键选择player 1或player 2,按回车键开启游戏。
图2Load Screen:游戏开始前的加载界面。
图3Game Run:游戏运行时的状态,在代码实现中是Level类。
图4Game Over:人物死亡且生命数目为0时到这个状态。
图5Time Out:在游戏中时间超时会到这个状态,这个和Game Over类似,就不截图了。
状态机代码实现
因为这篇文章的目的是游戏界面的状态机实现,所以专门写了一个state_demo.py文件,让大家可以更加方便的看代码。游戏启动代码开始是 pygame的初始化,设置屏幕大小为c.SCREEN_SIZE(800, 600)。所有的常量都保存在单独的constants.py中。
import os
import pygame as pg
import constants as c
pg.init()
pg.event.set_allowed([pg.KEYDOWN, pg.KEYUP, pg.QUIT])
pg.display.set_caption(c.ORIGINAL_CAPTION)
SCREEN = pg.display.set_mode(c.SCREEN_SIZE)
SCREEN_RECT = SCREEN.get_rect()
load_all_gfx函数查找指定目录下所有符合后缀名的图片,使用pg.image.load函数加载,保存在graphics set中。
GFX 保存在resources/graphics目录找到的所有图片,后面获取各种图形时会用到。
def load_all_gfx(directory, colorkey=(255,0,255), accept=('.png', '.jpg', '.bmp', '.gif')):
graphics = {}
for pic in os.listdir(directory):
name, ext = os.path.splitext(pic)
if ext.lower() in accept:
img = pg.image.load(os.path.join(directory, pic))
if img.get_alpha():
img = img.convert_alpha()
else:
img = img.convert()
img.set_colorkey(colorkey)
graphics[name] = img
return graphics
GFX = load_all_gfx(os.path.join("resources","graphics"))
下面是demo的入口函数,先创建了一个保存所有状态的state_dict set,调用setup_states函数设置起始状态是 MAIN_MENU。
if __name__=='__main__':
game = Control()
state_dict = {c.MAIN_MENU: Menu(),
c.LOAD_SCREEN: LoadScreen(),
c.LEVEL: Level(),
c.GAME_OVER: GameOver(),
c.TIME_OUT: TimeOut()}
game.setup_states(state_dict, c.MAIN_MENU)
game.main()
状态类
先定义一个State 基类, 按照上面说的状态需要的三个操作分别定义函数(startup, update, cleanup)。在 init 函数中定义了上面说的三个变量(next,persist,done),还有start_time 和 current_time 用于记录时间。
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'''
看一个状态类LoadScreen的具体实现,这个状态的显示效果如图3。
startup 函数保存了传入的persist,设置 next 为Level 状态类,start_time保存进入这个状态的开始时间。初始化一个Info类,这个就是专门用来显示界面信息的。update 函数根据在这个状态已运行的时间(current_time - self.start_time),决定显示内容和是否结束状态(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
Info类
下面介绍Info类,界面的显示大部分都是由它来完成,init函数中create_info_labels函数创建通用的信息,create_state_labels函数对于不同的状态,会初始化不同的信息。
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)
create_font_image_dict函数从之前加载的图片GFX[‘text_images’]中,截取字母和数字对应的图形,保存在一个set中,在后面创建文字时会用到。
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,