用Python和PyGame实现2.5D沙盒游戏:从伪3D渲染到完整游戏架构

用Python和PyGame实现2.5D沙盒游戏:从伪3D渲染到完整游戏架构 1. 项目概述与核心价值最近在整理自己的代码仓库翻出来一个几年前用Python和PyGame写的“我的世界”风格小游戏。当时写它纯粹是为了好玩想看看用最简单的工具能还原出多少那种方块世界的建造乐趣。没想到后来断断续续完善加了些基础功能现在它已经是一个能跑起来、有完整代码和贴图的轻量级沙盒游戏原型了。今天把它分享出来一方面是给对游戏开发感兴趣、特别是想从零开始理解3D游戏哪怕是伪3D基础原理的朋友一个参考案例另一方面这个项目麻雀虽小五脏俱全从地图生成、玩家控制、方块放置破坏到简单的光照系统它都涉及了非常适合作为学习Python和PyGame的进阶练手项目。这个项目的核心价值在于“轻量”和“可理解”。它没有用任何复杂的游戏引擎就是纯Python加上PyGame这个2D图形库。你可能好奇2D库怎么做3D效果其实这里用的是一种叫“2.5D”或“伪3D”的技术通过巧妙的坐标变换和贴图绘制在2D平面上模拟出3D的视觉深度。对于初学者来说这比直接上手真正的3D引擎如Unity、Unreal要友好得多你能清晰地看到每一行代码是如何驱动屏幕上每一个像素的。项目里包含了所有可运行的源代码和一套我自制的简易像素风贴图资源你下载后配置好环境就能直接运行看到那个熟悉的方块世界在命令行窗口里跑起来。2. 技术选型与架构设计思路2.1 为什么是Python PyGame选择这个技术栈首要考虑的是快速原型开发和学习的低门槛。Python语法简洁PyGame库的API也非常直观它封装了SDL处理窗口、事件、图像绘制和声音都很方便。对于实现一个“我的世界”风格的沙盒游戏我们最核心的需求是高效地管理大量方块地图数据、实时处理玩家输入、以及快速绘制带有深度感的场景。PyGame虽然是个2D库但它的Surface对象和blit函数性能足够应对这种网格化、贴图固定的游戏场景。当然如果追求极致的性能和复杂的3D效果C配合OpenGL或专业的游戏引擎是更好的选择但那会引入巨大的学习成本。我们这个项目的目标不是复刻一个商业级的《我的世界》而是理解其核心机制因此PythonPyGame的组合在开发效率和可理解性上达到了最佳平衡。2.2 核心架构数据与渲染分离游戏的核心架构遵循一个清晰的数据与渲染分离原则。整个游戏世界被抽象为一个三维数组或者用字典来稀疏存储以节省内存我们称之为world_map。数组的每个元素存储一个方块类型ID比如1代表草方块2代表泥土3代表石头。这个数据模型是游戏状态的唯一真相来源。另一个核心模块是玩家Player它保存了位置x, y, z、视角水平角yaw俯仰角pitch等信息。渲染系统Renderer则根据world_map和Player的状态计算每个方块在屏幕上的投影并贴上对应的纹理。输入处理系统InputHandler负责捕获键盘鼠标事件更新玩家状态或修改world_map比如放置/破坏方块。这种分离使得代码结构清晰调试方便比如你可以单独测试地图生成算法而不必启动整个渲染循环。2.3 伪3D渲染原理从体素到屏幕这是本项目最有趣的技术点。我们如何用2D库画出3D方块这里采用了一种基于“高度场”的2.5D渲染方法类似于早期《魔兽争霸2》或《运输大亨》的视角。我们并不进行真正的3D模型变换和透视投影而是将3D世界“拍扁”成一个从特定角度通常是斜45度角俯视观看的2D图像。具体到代码中确定绘制顺序为了正确处理遮挡我们需要从远到近绘制方块。通常按照从屏幕左上角远处到右下角近处的顺序遍历地图网格。计算屏幕坐标对于一个位于(wx, wy, wz)的世界坐标我们通过一个仿射变换公式计算其在屏幕上的(sx, sy)坐标。一个经典的等距投影公式是sx (wx - wy) * tile_width_halfsy (wx wy - wz) * tile_height_half。这里的tile_width_half和tile_height_half是单个方块贴图宽高的一半这种计算使得方块呈现菱形连接形成等距网格。绘制方块面每个方块我们通常只绘制可见的面。在我们的视角下一个方块通常有顶部Top、正面Front和侧面Side可能可见。我们会根据当前方块与周围方块的相对高度wz值来决定绘制哪些面。例如如果一个方块的上方没有其他方块我们就绘制它的顶面如果它的前方y方向没有方块就绘制正面。通过为不同面使用不同的贴图立体感就出来了。简单光照为了增强立体感我们还可以根据面的朝向法线方向应用一个简单的亮度系数。比如顶面最亮正面次之侧面最暗。这可以通过在绘制时调整贴图的颜色亮度pygame.Surface.fill配合BLEND模式来实现。注意这种渲染方式性能消耗与屏幕内可见的方块数量成正比对于非常大的地图需要进行视锥裁剪只绘制玩家周围的区域。本项目实现了一个基于玩家坐标的简单裁剪只渲染以玩家为中心的一定半径内的方块。3. 核心模块实现与代码解析3.1 游戏世界与方块数据管理游戏世界的基石是方块数据的管理。我们使用一个三维字典world来稀疏存储方块键是(x, y, z)元组值是方块类型。使用字典而非三维数组是为了节省内存因为在一个巨大的三维空间中大部分区域是空气空方块。我们定义了几个常量来表示方块类型# block_types.py AIR 0 GRASS 1 DIRT 2 STONE 3 WOOD 4 LEAF 5World类负责管理这个世界。它主要提供以下方法get_block(x, y, z): 获取指定坐标的方块类型如果坐标不存在于字典中则返回AIR。set_block(x, y, z, block_type): 在指定坐标放置方块。如果block_type是AIR则从字典中删除该键值对代表破坏方块。load_chunk(cx, cz): 加载以区块坐标(cx, cz)为中心的区块。这是地图动态加载的关键我们不会一次性生成整个世界。generate_terrain(chunk_x, chunk_z): 地形生成函数。这是沙盒游戏的灵魂之一。我实现了一个简单的基于Perlin噪声的高度图生成器。对于给定的区块我们为每个(x, z)坐标生成一个地面高度height然后从下往上填充方块最底层是石头中间几层是泥土最顶层是草方块。还可以加入矿脉石头中随机替换为煤、铁矿石ID、树木在草方块上生成木头和树叶方块的生成逻辑。# world.py 片段 - 简化版地形生成 import noise # 需要安装perlin-noise库 class World: def __init__(self): self.blocks {} self.seed random.randint(0, 10000) def generate_terrain(self, start_x, end_x, start_z, end_z): for x in range(start_x, end_x): for z in range(start_z, end_z): # 使用Perlin噪声生成高度 height int(noise.pnoise2(x * 0.1, z * 0.1, octaves4, persistence0.5, lacunarity2.0, repeatx1024, repeaty1024, baseself.seed) * 10 20) for y in range(height): if y height - 1: block_id GRASS elif y height - 5: block_id DIRT else: block_id STONE self.set_block(x, y, z, block_id) # 小概率在草方块上生成树 if random.random() 0.01 and self.get_block(x, height-1, z) GRASS: self._generate_tree(x, height, z)3.2 玩家控制器与物理模拟玩家控制器Player类负责处理所有与玩家实体相关的逻辑包括移动、视角、碰撞检测和与方块的交互。状态存储玩家对象保存位置(x, y, z)速度(vx, vy, vz)视角偏航角yaw左右看、俯仰角pitch上下看以及一个标志on_ground表示是否在地面上。移动处理在update方法中我们根据键盘输入WASD计算一个在水平面上的移动方向向量然后根据玩家的yaw角旋转这个向量使其相对于视角方向正确。接着将速度叠加到位置上。一个简单的重力模拟是如果玩家不在地面vy就持续减去一个重力常量如果检测到脚下有方块则将vy置零并设置on_ground True。碰撞检测这是实现“实体感”的关键。我们采用AABB轴对齐包围盒碰撞检测。将玩家视为一个1.8个单位高、0.6个单位宽和长的长方体。在移动前我们检测目标位置附近的所有非空气方块检查玩家的包围盒是否与之相交。如果相交则在对应的轴x, y, z上阻止移动。一个更精细的实现是分别处理每个轴实现“滑动”碰撞避免卡在角落。方块交互通过鼠标点击来放置或破坏方块。我们需要将屏幕上的鼠标坐标(mx, my)反向投射到游戏世界中找到玩家视线击中的第一个方块面。这可以通过发射一条从玩家眼睛位置出发沿视线方向的射线并逐步推进检测来实现称为“射线步进”算法。一旦找到击中点就可以根据鼠标按键左键破坏右键放置来调用world.set_block。# player.py 片段 - 简化的移动和碰撞检测 class Player: def __init__(self, pos(0, 20, 0)): self.x, self.y, self.z pos self.vx self.vy self.vz 0.0 self.yaw 0.0 # 偏航角弧度 self.pitch 0.0 # 俯仰角弧度 self.height 1.8 self.width 0.6 self.on_ground False def update(self, dt, world, move_forward, move_backward, move_left, move_right, jump): # 1. 处理水平移动输入 move_dir pygame.math.Vector2(0, 0) if move_forward: move_dir.y 1 if move_backward: move_dir.y - 1 if move_left: move_dir.x - 1 if move_right: move_dir.x 1 if move_dir.length_squared() 0: move_dir.normalize_ip() # 根据yaw角旋转移动方向 rotated_dir move_dir.rotate_rad(self.yaw) self.vx rotated_dir.x * PLAYER_SPEED self.vz rotated_dir.y * PLAYER_SPEED # 注意在3D中屏幕深轴是z但这里我们的地面是x-z平面y是高度。在2.5D投影中我们常把世界y当作高度x和z当地面坐标。这里为清晰假设地面是x-z平面。 else: self.vx * 0.8 # 摩擦力 self.vz * 0.8 # 2. 处理跳跃和重力 if jump and self.on_ground: self.vy JUMP_FORCE self.on_ground False if not self.on_ground: self.vy - GRAVITY * dt else: self.vy max(0, self.vy) # 确保在地面时不向下加速 # 3. 尝试移动并检测碰撞 (简化版仅检测y轴) new_y self.y self.vy * dt # 检测脚部和头部可能碰撞的方块 if not self._check_collision_at(self.x, new_y, self.z, world): self.y new_y self.on_ground False else: # 发生碰撞 if self.vy 0: # 向上撞到头 self.vy 0 else: # 向下落到地面 self.vy 0 self.on_ground True # 微调位置到刚好不碰撞 self.y int(self.y) (1 if self.vy 0 else 0) # 简化处理 # 类似地处理x和z轴的移动和碰撞...3.3 2.5D渲染器的实现细节渲染器Renderer类是连接数据世界和屏幕画面的桥梁。它的主要工作是在每一帧根据玩家视角和世界状态将方块画到pygame.Surface上。视锥裁剪与绘制顺序首先确定需要绘制的方块范围。以玩家为中心只绘制一定距离内的方块例如渲染距离为10个区块。然后为了正确处理遮挡我们必须按从远到近的顺序绘制。在我们的等距投影中这意味着需要按(x z)的和进行排序和值大的更靠近屏幕右下角即“更近”后绘制。更准确的方法是对每个待绘制的方块计算其屏幕深度通常就是(x z - y * 倾斜因子)然后按深度值从大到小排序后绘制。单个方块的绘制对于每个方块我们根据其世界坐标(wx, wy, wz)计算其屏幕基准坐标(sx, sy)。然后检查其六个邻接方块顶面如果(wx, wy1, wz)是空气则绘制顶面纹理。顶面的屏幕坐标是(sx, sy - tile_height)因为y增加在世界中是向上在屏幕中是向上偏移。正面假设朝向玩家的一面需要根据玩家视角决定哪个面是“正面”。在固定视角的2.5D中我们通常只绘制朝向南和东的面假设视角是西北方向。检查(wx, wy, wz1)和(wx1, wy, wz)是否为空气来决定是否绘制对应的侧面纹理。绘制时可以使用pygame.Surface.blit将对应的纹理图片根据方块ID和面类型索引绘制到屏幕Surface的对应位置。纹理映射与图集为了提高绘制效率我们将所有方块的纹理整合到一张大图上称为纹理图集Texture Atlas。这样只需要加载一次图片在绘制时通过计算纹理坐标UV坐标来截取对应的部分。例如草方块的顶面、泥土侧面等都有各自在图集上的矩形区域。简单光照与阴影为了增加视觉层次我们可以为每个面应用一个基础亮度。例如顶面亮度为1.0朝东的面为0.8朝南的面为0.6底面不绘制。这可以在blit时通过一个带有透明度的彩色遮罩来实现texture.fill((light, light, light, 255), special_flagspygame.BLEND_MULT)。# renderer.py 片段 - 核心绘制循环 class Renderer: def __init__(self, screen_width, screen_height): self.screen pygame.display.set_mode((screen_width, screen_height)) self.texture_atlas self._load_texture_atlas() # 加载纹理图集 self.tile_w 64 # 方块贴图宽度 (屏幕像素) self.tile_h 32 # 方块贴图高度 (屏幕像素) def render(self, world, player): self.screen.fill((135, 206, 235)) # 填充天空色 # 1. 收集需要绘制的方块 render_list [] RENDER_DISTANCE 20 px, py, pz int(player.x), int(player.y), int(player.z) for dx in range(-RENDER_DISTANCE, RENDER_DISTANCE1): for dz in range(-RENDER_DISTANCE, RENDER_DISTANCE1): for wy in range(0, WORLD_HEIGHT_LIMIT): # 假设世界高度有限 wx px dx wz pz dz block_id world.get_block(wx, wy, wz) if block_id ! AIR: # 计算屏幕深度用于排序 depth wx wz - wy * 0.5 # 简单的深度计算考虑了高度影响 render_list.append((depth, wx, wy, wz, block_id)) # 2. 按深度从远到近排序 render_list.sort(keylambda item: item[0]) # 3. 绘制每个方块 for depth, wx, wy, wz, block_id in render_list: self._draw_block(wx, wy, wz, block_id, world) def _draw_block(self, wx, wy, wz, block_id, world): # 计算方块基准屏幕坐标 (等距投影) sx (wx - wz) * (self.tile_w // 2) SCREEN_CX sy (wx wz) * (self.tile_h // 2) - wy * (self.tile_h // 2) SCREEN_CY # 检查并绘制顶面 if world.get_block(wx, wy1, wz) AIR: top_texture_rect self._get_texture_rect(block_id, top) self.screen.blit(self.texture_atlas, (sx, sy - self.tile_h//2), top_texture_rect) # 检查并绘制正面 (假设是南面即z1方向) if world.get_block(wx, wy, wz1) AIR: front_texture_rect self._get_texture_rect(block_id, front) # 正面绘制位置需要偏移 self.screen.blit(self.texture_atlas, (sx, sy), front_texture_rect) # ... 绘制其他面3.4 资源管理与游戏主循环一个完整的游戏还需要资源管理、状态机和流畅的主循环。资源加载我们将所有贴图、声音文件放在项目的assets文件夹下。使用pygame.image.load和pygame.mixer.Sound来加载它们。为了管理方便可以写一个ResourceManager单例类在游戏初始化时加载所有资源并提供接口供其他模块获取。游戏状态游戏可能有不同状态如MENU、PLAYING、PAUSED。用一个简单的状态机来管理每个状态有对应的enter、handle_events、update、render和exit方法。主循环这是游戏的心脏。它以一个固定的频率如60 FPS运行依次处理事件、更新游戏逻辑、渲染画面。使用pygame.time.Clock来控制帧率至关重要。# main.py - 游戏主循环骨架 def main(): pygame.init() screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(PyCraft - A Lightweight Minecraft-like Sandbox) clock pygame.time.Clock() running True # 初始化游戏世界、玩家、渲染器 world World() player Player() renderer Renderer(SCREEN_WIDTH, SCREEN_HEIGHT) # 生成初始地形 world.generate_initial_terrain(player.x, player.z) while running: dt clock.tick(60) / 1000.0 # 获取上一帧耗时单位秒 # 1. 事件处理 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_ESCAPE: running False # ... 处理其他按键 elif event.type pygame.MOUSEBUTTONDOWN: # 处理方块放置/破坏 handle_block_interaction(event, player, world) # 2. 更新游戏状态 keys pygame.key.get_pressed() player.update(dt, world, move_forwardkeys[pygame.K_w], move_backwardkeys[pygame.K_s], move_leftkeys[pygame.K_a], move_rightkeys[pygame.K_d], jumpkeys[pygame.K_SPACE]) # 更新玩家视角通过鼠标移动 mouse_dx, mouse_dy pygame.mouse.get_rel() player.yaw mouse_dx * MOUSE_SENSITIVITY player.pitch - mouse_dy * MOUSE_SENSITIVITY player.pitch max(-89, min(89, player.pitch)) # 限制俯仰角 # 3. 渲染 renderer.render(world, player) # 可以在这里渲染UI如十字准心、背包栏 draw_crosshair(screen) pygame.display.flip() pygame.quit() if __name__ __main__: main()4. 项目运行、配置与扩展指南4.1 环境搭建与项目运行要让这个项目跑起来你需要准备好Python环境和必要的库。安装Python确保你的电脑安装了Python 3.7或更高版本。可以从Python官网下载安装程序。安装时务必勾选“Add Python to PATH”这样可以在命令行直接使用python和pip命令。安装PyGame打开命令行Windows上是CMD或PowerShellMac/Linux上是Terminal运行安装命令。推荐使用pip进行安装pip install pygame如果你遇到网络问题可以使用国内镜像源加速例如pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple获取项目代码与资源你可以从提供的链接下载完整的项目压缩包。解压后你会看到类似如下的目录结构PyCraft/ ├── main.py # 游戏主入口 ├── world.py # 世界生成与管理 ├── player.py # 玩家控制与物理 ├── renderer.py # 2.5D渲染器 ├── block_types.py # 方块类型定义 ├── resources.py # 资源管理器 ├── assets/ # 资源文件夹 │ ├── textures.png # 纹理图集 │ └── ... # 其他音效等 └── requirements.txt # 依赖库列表运行游戏在命令行中进入项目根目录PyCraft文件夹然后运行python main.py如果一切顺利一个游戏窗口将会弹出你就能用WASD移动鼠标环顾四周点击鼠标左键破坏方块右键放置方块了。实操心得如果你在运行时报错“No module named pygame”说明PyGame没有安装成功。请确认你用来运行pip install的Python和用来运行python main.py的Python是同一个版本。在命令行中分别输入python --version和pip --version查看路径有时系统有多个Python版本会导致混淆。一个稳妥的方法是使用虚拟环境venv。4.2 核心参数调优与性能提升游戏的可玩性和流畅度很大程度上取决于一些核心参数的设置。你可以在代码开头的常量定义部分找到并调整它们RENDER_DISTANCE渲染距离在renderer.py中。它决定了你能看到多远的世界。增大它会让你看得更远但会显著增加需要绘制的方块数量降低帧率。建议从10-15开始根据你的电脑性能调整。性能优化的一个关键就是减少不可见方块的绘制。WORLD_HEIGHT_LIMIT世界高度限制在world.py中。定义了世界的最大高度Y轴。降低它可以减少地形生成和碰撞检测的计算量。PLAYER_SPEED玩家移动速度和MOUSE_SENSITIVITY鼠标灵敏度在player.py或main.py中。根据你的手感调整。GRAVITY重力和JUMP_FORCE跳跃力在player.py中。调整它们可以改变游戏的“物理感觉”让跳跃更轻盈或更沉重。性能提升技巧区块加载不要每一帧都遍历整个世界。将世界划分为固定大小的“区块”例如16x16x256只加载和渲染玩家周围几个区块内的方块。当玩家移动时动态加载新的区块卸载远离的区块。显示列表/批处理绘制PyGame的blit操作有一定开销。如果大量方块使用相同的纹理可以考虑先将它们绘制到一个离屏的Surface上或者使用pygame.sprite.Group进行批处理。但在我们这个2.5D等距绘制中每个方块位置和深度都不同批处理优化效果有限主要优化点还是减少绘制数量。简化碰撞检测玩家的碰撞检测不需要和所有方块进行精确的AABB检测。可以先进行粗略的空间划分如将世界划分为更大的网格只检测玩家所在网格及相邻网格中的方块。4.3 常见问题排查与解决在开发或运行过程中你可能会遇到以下典型问题问题现象可能原因解决方案游戏窗口一闪而过或立即关闭1. Python脚本有语法错误。2. 缺少必要的依赖库如noise。3. 资源文件路径错误。1. 在命令行中运行查看具体的错误信息。2. 运行pip install noise安装Perlin噪声库。3. 检查assets文件夹是否在正确位置纹理文件名是否与代码中一致。移动或视角控制不灵敏/奇怪1. 事件处理逻辑有误按键状态没正确获取。2. 鼠标被系统捕获pygame.mouse.get_rel()返回值异常。3. 移动速度PLAYER_SPEED或灵敏度MOUSE_SENSITIVITY参数设置不当。1. 确认在游戏循环中正确调用了pygame.key.get_pressed()。2. 在游戏初始化后调用pygame.mouse.set_visible(False)并可能调用pygame.event.set_grab(True)来锁定鼠标注意按ESC退出。3. 调整相关常量值。MOUSE_SENSITIVITY通常是一个很小的浮点数如0.001。放置或破坏方块的位置不对射线步进算法Ray Casting有bug计算出的击中点不准。调试射线步进函数。可以临时在击中点用特殊方块如发光的金块标记出来看看是否和鼠标指向一致。检查射线起点玩家眼睛位置和方向根据yaw和pitch计算是否正确。帧率FPS很低游戏卡顿1. 渲染距离过大绘制方块太多。2. 地形生成算法在每帧被调用或碰撞检测过于复杂。3. 纹理图片过大或没有使用纹理图集导致频繁切换纹理。1. 减小RENDER_DISTANCE。2. 确保地形生成只在区块首次加载时进行。优化碰撞检测使用空间划分。3. 确保所有纹理整合在一张图集里并确认图集尺寸合理如1024x1024。方块有闪烁或绘制顺序错误方块绘制顺序深度排序算法不正确。远处的方块被近处的方块挡住了。检查renderer.py中的render_list排序逻辑。深度计算必须考虑所有三个坐标轴。确保是按深度值从小到大远到近绘制。可以尝试简化深度公式为depth wx wz - wy。一个深度避坑技巧浮点数精度问题。在长时间运行或玩家移动很远后可能会因为浮点数精度累积导致奇怪的渲染错位或碰撞检测失效。一个常见的解决方案是使用“原点重定位”Origin Rebase。定期例如当玩家移动超过一定距离后将整个世界数据平移使玩家坐标始终在原点附近。这需要更新所有已加载区块的世界坐标和重新计算渲染位置实现起来较复杂但对于打造一个“无限世界”体验是必要的。5. 项目扩展与创意发挥方向这个基础框架搭建好后你可以像玩乐高一样添加各种功能来丰富你的世界。更多方块与物品系统在block_types.py中添加新的方块ID和属性如硬度、工具类型、掉落物。实现一个背包系统Inventory类当破坏方块时有概率掉落物品ItemEntity玩家走近可以拾取。右键使用物品如工具、火把可以触发不同效果。简单的生存机制为玩家添加生命值、饥饿值。添加敌对生物Mob类它们有简单的AI如朝玩家移动、攻击。添加昼夜循环夜晚刷怪。这需要引入游戏时间系统和一个更复杂的实体管理模块。改进的渲染与光影目前的渲染是固定视角的2.5D。可以挑战实现一个真正的第一人称3D渲染。这需要学习一些基础的3D图形学知识使用矩阵变换进行透视投影。或者在现有2.5D基础上加入动态光源如火把每个方块面的亮度根据附近光源的距离衰减计算。保存与加载游戏使用pickle或json模块将world.blocks字典和玩家状态序列化到文件中。可以实现按区块保存每次进入游戏时只加载玩家周围的区块。合成与建造系统实现一个像《我的世界》那样的2x2或3x3合成台。当玩家将特定物品按图案放入合成网格时可以合成出新物品或方块。这需要设计一套合成配方数据表。这个项目最大的乐趣在于它只是一个起点。每一行代码你都能理解并修改。你可以把它改造成一个地下城探险游戏、一个城市规划模拟器或者只是一个让你放松的虚拟积木桌。从理解“一个方块如何被画到屏幕上”开始到你亲手为这个世界添加第一条会飞的龙这中间的每一步都是游戏开发最纯粹的快乐。代码和资源都准备好了剩下的就看你的想象力了。