简介:这是一份用于Unity期末课程设计的跑酷游戏完整项目,面向计算机相关专业正在完成期末作业或学习Unity游戏开发基础的学生。玩法采用上下左右四向移动,J键发射子弹、K键跳跃,且仅在跳板上才能起跳,若从白色跳板掉落则角色无法再跳跃并触发游戏结束;吃能量包可累积分数,同时配有完整音效反馈。资源以zip压缩包形式提供,免积分下载,包体约304MB,内含9119个文件,涵盖C#脚本、Prefab预设、场景资源、动画控制器,以及mp3、wav音效和png贴图等,基本覆盖Unity 2D跑酷游戏从场景搭建、角色控制到计分与音效触发的常见开发环节。目前已有760人浏览学习。对于希望快速获得可运行参考案例的读者,这套项目可直接解压运行,用于拆解玩法逻辑、复用场景与素材,或在此基础上进行二次修改,能有效节省从零搭建的时间。
1. 期末跑酷游戏方案:先看玩法规则,再定技术方案
期末跑酷游戏在Unity课程设计里出现频率极高,需求很明确:角色自动往前跑,按K跳跃但只有踩上跳板才能起跳,按J发射子弹清理障碍,吃能量包统计分数,配上音效就能答辩。它把角色控制、刚体物理、Trigger碰撞检测和AudioSource这些Unity最基础的模块串成一个完整可玩的小项目,正好覆盖期末考核的常见知识点。适合从零开始做Unity期末项目的学生,也适合想快速过一遍这几个核心机制的开发者。下面按场景搭建、角色控制、跳板与子弹、计分音效、排查的顺序,把脚本和参数直接给出来,跟着做一遍就能跑。
2. 场景与角色:搭起一条能“跑起来”的跑道
2.1 手动摆场景与预制体规划
先别急着写代码,把场景摆出来,确认视觉和碰撞没问题,再往上加脚本。跑酷场景的固定套路是一段足够长的直跑道、若干跳板、悬浮的能量包、挡路的障碍物、一个玩家胶囊体和一台摄像机。手工摆就够期末用了,摆的时候顺手把每个东西的层级规划好,后面找问题会非常快。
推荐的结构是这样:建一个空物体叫Track,下面按顺序放四个子分类:Ground(跑道段,Cube,y=0.25,scale为(4, 0.5, 10))、JumpPad(跳板,Cube,y=0.55,scale为(4, 0.3, 3))、EnergyPack(能量包,Sphere,y=1.8)、Obstacle(障碍物,Cube,y=0.8)。直道总长摆40米左右,跳板每隔6到8米放一个,能量包放在跳板之间的空中,障碍物放在能量包附近。为什么要这个间隔?角色默认速度8米/秒,6到8米的跳板间隔意味着每次起跳到下次起跳之间有一秒左右的缓冲,玩家能看清下一个跳板在哪,不会手忙脚乱。
材质球直接用Project窗口右键Create → Material,改个颜色就行:跳板绿色、能量包黄色、障碍物红色、跑道灰色。不要为了好看去搞PBR贴图或从Asset Store拖大包,期末项目美术资源越少,答辩时越容易说清楚,老师问“这些材质是哪里来的”也更好回答。
摆完后把跳板、能量包、障碍物、子弹各存成Prefab:从Hierarchy拖到Project窗口即可。预制体是后面Instantiate动态生成的前提,也是答辩时“对象池”“批量生成”这些追问的起点。拖的时候注意,Prefab要放在Project窗口的Assets/Prefabs文件夹里,直接放根目录也行,别放进场景里。
碰撞关系在摆场景时就要定清楚:跑道和障碍物是普通Collider,玩家能踩上去、会被挡住;跳板和能量包是IsTrigger,只做重叠检测,不产生物理阻挡。这个区分解决了一大半后面会遇到的“能量包碰了不加分”“角色直接穿模掉下去”之类的问题。
2.2 PlayerController:用刚体控制自动前进
玩家用一个Capsule,挂上Rigidbody。自动前进的实现方式有三种:transform.Translate、CharacterController、Rigidbody速度控制。我直接推荐Rigidbody,原因有两个:一是Translate和Rigidbody混用会互相打架,出现角色抖动或穿地;二是CharacterController自带isGrounded,但这个项目要“只在跳板上才能跳跃”,isGrounded只告诉你脚下有没有地面,分辨不了是不是跳板,最后还是得自己写Trigger检测,不如直接用刚体干净。
把移动逻辑放进FixedUpdate,用直接赋值速度Z分量的方式:
using UnityEngine; public class PlayerController : MonoBehaviour { [Header("移动参数")] public float forwardSpeed = 8f; // Z轴前进速度 public float jumpForce = 7f; // 跳跃初速度,第3章使用 [Header("引用")] public Rigidbody rb; private bool isOnJumpPad; // 当前是否站在跳板上 private void Awake() { rb = GetComponent<Rigidbody>(); } private void FixedUpdate() { // 保持Z轴恒速前进,X和Y交给物理系统 Vector3 velocity = rb.velocity; velocity.z = forwardSpeed; rb.velocity = velocity; } }这个写法的关键在最后三行:先取出当前刚体速度,只改Z分量,再赋回去。如果直接写rb.velocity = new Vector3(0, 0, forwardSpeed),会把Y轴速度清零,重力累积被抹掉,角色永远跳不起来。这是新手最容易翻车的地方。
参数说明:forwardSpeed=8f是默认值,跑得偏快偏慢都先改它。Rigidbody的Interpolate建议设成Interpolate,让胶囊体在高帧率下移动更平滑;Collision Detection先保持Discrete,第5章讲子弹穿透时再单独处理。
一点提醒:不要在Awake之后用transform.position去初始化角色位置,刚体物体最好在场景里手动摆好初始位置,或者用rb.MovePosition,否则物理引擎会有一帧的抖动。
2.3 unity摄像机跟随:LateUpdate里的偏移
摄像机跟随是让“跑起来”有感觉的必要组件。项目里就一个Main Camera,挂一个CameraFollow脚本,LateUpdate里把摄像机放在玩家背后上方。为什么用LateUpdate而不是Update?因为Update里玩家的位置可能还没被物理引擎更新完,LateUpdate保证玩家移动完成后摄像机再跟随,画面不抖。
using UnityEngine; public class CameraFollow : MonoBehaviour { public Transform target; // 玩家胶囊体 public Vector3 offset = new Vector3(0, 6, -9); // 摄像机在玩家上方6米、后方9米 private void LateUpdate() { transform.position = target.position + offset; // 看向玩家头顶上方1.5米,保证前方跑道视野开阔 transform.LookAt(target.position + Vector3.up * 1.5f); } }逻辑说明:offset=(0, 6, -9)直接决定了俯视角。把-9改成-11,能看到更远的跳板但角色在画面上变小;把6改成4,视角更低更有冲刺感但能量包容易被头顶遮住。我建议保持默认,答辩时如果老师问“为什么摄像机不抖”,回答“LateUpdate加固定偏移”就是标准答案。
如果想让摄像机更顺滑,可以把position赋值改成Vector3.Lerp(transform.position, target.position + offset, 0.1f)。但Lerp会让摄像机一直滞后一小段距离,跑动快时能感觉到“拖尾巴”,期末阶段先不加。发射子弹时,firePoint建议放在玩家前方0.8米处,如果摄像机视角偏低,子弹容易被角色头部挡住,调offset的Y值或firePoint的y高度都能解决。
3. 跳板跳跃与J发射子弹:两个核心玩法的实现
3.1 跳板判定:用Trigger标记“可跳跃”状态
“只有在跳板上才可以跳跃”是整个项目最容易被问“怎么实现”的点。实现上要解决一件事:系统怎么知道玩家当前能不能跳。常见做法是给跳板加IsTrigger,玩家踩进触发器范围时置一个可跳跃标记,离开时清掉。我在2.2里已经给isOnJumpPad留了字段,这一步把判定补齐。
先改跳板的Collider设置:勾选IsTrigger,size改成(4, 0.6, 3.4),比视觉方块(4, 0.3, 3)大一圈。这样角色从边缘踩到跳板也能被判定到,不会出现“明明站上去了一半却跳不起来”的尴尬。
// 玩家脚本里补跳板状态检测 private void OnTriggerStay(Collider other) { if (other.CompareTag("JumpPad")) { isOnJumpPad = true; } } private void OnTriggerExit(Collider other) { if (other.CompareTag("JumpPad")) { isOnJumpPad = false; } }逻辑说明:OnTriggerStay在玩家与跳板Trigger重叠期间每一帧都会被调用,把isOnJumpPad持续置true;离开时OnTriggerExit置false。我不用OnTriggerEnter的原因:Enter只在进入瞬间触发一次,如果那一帧玩家恰好没按K,后面就再也不会进入可跳跃状态,必须等着离开再进一次。Stay是持续心跳,更稳。
跳跃动作放Update里,按K触发:
// 玩家脚本里补K跳跃 private void Update() { if (Input.GetKeyDown(KeyCode.K) && isOnJumpPad) { // 只在跳板上才能跳跃 rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); // 防止起跳瞬间Trigger残留导致空中二次触发 isOnJumpPad = false; // SoundManager.Instance.PlayJump(); // 音效第4章补 } if (Input.GetKeyDown(KeyCode.J)) { TryFire(); } }这里有个必须解释的细节:为什么起跳后手动把isOnJumpPad置false?角色被Impulse推离跳板的物理帧里,刚体还没完全脱离Trigger区域,OnTriggerStay还会再调一两帧。如果不手动关掉,玩家会在空中再按一次K又跳一次,变成“空中二段跳”,和“只能在跳板上跳跃”的规则冲突。这个bug在测试时极易被老师发现,手动关掉是最直接的后悔药。
跳跃高度由jumpForce=7f加上默认重力-9.8决定,大概能跳1.2米高。障碍物做得矮一点,y=0.8配上scale高度1.6,角色刚好越过或者被挡住一半,视觉上很合理。
3.2 J发射子弹:从生成到销毁的最短代码路径
射击的逻辑链条是:按J → 生成子弹预制体 → 赋予初速度 → 子弹飞行 → 撞到障碍物销毁。每一环单独看都很基础,连起来就是完整的武器系统。
先做子弹预制体:新建Sphere,scale=(0.3,0.3,0.3),挂Rigidbody,取消Use Gravity,碰撞检测改成Continuous(第5章会讲为什么必须改),再挂Bullet脚本。玩家身上需要补三个字段:bulletPrefab、firePoint、fireCooldown和bulletSpeed。
using UnityEngine; public class Bullet : MonoBehaviour { public float lifeTime = 2f; // 2秒后自动销毁,防止子弹永远飞下去 private void Start() { Destroy(gameObject, lifeTime); } private void OnTriggerEnter(Collider other) { if (other.CompareTag("Obstacle")) { // 打中障碍物,两个一起销毁 Destroy(other.gameObject); Destroy(gameObject); // SoundManager.Instance.PlayFireHit(); // 音效第4章补 } } }玩家脚本补射击逻辑:
// 玩家脚本里的字段 public GameObject bulletPrefab; // 子弹预制体 public Transform firePoint; // 发射点,玩家前上方空物体 public float fireCooldown = 0.3f; // 发射间隔 public float bulletSpeed = 25f; // 子弹初速度 private float lastFireTime; private void TryFire() { if (Time.time < lastFireTime + fireCooldown) return; // 冷却未结束 lastFireTime = Time.time; // 在发射点生成子弹,并赋予向前的初速度 GameObject bullet = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); bullet.GetComponent<Rigidbody>().velocity = firePoint.forward * bulletSpeed; }逻辑说明:fireCooldown用Time.time做时间戳是最轻量的冷却方案,不依赖协程,玩家连按J也不会像连珠炮一样刷屏。Instantiate生成子弹后立刻赋velocity,子弹会在下一物理帧开始移动。注意不要在Start里再设一次速度,两个地方赋值取最后一次,容易乱。
firePoint的rotation是子弹的方向来源。跑酷方向是Z正方向,如果firePoint是个空物体挂在玩家身上并保持默认旋转,firePoint.forward就是Z正方向,子弹直线朝前飞。如果发现子弹往天上飞,先检查firePoint是不是不小心转了角度。
发射间隔0.3秒意味着每秒约3发,体感是“有节奏但不会卡手”。如果障碍物排得很密,调成0.2;如果觉得子弹太飘,调成0.4更适合观察弹道。
3.3 跳跃与射击参数表:先抄这组数值再微调
手感是调出来的,但期末阶段先抄一组能跑通的数值,再按现象逐个微调。下表是我用过比较稳的默认值:
| 参数 | 建议值 | 说明 |
|---|---|---|
| forwardSpeed | 8 | 角色Z轴速度,调大要同步拉长跳板间隔 |
| jumpForce | 7 | 跳跃初速度,配合默认重力约跳1.2米 |
| bulletSpeed | 25 | 子弹飞行速度,超过30会明显增加穿透概率 |
| fireCooldown | 0.3 | 发射间隔,连发手感的核心参数 |
| 重力 | -9.81(默认) | 整体物理参数,不建议改 |
| 跳板Trigger size | (4, 0.6, 3.4) | 比视觉方块大一圈,容错更高 |
调参顺序的建议:先调forwardSpeed找到“跑步感”,再调jumpForce让跳跃高度刚好越过障碍物,最后调fireCooldown控制射击节奏。一次只改一个参数,进Play模式跑一段验证,不要同时改两个,否则翻车了根本不知道是哪一个造成的。
4. 能量包、统计分数与音效:把“能玩”变成“能交”
4.1 能量包触发加分:一层Trigger + Tag判断
能量包是分数来源,实现上就是Trigger检测到玩家进入,加分,销毁自己。和跳板不同,能量包“摸到就生效”,不需要Stay,OnTriggerEnter就够了。但在写脚本之前,先做一件很重要的事:给玩家和障碍物打Tag。
在Inspector顶部的Tag下拉菜单里,给玩家Capsule新建并选择“Player”,障碍物选“Obstacle”,跳板选“JumpPad”,能量包选“EnergyPack”。Tag判断比按物体名判断稳得多,因为名字容易拼错,Tag是选出来的。
using UnityEngine; public class EnergyPack : MonoBehaviour { public int scoreValue = 10; // 每个能量包10分 private void OnTriggerEnter(Collider other) { // 只有玩家碰到才加分,子弹碰到不加分 if (other.CompareTag("Player")) { GameManager.Instance.AddScore(scoreValue); // SoundManager.Instance.PlayPickup(); // 音效下一节接 Destroy(gameObject); } } }逻辑说明:使用Tag判断而不是玩家身上有没有某个脚本,是因为Tag是全局可见的,cost低且语义清晰。加完分立刻Destroy,防止同一帧被多次触发重复加分。加分操作走GameManager.Instance入口,避免能量包自己维护分数——这是“统计分数”需求里的关键单一职责设计。
给能量包加个旋转动画,视觉上更有“可吃”的感觉,代码挂在能量包上:
// 能量包旋转,让它在空中转起来 void Update() { transform.Rotate(0, 90 * Time.deltaTime, 0, Space.World); }90度每秒不算快,玩家跑过时能明显看出它和静态障碍物的区别。如果想更醒目,把颜色改成带一点发光感的黄色,或者加一个Point Light,期末答辩时这种小细节很加分。
4.2 统计分数与UI:GameManager单例统一入口
分数统计用GameManager单例。单例是期末项目里高频提问点,它的好处是所有脚本都通过GameManager.Instance访问同一个分数数据,逻辑不会散落在各个物体上。
using UnityEngine; using UnityEngine.UI; public class GameManager : MonoBehaviour { public static GameManager Instance; public Text scoreText; // 场景里的分数UI文本 private int score; private void Awake() { Instance = this; // 简单单例,期末阶段够用 } public void AddScore(int value) { score += value; UpdateScoreUI(); } private void UpdateScoreUI() { if (scoreText != null) { scoreText.text = "分数: " + score; } } }UI搭建步骤:场景里右键 → UI → Canvas,在Canvas下建一个Text,放到左上角。字体大小40,颜色用白色加黑色描边(默认黑字在深色场景里看不清)。把Text拖到GameManager的scoreText字段。Canvas的Render Mode保持默认的Screen Space - Overlay,不需要指定摄像机,期末最快。
UpdateScoreUI里的if判空是刻意的:如果场景里暂时没有UI,脚本照样能跑不报空引用;答辩前检查时,也能意识到“是不是忘了拖scoreText”。AddScore是唯一改分数的入口,能量包调用它,后面如果加“连击加分”“时间加分”都走这一个方法,分数记录不会乱。
4.3 音效挂载:跳跃、子弹、吃包三种音效分开管
音效是这个标题里的收尾项,做得好能让玩法质感上一个台阶。实现采用SoundManager统一管理,不要在PlayerController、Bullet、EnergyPack每个脚本里各写一段AudioSource.PlayClipAtPoint,那样答辩时讲不清,排查时也很难定位是哪条音效出了问题。
场景里建一个空物体SoundManager,挂上SoundManager脚本,下面创建三个AudioSource:JumpSfx、FireSfx、PickupSfx。每个AudioSource的AudioClip拖入对应音频文件,取消勾选Play On Awake,Spatial Blend设为0。
using UnityEngine; public class SoundManager : MonoBehaviour { public static SoundManager Instance; public AudioSource jumpSfx; // 跳跃音效 public AudioSource fireSfx; // 射击音效 public AudioSource pickupSfx; // 吃能量包音效 private void Awake() { Instance = this; } public void PlayJump() { jumpSfx.Play(); } public void PlayFire() { fireSfx.Play(); } public void PlayPickup() { pickupSfx.Play(); } }把3.1里注释掉的SoundManager.Instance.PlayJump()、3.2里的PlayFireHit()、4.1里的PlayPickup()依次恢复,整个音效链路就通了。Spatial Blend设为0是关键:默认的3D音效让AudioSource离AudioListener远时声音变小,跑酷里玩家一直移动,声音会忽大忽小,这是个很玄学的问题,统一设成2D音效一劳永逸。
5. 期末项目排查:5个常见翻车点与解决顺序
期末项目的bug通常不是逻辑复杂,而是几个固定地方的配置没对齐。下面列出的5个问题是带项目时出现频率最高的,按“现象 → 原因 → 解决”写,逐条对一遍,能省下大量Debug时间。
5.1 角色高刷屏上飞出去:物理帧率没锁
现象:在144Hz或更高刷新率的电脑上运行,角色跑得比预期快很多,甚至直接穿过跑道掉下去;在60Hz的教室电脑上又正常。
原因:移动逻辑如果放进了Update,角色位移会跟着渲染帧率走,高刷新率下每帧调用次数变多,角色自然“飞”起来。另一种情况是物理引擎的Fixed Timestep设置漂移,导致物理表现不稳。
解决:移动逻辑统一放FixedUpdate,用2.2的rb.velocity写法。同时在GameManager的Awake里锁帧率和垂直同步:
void Awake() { // 锁定渲染帧率与垂直同步,避免高刷屏导致物理表现不一致 Application.targetFrameRate = 60; QualitySettings.vSyncCount = 1; }验证方式:运行后打开Inspector观察Rigidbody的Velocity,Z轴稳定在8说明生效。修改Fixed Timestep的操作在Project Settings → Time面板,建议保持0.02秒先不要动。
5.2 跳板跳跃时灵时不灵:Trigger范围与刚体速度
现象:角色明明站在跳板上按K却跳不起来;或者从跳板边缘蹭过去时莫名其妙跳了一下;还有跳出跳板后在空中还能再跳一次。
原因:三个可能叠加:Trigger范围太窄,角色碰撞体只在边缘,OnTriggerStay没有被持续调用;跳跃后没有手动清isOnJumpPad,导致Trigger残留;角色刚体速度过快时,物理引擎在连续帧之间跳过了Trigger重叠检测。
解决:把跳板Trigger的Collider size加大到(4, 0.6, 3.4),保证角色完全置于其中;跳跃成功的代码里一定要加isOnJumpPad = false;另外检查玩家Capsule有没有被其他Collider卡住,导致没完全进入Trigger区域。验证方式:站在跳板上连续按K跳5次,每次都起跳才算通过。
5.3 子弹穿墙打不中目标:碰撞检测模式
现象:子弹飞得很快,直接穿过障碍物,OnTriggerEnter一次都没触发。
原因:高速运动的Rigidbody默认用Discrete离散碰撞检测,物理引擎在两次检测之间会“跳过”薄物体,表现就是穿模。这是Unity物理引擎的已知行为,不算bug,属于使用方式不对。
解决:把子弹预制体的Rigidbody → Collision Detection改成Continuous,障碍物Collider略微加厚到0.6米以上。Continuous用扫描预测碰撞,能覆盖绝大多数穿透情况。如果子弹还是要以更快速度飞行且场景里障碍物多,更彻底的方案是把子弹改成射线检测,每帧向前打一条Raycast,命中即销毁。期末阶段用Continuous即可,答辩时能说清楚区别就很加分。
5.4 能量包碰了不加分:IsTrigger与Tag没对齐
现象:角色穿过能量包,视觉上明显重叠了,但分数纹丝不动。
原因:排查顺序很重要。一是能量包Collider没有勾IsTrigger;二是加分脚本挂在子物体上而Collider在父物体上,Trigger回调只发给碰撞体所在的GameObject及其父级,脚本挂了子物体就收不到;三是玩家GameObject没有打“Player”Tag,CompareTag返回false。
解决:按顺序检查:Collider是否勾IsTrigger;脚本和Collider是否在同一个物体上;玩家Tag是否确实为Player;GameManager.Instance是否为空。验证方式:在GameManager的AddScore第一行加Debug.Log("加分" + value),穿过能量包看Console有没有输出,特别适合定位“到底触没触发”。
5.5 打包后音效不响:AudioListener和3D/2D设置
现象:编辑器里音效一切正常,打包成exe后打开要么没声音,要么只有部分音效。
原因:最常见的是场景里没有AudioListener——Main Camera被删了又没在别处补;其次是音频载入方式Streaming导致播放时还没加载完;再就是Spatial Blend设成了3D而场景里AudioListener距离过远,声音被空间衰减衰减没了。
解决:确认场景中至少有一个AudioListener(Main Camera自带);音频资源在Inspector里的Load Type改成Decompress On Load;所有音效的Spatial Blend设成0。验证方式:打包前直接用Build Settings里的Build And Run跑一次,不要只在编辑器里听。这三项查完,音效问题基本清零。
6. 答辩前冲刺:手感调优、固定帧率与一键重开
6.1 固定帧率与一键重开:演示不再翻车
答辩现场的电脑是什么配置完全不可控,最常见翻车就是“跑太快”和“死了之后不知道怎么重置”。跑太快在第5章已经解决,重开这件事一定要做成按键操作,而不是让答辩老师看你手动点Unity的Play按钮。在一个空物体上挂Restart脚本,按R键重载场景:
using UnityEngine; using UnityEngine.SceneManagement; public class RestartButton : MonoBehaviour { void Update() { if (Input.GetKeyDown(KeyCode.R)) { // 重载当前场景,分数、物体全部回到初始状态 SceneManager.LoadScene(SceneManager.GetActiveScene().name); } } }场景重载会把GameManager也重建,分数归零,正好是完整演示流程。如果还想更稳妥,可以把按键提示做成UI文字放在右下角:“K跳跃 / J射击 / R重开”,答辩时老师一眼就知道操作方式。
6.2 手感调优的病历:我改过的三个参数
带期末项目时最典型的手感问题是三个:跳跃太高导致节奏拖沓,jumpForce从7改到5.5;子弹冷却太慢导致连打三个障碍打不完,fireCooldown从0.5改到0.3;前进速度太慢导致等能量包要等好几秒,forwardSpeed从6改到8。这三个参数每改一个都要重新跑一遍完整的跳板评测:连续跳三块跳板感受节奏,连打三个障碍验证射击跟不跟得上。我前些年被“凭感觉调参”坑过几次,后来养成了习惯:每个参数改完就记录现象和时间,不记录就永远不知道自己调过什么,答辩时老师问“为什么是这个值”也能答得上来。
如果时间还有富余,给子弹加一个沿Z轴的旋转,给能量包加一个透明度闪烁动画,这两个小优化成本很低,但能让整个项目的完成度明显上一个台阶。做到这一步,核心玩法完整、参数可调、按键明确、音效正常,已经是一个能交的期末作品了。希望帮到你。
本文还有配套的精品资源,点击获取