ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity 2D小游戏开发实战:从Tilemap搭建到角色控制与优化

2026/9/13 2:39:39 拓冰建站 浏览量
Unity 2D小游戏开发实战:从Tilemap搭建到角色控制与优化 简介一套完整的 Unity 2D 小游戏项目工程对应作者的第一个实验作品 FirstGame2D(Sunny Land)面向刚接触 Unity 的初学者帮助理解一个 2D 小游戏从场景搭建到逻辑控制的全过程。包内含 C# 脚本、场景、预制体、动画控制器、材质、音频等共 12161 个文件压缩包大小约 167.38MB。项目中实现了简单 UI 界面及滑动按钮事件通过按钮和按键切换场景用代码控制跳跃和蹲下等动画切换基于碰撞体与触发器完成消灭敌人、收集物品和地面检测还通过相机与背景运动差营造立体视觉效果并由 SoundManager 统一管理场景音乐。源码与资源文件较为完整目录结构清晰便于逐模块对照学习 Unity 2D 开发中的常用交互、动画和物理机制。已有 1831 人学习下载适合想要从零掌握 Unity 2D 小游戏设计与编码逻辑的开发者参考。1. 从 FirstGame2D 说起Unity 2D 小游戏的技术骨架Sunny Land 这个项目名一眼看就是要在阳光色的场景里跳跃、收集、跑到终点。很多人第一次接触 Unity 拿到这个题目第一件事是拖几个方块进场景然后开始写角色移动结果被 2D 世界的坐标、层级和物理参数反复折腾。我的习惯是先定技术骨架2D 小游戏的循环其实很固定场景搭建、角色控制、物理交互、数值与 UI、发布优化。Unity 对这个流程支持得相当完整Sprite、Tilemap、Physics2D、Canvas 和预制体各司其职按顺序搭可以少走很多弯路。这篇文章直接按照 FirstGame2D(Sunny Land) 这个目标动手给出一个能编译能运行的基础方案。如果你是刚接触 Unity照抄再改参数也足够跑起来如果是老手重点看跳跃手感调参和发布前那些不常注意的坑。2. Unity 2D 项目结构精灵导入、像素校准与 Tilemap 场景搭建在写任何游戏逻辑之前先把 Sunny Land 的地面、平台和背景摆到场景里。这一章解决三件事项目目录怎么分、素材怎么切成可用 Sprite、Tilemap 怎么刷出带碰撞的地形。除非你做的游戏完全靠程序化生成否则这些步骤一个都跳不过。2.1 创建 2D 工程并规划 Assets 目录Unity Hub 里新建项目时选择 2D 模板模板已经把渲染管线、默认正交相机和 2D 物理模式配好。如果打开后找不到 Tilemap 菜单说明模板选错成了 3D需要手动在 Project Settings 里切换颜色空间并给相机改为正交。项目名称直接叫 FirstGame2D后续脚本命名空间也会沿用这个前缀避免类名冲突。创建完成后第一件事不是写代码而是先在 Assets 下建好目录结构Assets/ Scenes/ Scripts/ Art/ Sprites/ Tiles/ Animations/ Prefabs/Art/Sprites 存放原始切图Art/Tiles 存放 Tile 资产Prefabs 用来放玩家、敌人、金币这类需要反复实例化的对象。目录分清楚对 2D 项目特别重要因为美术资源、图集和预制体一旦混在一起几周后你自己都找不到该改哪个文件。多人协作时目录整洁程度直接决定版本合并的冲突数量。2.2 素材导入与像素校准Pixels Per Unit 怎么设Sunny Land 如果走像素风素材导入设置错了会变成模糊一团。选中图集图片在 Inspector 里按顺序改四个选项Texture Type 设为 Sprite (2D and UI)Sprite Mode 设为 Multiple前提是图集里包含多个帧Pixels Per Unit 设为美术资源设计值常见是 16 或 32Filter Mode 设为 Point (no filter)并且 Compression 选 NonePixels Per UnitPPU是像素校准的核心它决定 1 个 Unity 单位对应多少个像素。16x16 的素材PPU 设 16放到场景里正好 1 单位宽PPU 设 32大小缩到 0.5 单位。所有物理参数都以这个单位为基准所以项目开始前最好一次性确定公共 PPU否则后面对 Tilemap 的 Cell Size 和相机尺寸全都要跟着乱套。PPU16x16 素材显示尺寸32x32 素材显示尺寸161 Unity 单位2 Unity 单位320.5 Unity 单位1 Unity 单位切 Multiple 图集时打开 Sprite Editor选择 Slice 的 Grid by Cell Size把 Pixel Size 填成素材实际尺寸例如 16x16然后点击 Slice。这里默认按图片尺寸切如果图片有留白切出来的 Sprite 会包含透明边刷 Tilemap 时会出现接缝。切完把每个 Sprite 的 Mesh Type 设为 Full Rect避免透明区域裁剪导致边缘闪烁。2.3 用 Tilemap 搭建 Sunny Land 地形并合并碰撞菜单栏选择 GameObject 2D Object Tilemap场景里会出现一个 Grid 加一个 Tilemap 子节点。打开 Window 2D Tile Palette新建一个 Palette把切好的 Sprite 拖进 Palette 窗口Unity 会在指定目录生成 Tile 资产。刷地形之前先确认 Grid 组件的 Cell Size 是 1,1,0并且每个 Tile 对应正确的精灵。如果刷出来后地面尺寸和角色比例不对回头查 PPU 有没有统一。地形刷好后选中 Tilemap 节点添加 Tilemap Collider 2D。这个组件会给画到网格里的每个 Tile 都生成独立碰撞体但 Tile 之间接缝处偶尔会在角色高速移动时产生轻微卡顿。我一般会再加一个 Rigidbody2D设为 Static然后添加 Composite Collider 2D。关键步骤是把 Tilemap Collider 2D 的 Used By Composite 勾上让所有小块合并成一个大碰撞体Tilemap Collider 2D - Used By Composite: true Rigidbody2D (Static) - Body Type: Static Composite Collider 2D - Geometry Type: Polygons这样物理引擎只维护一个多边形边界角色在地上的表现稳定得多也降低了用到大量 Tile 时的性能损耗。背景层如果不需要碰撞放在另一个没有 Collider 的 Tilemap 或 Sprite 上把 Sorting Order 设为 -1。2.4 相机、背景色和 2D 光照Sunny Land 的“Sunny”靠背景色和光照体现。将 Main Camera 的 Projection 设为 OrthographicSize 决定屏幕垂直方向能看到多少单位。Size5 时屏幕从中心到上边缘是 5 单位总共显示 10 单位高度。这个值要配合关卡高度和 PPU 设置不然跳起来角色就出屏。背景可以用一个纯色块也可以用天空素材当 Sprite置于 Sorting Order 的最底层。如果用 URP 模板新建的 2D 项目记得检查场景里有没有 Global Light 2D。没有光照的话所有 Sprite 会以暗色显示。内置渲染管线则在 Camera 的 Clear Flags 里选 Solid Color把背景色改成浅蓝这一步能让整体画面立刻亮起来。3. Unity 2D 玩家角色Rigidbody2D 参数、移动跳跃代码与碰撞检测场景能看了接下来的问题是手感。Sunny Land 是平台跳跃角色控制直接决定玩家会不会留在游戏里。这一章我把组件挂载顺序、移动代码、跳跃参数和敌人碰撞分开讲方便你单独调每一项。3.1 先搭好 Player 对象的组件顺序创建一个空对象命名 Player把角色精灵挂上Sorting Order 设成 1。然后按顺序添加组件Rigidbody2DBody Type 设为 Dynamic这是角色受重力影响的基础。CapsuleCollider2D调整 Size 和 Offset让碰撞体覆盖角色身体主体而不含透明边界。不要勾 Is Trigger真正的物理碰撞靠它。在 Player 下新建子物体 GroundCheck给它挂一个 BoxCollider2D勾 Is Trigger大小贴近脚底。这个触发器只用来检测地面。这样分离物理碰撞和检测比每帧向脚下发射射线更稳定。Rigidbody2D 上不要直接挂物理材质特别是摩擦力大的材质会让角色卡在墙上墙和地面的 Tilemap 碰撞可以留默认零摩擦。角色落地时如果轻微粘滞检查 GroundCheck 是否超出角色底边的位置。3.2 移动与跳跃的 C# 代码实现写作代码的时候关键是把水平速度交给输入把垂直速度交给物理。新建PlayerController.cs挂到 Player 根节点using UnityEngine; public class PlayerController : MonoBehaviour { [Header(移动参数)] public float moveSpeed 8f; public float jumpForce 12f; public float gravityScale 3f; [Header(组件引用)] private Rigidbody2D rb; private Collider2D groundCheck; private bool isGrounded; private void Awake() { rb GetComponentRigidbody2D(); groundCheck GetComponentInChildrenCollider2D(); rb.gravityScale gravityScale; } private void Update() { float inputX Input.GetAxisRaw(Horizontal); rb.velocity new Vector2(inputX * moveSpeed, rb.velocity.y); bool jumpPressed Input.GetButtonDown(Jump) || Input.GetKeyDown(KeyCode.Space); if (jumpPressed isGrounded) { rb.velocity new Vector2(rb.velocity.x, jumpForce); } } private void OnTriggerStay2D(Collider2D other) { isGrounded other.CompareTag(Ground); } private void OnTriggerExit2D(Collider2D other) { if (other.CompareTag(Ground)) { isGrounded false; } } }代码逻辑说明几处关键选择Input.GetAxisRaw返回 -1、0、1 三个离散值手感非常“脆”适合平台游戏。想要平滑加速就改用GetAxis再用 Mathf.Lerp 把当前速度插值到目标速度。Update 里覆盖rb.velocity.x保留rb.velocity.y。物理引擎继续负责重力迭代和碰撞约束不会因为你的指令把下落速度清零。跳跃同样直接设置velocity.y而不是调用AddForce。给固定速度能保证每帧起跳高度一致不受帧率影响。OnTriggerStay2D在物理回调阶段运行不会在 Update 里和输入竞争状态。但要注意触发器退出时如果和跳跃同一帧可能会错过一次起跳。优化办法是把OnTriggerExit2D里的设置延迟一帧通常没必要先按这个能用的版本跑。注意如果项目启用了新的 Input System默认会让旧接口全部失效。需要在 Player Settings 里把 Active Input Handling 设为 Both或者改用 Input System 包中的 API否则Input.GetAxisRaw会抛异常。3.3 跳跃手感的核心参数表平台游戏手感好不好两个数决定大部分gravityScale 和 jumpForce。Unity 2D 的物理单位是米/秒重力加速度默认是 -9.81。这里给一组从零调通的参考值参数数值说明Gravity Scale3.0数值越高下落越重跳跃越短促Jump Force12峰值上升速度配合重力决定跳跃高度Move Speed8水平移动速度超过 12 会容易冲出平台Linear Drag0保持 0不要在刚体上加拖动InterpolateInterpolate减少相机跟随时的抖动跳跃高度大概是jumpForce * jumpForce / (2 * 9.81 * gravityScale)代入后约 2.4 米正好两到三块砖的高度。调整时先改 gravityScale别急着改 jumpForce。高 gravityScale 配适中 jumpForce 会比低 gravityScale 配高 jumpForce 更容易做出短促的起跳玩家按一下就能快速下落手感更利落。3.4 敌人碰撞和踩踏判定敌人的基础移动可以放在单独的 EnemyMover 里用transform.Translate每帧朝一个方向走撞墙后翻转方向。角色与敌人的碰撞分两种情况从上面踩到敌人、被敌人侧面撞到。我的做法是在角色脚底再放一个踩踏触发器而不是在主碰撞体里判断。示例代码放在玩家脚本里private void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag(Enemy)) return; float hitDirection transform.position.y - other.transform.position.y; if (hitDirection 0.3f) { rb.velocity new Vector2(rb.velocity.x, jumpForce * 0.6f); other.GetComponentEnemy()?.Hurt(); } else { GameManager.Instance.KillPlayer(); } }hitDirection用世界坐标的 y 差判断相对位置阈值 0.3 约等于半块砖的高度。这里踩踏反弹力只取了跳跃力的一半避免踩中敌人后弹到屏幕外。如果 Enemy 不挂脚本GetComponentEnemy()会返回 null空条件调用不会报错。别忘了 GameManager 的KillPlayer要到下一章才实现这里先留接口。4. Sunny Land 游戏循环收集物、生命值与 UI 状态管理从这一章开始Sunny Land 才算“可玩”。金币、生命、计分、重生的实现模式是固定的关键在于怎么把对象间的引用减到最少。4.1 金币的触发收集与对象销毁在 Assets 里做一枚金币预制体放一个 Sprite、一个 BoxCollider2D 且 Is Trigger 勾上再挂上 Coin.cs。触发式收集好处是不用管碰撞方向玩家碰到就生效。实例化可以先用Instantiate后续章节再考虑对象池。using UnityEngine; public class Coin : MonoBehaviour { public int value 1; private void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag(Player)) return; GameManager.Instance.AddCoins(value); Destroy(gameObject); } }代码里最先判断CompareTag防止金币被敌人的 GroundCheck 或者子弹碰到后直接消失。Destroy 是延迟到当前帧结束的不会有访问已销毁对象的立即异常。如果你在AddCoins里同步刷新 UITransform 和 UI 之间没有引用关系更新时机很安全。4.2 用 GameManager 管理玩家状态多个脚本都想知道玩家加了多少金币、剩下几条命直接互相引用会越写越乱。我做了一个全局唯一的 GameManager 挂在场景空物体上单例模式只暴露Instanceusing UnityEngine; public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } public int coins { get; private set; } public int lives { get; private set; } public Transform spawnPoint; private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; } public void AddCoins(int amount) { coins amount; UIManager.Instance.UpdateCoinText(coins); } public void KillPlayer() { lives--; UIManager.Instance.UpdateLivesText(lives); if (lives 0) { UIManager.Instance.ShowGameOver(); } else { GameObject player GameObject.FindWithTag(Player); player.transform.position spawnPoint.position; } } }coins和lives用private set外部只能读取不能直接赋值避免哪里冒出来的脚本把生命改成负数。spawnPoint在 Inspector 里拖一个空物体进去重生点美术就可以自己调。需要注意的是这个 GameManager 不会跨场景保留。如果 Sunny Land 有多个关卡场景要把 GameManager 做成DontDestroyOnLoad并处理场景切换时的事件注册。小游戏单场景内这样已经够用。4.3 Canvas 与 UI 数值更新UI 用 Unity 的 uGUI 系统。创建 UI - Canvas 后把 Canvas Scaler 的 UI Scale Mode 选为 Scale With Screen Size参考分辨率设 1920x1080。这样窗口比例变化时 UI 缩放一致不会在竖屏手机上一片乱。UIManager 挂到 Canvas 上直接引用两个 Text 和一个结束面板using UnityEngine; using UnityEngine.UI; public class UIManager : MonoBehaviour { public static UIManager Instance { get; private set; } public Text coinText; public Text livesText; public GameObject gameOverPanel; private void Awake() { Instance this; gameOverPanel.SetActive(false); } public void UpdateCoinText(int coins) { coinText.text $金币: {coins}; } public void UpdateLivesText(int lives) { livesText.text $生命: {lives}; } public void ShowGameOver() { gameOverPanel.SetActive(true); Time.timeScale 0f; } }UI 更新全部由事件驱动不做每帧轮询。Time.timeScale 0f让场景内除了 UI 外的 Update 和物理模拟全部暂停游戏结束界面自然停住。重新开始时记得在 Start 或点击按钮的处理函数里把 timeScale 设回 1。4.4 状态切换菜单、游戏进行中与结束游戏循环至少要分清三个状态。用一个枚举比用散乱的 bool 清楚得多public enum GameState { MainMenu, Playing, GameOver }在 GameManager 里放一个public GameState state菜单按钮点击后设成 Playing同时隐藏菜单面板。ShowGameOver里设成 GameOver。PlayerController 的 Update 开头可以做一次状态判断避免游戏结束后角色还在响应输入。一个容易漏的细节游戏结束面板重新开始时要把 Player 的 velocity 清零并移动到 spawnPoint否则角色会从旧位置继续下落。状态切换的目的是把这类初始化都集中到RestartLevel()方法里而不是散落在 UI 按钮事件中。这样做之后无论菜单按钮还是按键控制重开都走同一段逻辑。5. Unity 2D 发布前验证像素校准、对象池优化和 WebGL 存档告警Sunny Land 到了能玩的阶段剩下的是让它在不同设备上看起来正常、跑起来不卡。这一章直接列可以逐项检查的做法。5.1 像素级校准让 2D 画面不抖动2D 横版游戏在相机移动时经常出现像素抖动根因是相机位置没有对齐到像素网格。Unity 提供了 Pixel Perfect Camera 组件通过 Package Manager 搜索 2D Pixel Perfect 并安装挂到相机上它会自动把渲染分辨率对齐到目标像素。启用后相机不再使用分数像素位置场景边缘清晰很多。如果没有用 Pixel Perfect 包可以手动设置正交相机的 sizesize 屏幕垂直像素数 / (2 * PPU)。例如 PPU 16垂直分辨率为 480size 就是 15。这个数值必须能被整除不能有小数。调整后移动角色画面边缘不会再出现半像素的模糊线。这条规则对所有像素风小游戏都适用。5.2 Unity 游戏优化物理步长、合并碰撞和对象池打开 Project Settings Physics 2D有一个被大多数新项目忽视的参数Fixed Timestep默认 0.02 秒对应 50Hz 物理更新。50Hz 对平台跳跃足够。如果角色高速穿过薄墙先检查刚体的 Collision Detection 是否改成了 Continuous而不是把 Fixed Timestep 减小到 0.01因为那会让 CPU 负担翻倍。金币这类高频创建销毁对象建议用对象池替代Destroy。最简单的实现是多两个队列public class CoinPool : MonoBehaviour { public GameObject coinPrefab; private readonly QueueCoin pool new QueueCoin(); public Coin Get() { if (pool.Count 0) { Coin coin pool.Dequeue(); coin.gameObject.SetActive(true); return coin; } return Instantiate(coinPrefab).GetComponentCoin(); } public void Return(Coin coin) { coin.gameObject.SetActive(false); pool.Enqueue(coin); } }这样从Get得到的对象用完以后回到Return不再和 GC 打交道。如果你的目标平台是 WebGL频繁Instantiate和Destroy会放大内存碎片这个优化尤其值得做。5.3 发布 WebGL 时 IDBFS 写入失败与核对表Unity 发布 WebGL 后存档默认走 IndexedDB页面处于无痕模式或浏览器存储策略较紧时File.WriteAllText这类调用可能直接失败。我一般在第一次写存档前先用一个轻量探针测试 IndexedDB 是否可用private bool CanWriteSave() { try { string path Application.persistentDataPath /probe.txt; System.IO.File.WriteAllText(path, test); return true; } catch { return false; } }探测失败就把存档降级为内存模式并在 UI 里给出“数据不会被保存”的提示而不是让游戏启动后反复弹异常。这个处理在隐私模式下的浏览器里非常常见纯粹是浏览器存储策略问题和游戏逻辑无关。发布前最终核对表相机 Size、PPU 和 Pixel Perfect 是否在目标分辨率下正常字符边缘没有半像素模糊。Tilemap 是否合并了碰撞角色高速移动时不会卡在 Tile 接缝处。Jump 和重力参数在 30 FPS 和 60 FPS 下手感一致没有起跳高度差异。金币和敌人的对象池有没有清理残留引用避免切场景后空对象堆积。WebGL 存档写入失败时有没有降级逻辑保证主流程仍可继续。本文还有配套的精品资源点击获取