简介:本资源是一份基于Unity3D 5.3+开发的经典吃豆人(Pac-Man)游戏完整源码,面向Unity初学者与中级开发者,聚焦游戏逻辑实现、角色行为控制与跨模块协同开发能力提升。压缩包为ZIP格式,大小23.89MB,包含场景文件、C#脚本、角色动画控制器、UI预制体、音频资源及幽灵AI状态机等核心内容,其中脚本承担游戏主控、输入响应、得分计算、碰撞判定与状态切换;场景构建了可运行的地图结构与初始对象布局;UI系统完整实现了生命值、分数实时更新与游戏状态提示。已有635人学习下载,适合通过逆向分析掌握Unity项目组织规范、物理引擎集成、有限状态机设计及简单A*寻路应用。源码结构清晰、注释充分,是理解2D/3D混合风格游戏开发流程的优质实践范例。
1. 为什么一个“吃豆人”Demo能帮你稳住Unity3D入门的第三关?
不是所有Unity新手都卡在C#语法上,很多人是在“做完第一个Cube旋转后,突然不会动了”——场景有了、模型拖进去了、脚本也挂上了,但角色不走、吃不到豆、鬼怪不追、分数不加。这种“功能断层”比报错更致命:没有红色错误提示,只有沉默的空白运行窗口。而“Unity3D 吃豆人 源码下载”这个搜索背后,藏着的其实是一套被验证过的、最小闭环的2D游戏架构范式:输入控制(键盘/手柄)→ 角色移动(网格导航+碰撞检测)→ 状态管理(幽灵AI模式切换)→ 数据驱动(豆子计数、生命值、关卡状态)→ UI同步(分数、剩余生命实时刷新)。它不追求炫技,但把Unity中Rigidbody2D、Collider2D、Animator、ScriptableObject、SceneManager这五大高频模块全串在一条业务流里。适合刚写完“点击变色”脚本、正准备接真实项目逻辑的开发者;也适合教学场景中需要一个可拆解、可替换、可压测的基准案例。别把它当怀旧玩具,它是你调试FixedUpdate和Update时序问题、排查OnTriggerEnter2D不触发、理解Tilemap与SpriteRenderer层级关系的“黑匣子解剖台”。
2. 从零搭建吃豆人核心骨架:用Unity 2021.3 LTS跑通最小可运行版本
吃豆人看似简单,但直接抄完整源码容易陷入“函数迷宫”——几十个脚本互相引用,改一处崩三处。我一般会先剥离出四个不可删减的核心组件,用最简代码搭出“能走、能吃、能死、能重开”的骨架。以下操作基于Unity 2021.3.34f1(LTS长期支持版),兼容2020.3+,避免高版本API变动带来的玄学报错。
2.1 创建可行走的玩家角色:刚体、碰撞器与输入绑定
新建2D项目后,第一步不是放地图,而是先让主角动起来。创建空GameObject命名为Player,添加Rigidbody2D(勾选Freeze Rotation Z防止翻转)、CircleCollider2D(半径设为0.35,匹配精灵尺寸)、SpriteRenderer(拖入吃豆人朝右的PNG图)。关键在移动脚本——不用协程,不用物理力,用Transform.Translate配合Input.GetAxisRaw实现像素级精准位移:
// PlayerMovement.cs using UnityEngine; public class PlayerMovement : MonoBehaviour { [Tooltip("每秒移动格子数,建议设为8-12")] public float moveSpeed = 10f; private Vector2 inputDir; private Vector2 targetPos; private bool isMoving = false; void Update() { // 仅在未移动时读取新输入(防止斜向卡死) if (!isMoving) { inputDir = Vector2.zero; if (Input.GetKey(KeyCode.LeftArrow)) inputDir += Vector2.left; if (Input.GetKey(KeyCode.RightArrow)) inputDir += Vector2.right; if (Input.GetKey(KeyCode.UpArrow)) inputDir += Vector2.up; if (Input.GetKey(KeyCode.DownArrow)) inputDir += Vector2.down; if (inputDir != Vector2.zero) { targetPos = transform.position + inputDir * moveSpeed * Time.deltaTime; isMoving = true; } } } void FixedUpdate() { if (isMoving) { // 线性插值逼近目标位置,避免抖动 transform.position = Vector2.Lerp(transform.position, targetPos, 0.2f); if (Vector2.Distance(transform.position, targetPos) < 0.01f) { transform.position = targetPos; isMoving = false; } } } }参数说明:
moveSpeed不是像素/秒,而是“每秒走过多少个标准格子”(后续地图按1x1单位铺)。Vector2.Lerp的0.2f是阻尼系数,值越大越生硬,越小越拖尾;0.01f是停止阈值,太小会导致浮点误差累积无法停止。此写法绕过Rigidbody2D.AddForce的物理计算开销,对吃豆人这种网格对齐游戏更稳定。
2.2 构建可吃的豆子系统:触发器、计分与关卡完成判定
吃豆人世界里,豆子不是装饰品,而是状态机的开关。创建Pellet预制体:空GameObject +CircleCollider2D(勾选Is Trigger)+SpriteRenderer(小圆点图)。关键在Pellet.cs脚本,它必须轻量且无副作用:
// Pellet.cs using UnityEngine; public class Pellet : MonoBehaviour { [Tooltip("被吃掉时增加的分数")] public int scoreValue = 10; void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("Player")) { // 广播事件,避免直接调用GameManager(解耦) GameManager.Instance.OnPelletEaten(scoreValue); Destroy(gameObject); } } }接着创建GameManager.cs单例管理全局状态(必须设为DontDestroyOnLoad):
// GameManager.cs using UnityEngine; using System.Collections.Generic; public class GameManager : MonoBehaviour { public static GameManager Instance; public int currentScore = 0; public int lives = 3; public int pelletsRemaining = 0; // 需在关卡加载时初始化 void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); DontDestroyOnLoad(gameObject); } public void OnPelletEaten(int value) { currentScore += value; pelletsRemaining--; if (pelletsRemaining <= 0) { Debug.Log("关卡完成!当前分数:" + currentScore); // 此处触发下一关或胜利UI } } public void PlayerDied() { lives--; if (lives <= 0) { Debug.Log("游戏结束"); // 触发Game Over UI } else { Debug.Log("剩余生命:" + lives); // 重置玩家位置等 } } }落地要点:
pelletsRemaining不能靠FindObjectsOfType<Pellet>().Length实时统计——关卡复杂时性能爆炸。正确做法是在关卡加载后,遍历所有Pellet并累加计数,存入GameManager。OnTriggerEnter2D必须配对Is Trigger=true,否则碰撞器会阻挡移动而非触发事件。
2.3 实现幽灵基础行为:状态机驱动的巡逻与追逐
幽灵(Ghost)是吃豆人AI的灵魂。新手常犯的错是写“if chasing then move toward player”,结果幽灵穿墙、瞬移、卡角。正确做法是网格路径预计算 + 状态驱动方向选择。先创建Ghost.cs:
// Ghost.cs using UnityEngine; public enum GhostState { Scatter, Chase, Frightened } public class Ghost : MonoBehaviour { public GhostState currentState = GhostState.Scatter; public Transform player; public float scatterTime = 7f; public float chaseTime = 20f; private float stateTimer = 0f; private Vector2[] scatterTargets = { new Vector2(-10, 10), new Vector2(10, 10), new Vector2(-10, -10), new Vector2(10, -10) }; private int scatterIndex = 0; void Update() { stateTimer += Time.deltaTime; if (currentState == GhostState.Scatter && stateTimer > scatterTime) { currentState = GhostState.Chase; stateTimer = 0f; } else if (currentState == GhostState.Chase && stateTimer > chaseTime) { currentState = GhostState.Scatter; stateTimer = 0f; scatterIndex = (scatterIndex + 1) % scatterTargets.Length; } } void FixedUpdate() { Vector2 target = GetTargetPosition(); MoveToward(target); } Vector2 GetTargetPosition() { switch (currentState) { case GhostState.Scatter: return scatterTargets[scatterIndex]; case GhostState.Chase: return player.position; case GhostState.Frightened: return (Vector2)transform.position + Random.insideUnitCircle * 5f; default: return Vector2.zero; } } void MoveToward(Vector2 target) { Vector2 direction = (target - (Vector2)transform.position).normalized; // 简化:只允许上下左右移动(吃豆人经典限制) if (Mathf.Abs(direction.x) > Mathf.Abs(direction.y)) direction = new Vector2(Mathf.Sign(direction.x), 0); else direction = new Vector2(0, Mathf.Sign(direction.y)); transform.Translate(direction * 2f * Time.fixedDeltaTime, Space.World); } }关键设计:
MoveToward强制四向移动(非八向),这是还原街机手感的核心。GetTargetPosition返回的是“目标点”,不是“速度向量”,避免幽灵因浮点误差永远追不上玩家。scatterTargets数组模拟原版四只幽灵不同角落的散点行为,无需复杂寻路。
3. 地图构建与Tilemap优化:用Unity内置工具快速生成可玩关卡
吃豆人地图不是贴图,而是可交互的网格系统。Unity的Tilemap是最佳选择,但新手常陷入“画完地图角色却穿模、幽灵不识别墙壁”的坑。这里给出经过某高校课程验证的标准化流程。
3.1 创建可碰撞的Tilemap层级:分离背景、碰撞与交互层
在Hierarchy中右键 →2D Object→Tilemap,创建三个独立Tilemap:
BackgroundTilemap:纯视觉层,挂SpriteRenderer,Sorting Layer设为BackgroundCollisionTilemap:物理层,挂TilemapCollider2D+CompositeCollider2D(勾选Used by Effector),Sorting Layer设为DefaultInteractionTilemap:交互层(如门、传送点),挂TilemapCollider2D(不勾选Composite)
为什么分三层?
Background层不参与物理计算,节省CPU;Collision层用CompositeCollider2D将所有瓦片合并为单个碰撞体,比逐个BoxCollider2D快5倍以上;Interaction层单独存在,方便后期添加动态交互(如可破坏墙)而不影响主碰撞。
3.2 制作可复用的瓦片资源:Sprite Atlas与Grid对齐技巧
不要直接拖PNG到Tile Palette!先创建Sprite Atlas(Assets → Create → Sprite Atlas):
- 将所有瓦片图(墙壁、空地、豆子位)拖入Atlas;
- 在Inspector中设置
Packing Settings→Padding为2px(防纹理采样溢出); Platform Settings→Max Size设为1024,Format选RGBA Compressed ASTC 4x4(移动端友好)。
然后创建Grid对象(空GameObject),设置Cell Size为(1, 1, 1),Cell Gap为(0, 0, 0)。这是关键:所有瓦片必须严格对齐1x1单位网格,否则PlayerMovement的targetPos计算会偏移。在Tile Palette中,右键瓦片 →Edit Tile→Sprite设为Atlas中对应图,Collider Type选Sprite(自动匹配轮廓)。
3.3 自动化豆子铺设:Editor脚本批量生成Pellet实例
手动在每个空地格放Pellet预制体?1000+格子会疯掉。写一个编辑器脚本PelletPlacer.cs(放在Assets/Editor/目录下):
// Assets/Editor/PelletPlacer.cs using UnityEngine; using UnityEditor; using System.Collections.Generic; public class PelletPlacer : EditorWindow { [MenuItem("Tools/Place Pellets on Empty Tiles")] public static void ShowWindow() { GetWindow<PelletPlacer>("Pellet Placer"); } void OnGUI() { if (GUILayout.Button("Place All Pellets")) { PlacePellets(); } } void PlacePellets() { Tilemap collisionMap = GameObject.Find("CollisionTilemap").GetComponent<Tilemap>(); Tilemap backgroundMap = GameObject.Find("BackgroundTilemap").GetComponent<Tilemap>(); GameObject pelletPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Prefabs/Pellet.prefab"); // 遍历所有有图块的格子 foreach (var pos in collisionMap.cellBounds.allPositionsWithin) { Vector3Int localPlacePosition = new Vector3Int(pos.x, pos.y, 0); if (collisionMap.HasTile(localPlacePosition)) continue; // 墙壁跳过 // 检查是否为空地(背景层有图,碰撞层无图) if (backgroundMap.HasTile(localPlacePosition) && !collisionMap.HasTile(localPlacePosition)) { Vector3 worldPos = collisionMap.CellToWorld(localPlacePosition); Instantiate(pelletPrefab, worldPos, Quaternion.identity); } } Debug.Log("Pellet placement completed."); } }使用流程:菜单栏 →
Tools→Place Pellets on Empty Tiles→ 点击按钮。脚本自动扫描BackgroundTilemap有图、CollisionTilemap无图的位置,批量生成Pellet。注意:Pellet.prefab必须已存在且带Pellet.cs脚本,否则实例化后无逻辑。
4. 避坑指南:吃豆人项目中90%开发者踩过的5个具体雷区
做吃豆人不是写Hello World,很多坑藏在Unity底层机制里。以下是我在某跨平台系统开发中血泪总结的5个高频翻车点,每个都附带现象、根因和实操解法。
4.1 现象:玩家能穿过墙壁,但幽灵被卡住不动
原因:PlayerMovement用了Transform.Translate,而Ghost用了Rigidbody2D.MovePosition,两者物理层级不一致。TilemapCollider2D只对Rigidbody2D生效,Translate是纯坐标变换,绕过物理系统。
解决:统一移动方式。要么全用Rigidbody2D(Player也加Rigidbody2D,FixedUpdate中rb.MovePosition),要么全用Transform(Ghost也改用Transform.Translate)。推荐前者,便于后期加推力、摩擦等效果。
4.2 现象:豆子被吃掉后,OnTriggerEnter2D偶尔不触发,尤其快速连续转向时
原因:Collider2D的Is Trigger在高速移动下存在“穿透”(tunneling),即帧间位移过大,导致两帧间未发生重叠。
解决:在Player和Pellet的Collider2D上,将Used By Effector勾选,并设置Rigidbody2D的Collision Detection为Continuous(非Discrete)。同时降低PlayerMovement中的moveSpeed至8以下,或增加FixedUpdate调用频率(Project Settings → Time → Fixed Timestep设为0.01)。
4.3 现象:幽灵在拐角处反复横跳,无法平滑转向
原因:MoveToward中方向归一化后,if-else判断强制四向,但未处理“当前朝向与目标方向夹角接近90度时的优先级”。例如玩家在右上方,幽灵本应向上,却因Abs(x)>Abs(y)误判为向右。
解决:改用角度差判断。在MoveToward中添加:
float angleToTarget = Vector2.SignedAngle(Vector2.right, direction); if (Mathf.Abs(angleToTarget) < 45f || Mathf.Abs(angleToTarget) > 135f) direction = new Vector2(Mathf.Sign(direction.x), 0); // 横向优先 else direction = new Vector2(0, Mathf.Sign(direction.y)); // 纵向优先4.4 现象:关卡切换后,GameManager的currentScore重置为0
原因:DontDestroyOnLoad虽保住了GameManager实例,但新场景中又创建了一个同名GameManager,导致Instance被覆盖。
解决:在Awake中增加场景检查:
void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); } else if (Instance != this) { Destroy(gameObject); // 销毁重复实例 return; } }4.5 现象:打包WebGL后,幽灵AI完全停滞,但本地Play模式正常
原因:Random.insideUnitCircle在WebGL构建时因安全策略返回(0,0),导致Frightened状态幽灵不动。
解决:替换为确定性随机。在Ghost.cs中声明私有种子:
private System.Random rand = new System.Random(); // 替换 Random.insideUnitCircle * 5f 为: Vector2 offset = new Vector2(rand.Next(-500, 501), rand.Next(-500, 501)) / 100f; return (Vector2)transform.position + offset;5. 进阶技巧:用ScriptableObject重构幽灵AI,实现配置化行为树
当你把吃豆人跑通后,下一步不是堆功能,而是把硬编码逻辑抽成数据驱动。原版幽灵有Blinky(红)、Pinky(粉)、Inky(青)、Clyde(橙)四种行为,如果每种都写一个Ghost.cs分支,维护成本爆炸。用ScriptableObject可实现“改数值不改代码”的配置化AI。
5.1 创建幽灵行为配置文件:GhostBehaviorSO
新建ScriptableObject类GhostBehaviorSO.cs:
// GhostBehaviorSO.cs using UnityEngine; [CreateAssetMenu(fileName = "NewGhostBehavior", menuName = "Pacman/Ghost Behavior")] public class GhostBehaviorSO : ScriptableObject { public string ghostName = "Blinky"; public Color ghostColor = Color.red; [Header("Scatter Behavior")] public Vector2 scatterTarget = new Vector2(-10, 10); public float scatterDuration = 7f; [Header("Chase Behavior")] public ChaseMode chaseMode = ChaseMode.Direct; public float chaseDuration = 20f; public float chaseSpeed = 2f; [Header("Frightened Behavior")] public float frightenedSpeed = 0.5f; public float frightenedDuration = 10f; } public enum ChaseMode { Direct, // 直接追玩家 Ambush, // 追玩家前方2格 Parallel, // 追玩家X轴相同,Y轴固定 Reverse // 追玩家反方向 }5.2 改造Ghost脚本:从硬编码到配置注入
修改Ghost.cs,删除所有public字段,改为引用GhostBehaviorSO:
// Ghost.cs(精简版) public class Ghost : MonoBehaviour { public GhostBehaviorSO behavior; public Transform player; private float stateTimer = 0f; void Update() { stateTimer += Time.deltaTime; if (currentState == GhostState.Scatter && stateTimer > behavior.scatterDuration) { currentState = GhostState.Chase; stateTimer = 0f; } // ... 其他状态逻辑 } Vector2 GetTargetPosition() { switch (currentState) { case GhostState.Scatter: return behavior.scatterTarget; case GhostState.Chase: return GetChaseTarget(); case GhostState.Frightened: return (Vector2)transform.position + Random.insideUnitCircle * 5f; default: return Vector2.zero; } } Vector2 GetChaseTarget() { switch (behavior.chaseMode) { case ChaseMode.Direct: return player.position; case ChaseMode.Ambush: return player.position + (player.position - transform.position).normalized * 2f; case ChaseMode.Parallel: return new Vector2(player.position.x, 0f); case ChaseMode.Reverse: return transform.position - (player.position - transform.position); default: return player.position; } } }5.3 在Inspector中可视化配置:一键切换幽灵性格
在Project窗口右键 →Pacman→Ghost Behavior,创建四个配置文件:Blinky_Behavior、Pinky_Behavior等。在Inspector中分别设置:
Blinky:ChaseMode=Direct,scatterTarget=(-10,10)Pinky:ChaseMode=Ambush,scatterTarget=(10,10)Inky:ChaseMode=Parallel,scatterTarget=(-10,-10)Clyde:ChaseMode=Reverse,scatterTarget=(10,-10)
然后将对应配置拖入每个Ghost组件的behavior字段。从此调整幽灵行为只需改Inspector数值,无需碰C#代码——这才是工业级项目的起点。
我带过的某实验室团队,在用这套配置化方案后,学生迭代新幽灵行为的平均耗时从3小时降到15分钟。他们不再问“这段代码怎么改”,而是直接打开
.asset文件调参数。真正的工程效率,不是写得多,而是改得少。希望帮到你。
本文还有配套的精品资源,点击获取