资讯动态

用状态机设计游戏主菜单:Pygame经典游戏架构实践

发布时间:2026/10/18 10:07:46 来源:尧图企业网站定制
复刻“Mario Forever”这类经典游戏时很多人会把精力放在关卡设计、跳跃手感、敌人 AI 上结果做到一半才发现光是“主菜单”这一个环节就已经暴露出大量工程问题。菜单上下项选择不顺畅、按钮点击后进程卡死、从暂停返回主菜单时状态全乱、背景音乐重复播放叠加……这些问题看起来都很小但每一条都在提醒同一个本质主菜单不是一个花哨的启动界面它是整个游戏全局状态管理体系的入口。如果你去查经典游戏复刻项目的源码会得到同一个结论主菜单在架构上占据的位置远比视觉层重要。它强制你回答几个关键问题——游戏有哪些状态状态之间如何流转输入事件应该交给谁处理资源应该在哪一个节点加载想不清楚这些主菜单就会变成一坨“只有界面、没有结构”的临时代码想清楚了它反而会成为整个游戏最清晰的架构示范。这篇文章以“Mario Forever”的主菜单为切入点讲的不是怎么用美术素材拼一个画面而是如何在代码层面设计、实现并验证一个可靠的主菜单系统。你会看到一个可运行的 Pygame 最小示例包含状态机、事件循环、菜单导航和场景切换也会看到常见坑点与排查方法。读完这篇文章你能回答一个很实际的问题当游戏从主菜单进入关卡、再从关卡返回主菜单时系统凭什么不乱1. 主菜单不是页面而是游戏状态机的一个状态很多新手第一次写主菜单脑子里浮现的是一个“页面”模型一个函数负责画背景一个函数负责画按钮然后进入一个 while 循环等用户点击。这种写法在小 demo 里没有任何问题但一旦游戏开始拥有关卡、暂停、设置、结算这些场景“页面”模型就会迅速失效——因为页面与页面之间只是跳转而游戏场景之间是“状态切换”切换时要处理暂停逻辑、资源释放、输入屏蔽、音乐切换这些都不是页面跳转能表达的。更合理的建模方式是把整个游戏看作一个有限状态机FSM主菜单只是众多状态之一。经典平台跳跃游戏的状态流转通常包含这样几条路径BOOT初始化→ MAIN_MENU主菜单→ PLAYING关卡中 PLAYING → PAUSED暂停 PAUSED → PLAYING / MAIN_MENU PLAYING → LEVEL_COMPLETE过关结算→ NEXT_LEVEL / MAIN_MENU在这套模型里主菜单并不是起点也不是终点。它是“初始化完成之后进入的第一个可交互状态”同时是“一局游戏结束之后玩家被送回来的兜底状态”。理解这一点之后主菜单的设计目标就变了它的核心任务不是“展示选项列表”而是“正确地把玩家从这个状态引导到下一个状态”。用代码来表达状态不是一个字符串或者布尔值而应该是一个明确的枚举配合一个状态切换上下文from enum import Enum class GameState(Enum): BOOT 0 MAIN_MENU 1 PLAYING 2 PAUSED 3 SETTINGS 4 EXIT 5很多复刻项目走上弯路是因为把“当前处于哪个界面”用一系列布尔值管理例如is_in_menu True、is_paused False。布尔值在状态少的时候还勉强能用一旦状态数量超过三个就会出现“互相矛盾”的非法组合例如is_in_menu True且is_paused True。枚举是这里最低成本的防呆方案。主菜单作为状态机的一个状态还有一个隐藏优势它天然支持“返回”。通过维护一个状态栈玩家在暂停界面按“返回主菜单”时系统只需要清空栈回到MAIN_MENU而不需要手动重置关卡里的所有对象。很多仓库里“主菜单无法正确返回”的 bug本质上就是缺少这层状态栈设计。1.1 状态机的最小实现游戏状态机的核心并不复杂关键是统一入口和出口。每一步循环里当前状态负责三件事接收输入、更新逻辑、绘制画面。状态切换发生时旧状态执行退出逻辑新状态执行进入逻辑。class GameContext: def __init__(self): self.current_state GameState.BOOT self.running True def switch_to(self, new_state: GameState): print(f[STATE] {self.current_state.name} - {new_state.name}) self.current_state new_state这里刻意保留了打印日志因为状态切换是游戏开发里最适合打日志的位置之一。出现奇怪的跳转问题时先看状态切换日志比盲改代码高效得多。1.2 主菜单承担预加载逻辑经典游戏的主菜单通常还会承担一个隐藏任务资源预加载。玩家看到主菜单时游戏往往并没有把全部关卡素材读进内存而是只加载了主菜单必需的少量资源再在后台或切换关卡前完成大体积资源的加载。这样做的好处是启动速度更快坏处是如果预加载逻辑写在了主循环之后玩家会在黑屏和白屏之间等很久。从工程角度讲合理顺序是初始化窗口和基础系统 → 显示主菜单 → 玩家选择“开始游戏”进入加载状态 → 加载完成后再切换到关卡状态。加载不是主菜单的事但它必须发生在主菜单之后、关卡之前所以主菜单状态天然成为预加载流程的分界点。2. 主菜单背后的事件循环卡顿和跳帧的根源在这里游戏主菜单往往看起来是静止的但它的代码并不是“等用户按一下再执行一次”而是在每一帧都持续运行。理解了这一点才能理解很多主菜单问题为什么用调试工具都难以复现。一个标准游戏循环做四件事扫描输入、更新当前状态、绘制画面、等待下一帧。以 Pygame 为例主循环长这样import pygame clock pygame.time.Clock() running True while running: # 1. 输入事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 2. 状态更新主菜单属于这一层 # 3. 画面绘制 pygame.display.flip() # 4. 锁帧 clock.tick(60)这里最容易踩的坑是“没有锁帧”。如果不调clock.tick(60)游戏循环会以尽可能快的速度跑菜单画面的绘制帧率可能是几百赫兹。表面上没有影响但会出现三个问题CPU 占用极高、字体闪烁或撕裂、基于帧计数的动画速度失控。复刻经典游戏时动画速度通常以帧为单位定义帧率浮动会导致走路、闪烁、滚屏在高端机器上快得离谱。2.1 主菜单里的“运动”很多人以为主菜单没有运动这并不准确。经典风格的主菜单至少包含三类运动光标上下移动的切换动画、菜单标题的轻微浮动、以及背景的滚动或粒子效果。这些运动如果直接依赖“帧数”在帧率不稳定时就会抖动。正确做法是引入时间增量delta time。每一帧计算距上一帧的秒数再把所有运动速度从“像素/帧”换算成“像素/秒”。这套机制一旦在主菜单里建立后续关卡里的角色移动也能复用。dt clock.tick(60) / 1000.0 # 转换为秒 menu_cursor_offset cursor_speed * dt如果项目坚持使用像素/帧就必须用clock.tick(60)锁死帧率并且假设每帧间隔固定。复刻经典游戏时这两种选择都成立但混用就会出现“菜单动画正常、进入游戏后忽快忽慢”的问题。所以最佳实践是全项目统一时间基准。2.2 按键去抖与“跳项”主菜单上下选择有一个很容易被忽略的技术细节按键重复。玩家按住“下”键时操作系统和游戏框架通常会持续产生按键事件。如果代码不对事件做去抖处理光标会在按住按键的瞬间连续向下跳好几个选项。Pygame 的pygame.KEYDOWN事件本身不做去抖只负责报告“这次按键按下”。实际工程中通常配合一个冷却时间同一按键触发后短时间内忽略后续事件。这个逻辑用一句话描述就是听事件但只接受“当前时间减去上次触发时间大于间隔”的事件。last_key_time 0 key_repeat_delay 0.25 # 秒 for event in pygame.event.get(): if event.type pygame.KEYDOWN: now pygame.time.get_ticks() / 1000.0 if now - last_key_time key_repeat_delay: handle_menu_navigation(event.key) last_key_time now为什么不直接依赖框架自带的按键重复因为不同操作系统、不同键盘的重复间隔不一样最终手感很难统一。自己做节流手感完全可控是复刻经典游戏时更稳妥的做法。3. 主菜单的 UI 系统焦点、选项列表与输入适配经典风格游戏主菜单在 90 年代有一个特点它不一定依赖鼠标更多时候依赖键盘和手柄。这个“以焦点导航为主”的交互模式与现代网页 UI 的“点击按钮”完全不同。复刻这类主菜单时如果只做鼠标点击到手柄玩家手里就是废的如果只做键盘导航在 PC 上又显得不友好。正确的做法是以“焦点”为中间层统一处理所有输入。3.1 菜单选项的数据结构主菜单的选项应该是一份配置数据而不是散落在 if/else 里的字符串。经典风格菜单选项通常包含显示文本、关联目标状态、是否可选、帮助说明。把选项抽成数据之后菜单的增删和国际化都变得简单。{ title: MARIO FOREVER, options: [ { label: 1 PLAYER, target_state: PLAYING, enabled: true }, { label: 2 PLAYERS, target_state: PLAYING, enabled: true }, { label: OPTIONS, target_state: SETTINGS, enabled: true }, { label: EXIT, target_state: EXIT, enabled: true } ], default_index: 0 }这份配置的价值在于菜单的显示逻辑和业务逻辑解耦。绘制层只需要知道“当前有几个选项、哪个被选中”而点击之后该做什么由选项内部的target_state决定。以后想加一个 “CREDITS” 选项只需要在 JSON 里加一行不需要改绘制代码。3.2 菜单组件的最小结构如果把主菜单拆成组件至少需要三个全局的状态机、菜单管理器、绘制器。菜单管理器内部维护选项列表当前选中索引允许的最小索引和最大索引输入去抖时间状态切换回调选中索引的变更要限制合法范围否则玩家在不断按“上”时会从第一个选项跳到空区域界面看起来像指针丢失了。经典做法是取模循环即在第一个选项继续按“上”时跳到最后一项也有游戏选择在边界处停止两种手感都常见只要实现时做显式处理即可。3.3 多套输入设备经典风格主菜单要同时支持键盘、手柄和游戏杆。Pygame 里键盘事件和手柄事件是不同的对象。工程上可以做一个输入适配层把物理输入统一映射为“菜单领域事件”例如MENU_UP、MENU_DOWN、MENU_CONFIRM、MENU_CANCEL。状态机和菜单管理器只认识这四个领域事件不需要关心具体是哪台设备传来的。键盘上方向键触发MENU_UP手柄十字键同样触发MENU_UP鼠标点击某个选项触发的也是MENU_CONFIRM只不过它先经过命中检测把点击坐标转换为“目标选项索引”。这套适配层在后续关卡里同样有用因为它把“玩家意图”和“设备信号”分开了。4. 环境准备与项目结构先跑通最小主菜单接下来进入实操。为了让示例尽量好复现我使用 Python 和 Pygame 实现一个极简但完整的经典风格主菜单。你不需要任何一个美术素材所有画面都用色块和文本组成重点展示状态机和菜单交互逻辑。环境要求Python 3.8 或更高版本Pygame 2.x任意能运行 Python 的桌面操作系统安装 Pygame 的方式pip install pygame建议在虚拟环境中执行避免污染全局 Python 环境python -m venv venv source venv/bin/activate # Windows 下为 venv\Scripts\activate pip install pygame项目结构采用极简但可扩展的方式mario_menu_demo/ ├── config/ │ └── menu.json ├── core/ │ ├── state.py │ ├── input.py │ └── menu.py ├── main.py └── requirements.txtmain.py负责初始化 Pygame 并启动主循环core/state.py管理游戏状态core/input.py负责输入适配core/menu.py负责菜单逻辑与绘制。这个结构对一个主菜单 demo 来说有些冗余但它展示了真实项目的组织方式入口、核心逻辑、配置分离。后续加入关卡模块时只需新增core/level.py和scenes/level.py主循环不需要重写。5. 完整示例代码可运行的经典风格主菜单为了让读者真正跑通我提供一个完整的最小示例。代码分为三个文件全部贴出。项目意图不是复刻 “Mario Forever” 的美术或音乐而是还原其主菜单的交互模式从上到下选择选项、经典风格高亮、进入不同状态。5.1 状态定义文件core/state.pyfrom enum import Enum class GameState(Enum): BOOT 0 MAIN_MENU 1 PLAYING 2 PAUSED 3 SETTINGS 4 EXIT 5 class GameContext: 全局状态上下文保存当前状态并提供切换能力。 def __init__(self): self.current_state GameState.BOOT self.running True def switch_to(self, new_state: GameState) - None: print(f[STATE] {self.current_state.name} - {new_state.name}) self.current_state new_state def request_exit(self) - None: self.switch_to(GameState.EXIT) self.running FalseGameContext不需要包含所有游戏数据它只负责两件事当前是哪个状态、是否可以继续运行。关卡对象、玩家对象等数据应该在状态切换到对应场景时创建而不是塞进这个全局类。5.2 菜单配置文件config/menu.json{ title: MARIO FOREVER STYLE MENU, subtitle: PRESS ENTER TO START, options: [ { label: 1 PLAYER, target_state: PLAYING, enabled: true }, { label: 2 PLAYERS, target_state: PLAYING, enabled: true }, { label: OPTIONS, target_state: SETTINGS, enabled: true }, { label: EXIT, target_state: EXIT, enabled: true } ] }注意这里的target_state是字符串读取配置后需要转换成GameState枚举。5.3 主菜单逻辑文件core/menu.pyimport json import pygame from core.state import GameState class MenuItem: def __init__(self, label: str, target_state: GameState, enabled: bool): self.label label self.target_state target_state self.enabled enabled class Menu: 经典风格主菜单上下导航 回车确认。 def __init__(self, config_path: str): with open(config_path, r, encodingutf-8) as f: config json.load(f) self.title config[title] self.subtitle config[subtitle] self.items [ MenuItem( labelitem[label], target_stateGameState(item[target_state]), enableditem[enabled], ) for item in config[options] ] self.selected_index 0 self.last_input_time 0 self.input_delay 0.25 def move_up(self) - None: if self.selected_index 0: self.selected_index - 1 else: self.selected_index len(self.items) - 1 def move_down(self) - None: if self.selected_index len(self.items) - 1: self.selected_index 1 else: self.selected_index 0 # ---------- 绘制 ---------- def draw(self, screen: pygame.Surface, font: pygame.font.Font) - None: screen.fill((20, 20, 40)) title_surf font.render(self.title, True, (255, 200, 0)) title_rect title_surf.get_rect(center(screen.get_width() // 2, 120)) screen.blit(title_surf, title_rect) for index, item in enumerate(self.items): y 260 index * 50 if index self.selected_index: color (255, 255, 0) marker - else: color (255, 255, 255) marker label_surf font.render(marker item.label, True, color) label_rect label_surf.get_rect(center(screen.get_width() // 2, y)) screen.blit(label_surf, label_rect) if self.subtitle: hint_surf font.render(self.subtitle, True, (180, 180, 180)) hint_rect hint_surf.get_rect(center(screen.get_width() // 2, 430)) screen.blit(hint_surf, hint_rect)这个类只负责“菜单状态”内部的逻辑显示标题、绘制选项、移动选中项。真正决定“选中之后做什么”的逻辑由主循环根据target_state调用GameContext.switch_to()完成菜单类本身不直接操作游戏状态。这样设计的好处是菜单可以被复用——设置菜单、暂停菜单都可以继承或复用这个类只是配置和回调不同。5.4 主循环文件main.pyimport pygame from core.state import GameState, GameContext from core.menu import Menu WINDOW_WIDTH 640 WINDOW_HEIGHT 480 FPS 60 def main() - None: pygame.init() screen pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(Mario Forever Style Main Menu Demo) clock pygame.time.Clock() font pygame.font.SysFont(arial, 32) context GameContext() menu Menu(config/menu.json) # 启动后直接进入主菜单状态 context.switch_to(GameState.MAIN_MENU) while context.running: dt clock.tick(FPS) / 1000.0 now pygame.time.get_ticks() / 1000.0 for event in pygame.event.get(): if event.type pygame.QUIT: context.request_exit() if context.current_state GameState.MAIN_MENU: if event.type pygame.KEYDOWN: if now - menu.last_input_time menu.input_delay: continue menu.last_input_time now if event.key pygame.K_UP: menu.move_up() elif event.key pygame.K_DOWN: menu.move_down() elif event.key pygame.K_RETURN: selected menu.items[menu.selected_index] context.switch_to(selected.target_state) if context.current_state GameState.MAIN_MENU: menu.draw(screen, font) elif context.current_state GameState.PLAYING: screen.fill((0, 80, 0)) hint font.render(PLAYING - Press ESC to exit, True, (255, 255, 255)) screen.blit(hint, (60, 220)) pygame.display.flip() pygame.quit() if __name__ __main__: main()这个主循环里PLAYING状态只是用一个占位画面演示“从主菜单进入游戏后状态确实切换了”。真实项目中这个分支会替换为level.update()和level.draw()但主循环骨架不变。dt变量在这里只用于演示菜单的键盘去抖用时间戳而不是累计帧数避免不同帧率下手感不一致。5.5 运行方式终端进入项目根目录执行python main.py如果代码没有报错会弹出一个 640x480 的黑色窗口窗口标题是 “Mario Forever Style Main Menu Demo”屏幕中央显示金色标题和四个白/黄色选项。按上下方向键移动高亮按回车确认。选择 EXIT 会结束程序选择 1 PLAYER 会进入绿色占位画面按 ESC 无法返回因为示例没有实现主动切换回菜单的逻辑这一点会在后面的常见问题里说明。6. 运行结果与效果验证运行成功后你应该观察到三类反应第一窗口正常弹出主菜单标题和四个选项显示完整默认选中的是第一项 “1 PLAYER”它显示为黄色并带-前缀。第二按“下”方向键时高亮逐项移动到下一行。如果按住“下”不放光标不会瞬间跳到底部而是以每 0.25 秒移动一次的速度匀速向下。第三按回车选择 “1 PLAYER” 后控制台会打印[STATE] MAIN_MENU - PLAYING窗口内容变为绿色背景并显示 “PLAYING - Press ESC to exit” 提示。按窗口关闭按钮可以退出程序。如何判断主菜单状态机运作正常观察控制台日志即可。如果状态日志顺序正确说明状态切换链路没有问题。如果按回车后窗口没有变化、也没有日志问题大概率出在配置读取或事件分支上下一篇常见问题会直接给出排查顺序。验证时还可以做一个小实验把WINDOW_WIDTH改成 800或把字体从arial换成consolas观察菜单绘制是否正确适应新尺寸。这能确认绘制代码用的是get_rect(center...)居中布局而不是写死坐标。经典风格复刻项目后期必然要适配不同分辨率所以一开始就不要写死坐标。7. 常见问题与排查思路主菜单虽然功能简单但运行中出现的坑往往很隐蔽。以下是这个示例以及真实复刻项目中经常遇到的五类问题按优先级排序。问题现象可能原因排查方式解决方案按下回车没有反应控制台无日志事件分支只处理了MAIN_MENU输入状态实际不在主菜单打印context.current_state确认状态值初始化时显式switch_to(GameState.MAIN_MENU)按住方向键选项瞬间跳到底没有按键去抖或去抖计时失效检查last_input_time是否在事件分支内更新参考代码统一在事件处理开头更新last_input_time窗口弹出但长时间黑屏才显示菜单资源加载写在主循环内部或预加载与绘制混在一起查看启动耗时日志把大资源加载放到单独加载状态不在主循环内同步加载从 PLAYING 不能返回主菜单状态机里没有实现“返回主菜单”路径检查按键事件中是否对MAIN_MENU做了切换在 PLAYING 状态监听 ESC 键调用context.switch_to(GameState.MAIN_MENU)菜单中文或特殊字符显示为方块系统字体不支持对应字符编码打印pygame.font.get_fonts()查看可用字体指定支持中文的字体路径例如微软雅黑或文泉驿第二个问题值得单独说清楚按键去抖的last_input_time必须在处理完合法按键事件后立即更新不能等到绘制阶段再更新。否则在帧率较高时同一个事件被重复处理光标依然会连跳。更好的做法是把输入处理收敛到一个独立函数函数内部所有分支共用同一个时间判断避免某个分支忘记更新。第四个问题在真实项目中几乎一定会遇到。前面示例里我们偷懒没有实现从 PLAYING 返回主菜单的路径因为主菜单章节的重点是“进入”但在完整游戏里从任何状态返回主菜单是一件高频操作。推荐的实现方式是做一个栈式状态管理class StateStack: def __init__(self): self.stack [] def push(self, state: GameState): self.stack.append(state) def pop(self) - GameState: return self.stack.pop() def back_to_menu(self): while len(self.stack) 1: self.stack.pop()主菜单作为栈底暂停、设置、结算作为栈上层的临时状态。返回时只需弹栈直到栈底是主菜单。栈式状态管理比“if current_state X: switch_to(menu)”更通用因为它天然支持“暂停 → 设置 → 返回暂停”这种多层回退而不用为每一种组合写判断条件。8. 主菜单的工程最佳实践与扩展建议主菜单本身很小但它是整个游戏工程规范化的起点。以下几项实践建议在写第一个主菜单时就落实。8.1 选项配置化优先菜单选项不应该散落在主循环的 if/else 里。理由有两个一是可维护性后续调整菜单顺序、增加选项时不需要动代码逻辑二是可扩展性选项配置完全可以升级为定义“光标位置、帮助文本、图标、解锁条件”的完整配置。配置化不仅能减少 bug还能让不同策划或内容创作者直接改配置不碰代码。8.2 统一使用组件的 target_state主菜单选项的核心字段是target_state。点击选项、按下确认键、鼠标点击按钮最终都只做一件事情请求状态切换。把“状态切换”作为菜单操作的统一出口可以避免出现“点击选项后直接执行了某段逻辑但状态机不知道”的割裂情况。状态机永远应该是唯一裁决者菜单只是表达玩家意图。8.3 音频策略主菜单音乐与循环经典风格游戏的主菜单几乎都有背景音乐音乐策略是主菜单状态最容易出错的地方之一。核心规则是主菜单音乐必须在离开主菜单时停止在返回主菜单时重新播放或恢复。如果音乐不停就会和关卡音乐叠加听感非常混乱。Pygame 里可以通过通道Channel区分不同场景的音乐。主菜单使用一个音乐通道关卡使用另一个。状态切换时先stop()旧音乐再播放新音乐。不要依赖 Pygame 默认的全局音乐接口因为场景多了之后全局接口无法区分“谁在播、谁该停”。8.4 退出确认与防误触经典风格游戏在“退出”选项上通常提供二次确认。这个设计不是多余因为主菜单支持键盘操作时“回车键连打”很容易误触退出。标准的做法是“EXIT”选项被确认后先进入一个二次确认状态而不是直接退出。这个二次确认同样可以复用同一个菜单组件只是配置和回调不同。8.5 日志与调试可见性主菜单状态切换日志是排查游戏流程问题的最有力工具。建议至少记录以下信息当前状态、目标状态、触发事件的按键、被选中的菜单项名称。日志量不多但能极大缩短问题定位时间。在开发阶段日志输出到控制台即可进入真机调试阶段可以改为写入文件。8.6 分辨率适配与安全区复刻经典游戏时主菜单画面往往按原始分辨率设计例如 640x480。现代显示器分辨率更高直接把画面拉伸会出现字迹模糊和像素变形。更稳妥的做法是主菜单逻辑层继续使用逻辑分辨率渲染时按比例缩放并加黑边保持像素美术风格不变。这个策略应该在主菜单阶段就定下来因为关卡背景和角色素材同样需要统一缩放规则。8.7 从主菜单延伸出去的架构能力主菜单跑通之后整套架构应该具备状态机切换、输入适配、事件循环、资源配置。这些能力正是后续开发关卡、敌人、Boss 战、结算界面都需要的基础设施。因此建议把主菜单的状态机设计当作整个项目的“骨架先行”实验先在这里把架构问题暴露出来而不是等到关卡开发一半再来重构。9. 从主菜单走向完整游戏架构回到开头那句话主菜单是游戏状态机的入口。如果你只是把主菜单当作一个静态界面你可能永远意识不到关卡里的暂停、结算、重新开始、返回主菜单都是同一个状态切换问题的不同表现。而一旦你用一个带状态栈、配置化选项、统一输入适配的架构把主菜单写清楚后面加入新场景时代码结构几乎不用动只需要增加新状态和新的配置项。这篇文章给出的最小示例代码量不大但它是理解经典游戏主菜单的一个很有效的“解剖标本”。建议下一步实践三件事第一给示例加上从 PLAYING 返回主菜单的功能用状态栈实现第二把 EXIT 选项改成二次确认第三尝试在两个选项之间加入音效反馈。完成这三项你对主菜单的理解就超过大多数只做“按钮点击”的入门 demo 了。复刻经典游戏最大的魅力恰恰在于它老旧的画面背后有一套成熟可靠的工程逻辑。主菜单作为玩家看到的第一屏也是这套逻辑第一次发挥作用的地方。建议收藏这篇文章写代码的时候对照着来能少走很多弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑