☰
Unity赛车源码怎么用:从WheelCollider到手感调校的完整拆解
2026/10/7 16:48:10 网站建设 项目流程

简介:面向Unity开发者的完整3D赛车游戏源码项目,支持Unity 5.5及以上版本,覆盖从场景搭建、车辆控制到任务关卡与移动端UI交互的完整框架。游戏内置越南、香港、东京、纽约等多个城市赛道,收录10辆超级跑车,玩家通过左右倾斜操控赛车,躲避障碍并收集金币、氮气;提供快速比赛与生涯模式两种模式,快速比赛进一步细分为普通、检查站、计时赛,共36个任务关卡。资源包共2000个文件,以C#逻辑脚本、prefab预制体、材质贴图与Unity场景文件为主体,辅以XML配置、安卓安装包和少量Java原生文件,压缩后大小约330MB,文件分类细致,可直接导入工程使用。目前已有298人学习/下载。项目内置广告集成与IAP支付模块,便于开发者直接移植到新游戏;同时完整UI界面和多样化城市赛道场景,无论是学习赛车操控、任务系统还是商业化打包,都能找到对应模块,非常适合作为毕业设计或商业原型起点。

1. 为什么一份Unity赛车源码值得你花时间读:从“转圈Demo”到可玩赛车的距离

打开一个名为 King Of Racing 3D 的 Unity 赛车项目源码,第一次按下 Play 键时,大多数人的反应是“就这?不就一辆车在环形赛道上来回跑吗”。但当你把场景里的车换成自己的模型、把物理参数随手一改,却发现车辆要么原地打转、要么过弯就翻、要么刹车时像踩了块冰,这时候才会意识到,一份能稳定发售的赛车游戏源码,真正值钱的不是 3D 模型和赛道摆件,而是那几十个 C# 脚本里围绕 WheelCollider、刚体、相机和粒子特效做的手感调校。这篇文章就从这套源码出发,讲清楚它拆开以后有哪些模块、怎么在自己机器上跑通、调参时该动哪里,以及哪些坑是一线项目里反复出现的血泪经验。适合手里拿着 Unity 源码但不知道从哪下手的初学者,也适合想快速搭一套可复用赛车框架的熟手。

2. 拆解 King Of Racing 3D 的项目结构:用 C# 脚本看清一套赛车游戏的骨架

拿到任何游戏源码,第一步不是急着打开场景看效果,而是先在 Project 面板里把 Assets 目录过一遍。这套赛车项目的目录安排非常典型,基本是“场景、脚本、预置体、美术资源、音频”五大块。搞清楚它们之间的关系,后面改任何东西都不会迷路。

2.1 先看目录:Assets 下的文件夹决定了项目的“性格”

我一般会按这个顺序去读一个 Unity 赛车源码的目录:先找 Assets/Scenes,看里面有几个可用场景,这决定了项目是单关卡演示还是完整可循环的玩法闭环;再找 Assets/Scripts,看 C# 脚本的组织方式是按功能拆分还是全堆在一个叫 GameManager 的文件里;最后看 Assets/Prefabs,里面有多少个可复用的车辆预制体。

King Of Racing 3D 这类项目的标准结构长这样:

Assets/ Scenes/ 主场景、菜单场景、赛道场景 Scripts/ Car、Camera、UI、GameManager、Track 等子目录 Prefabs/ 赛车、轮胎、检查点、粒子特效的预制体 Models/ 车身、赛道、场景道具的 fbx 模型 Materials/ 车漆、赛道路面、天空盒材质 Textures/ 贴图 Audio/ 引擎音效、碰撞音效、背景音乐

这套划分方式的好处是职责清晰:车辆相关脚本不会和 UI 脚本互相引用得到处都是,预制体里保存好的物理参数也让每次调整不需要重新拖场景物体。我拿到源码后会先做一件事:把主场景里的赛车层级结构展开,看它挂了哪些组件。如果一辆车上同时挂了 Rigidbody、WheelCollider 列表、AudioSource、ParticleSystem,那这辆车的核心逻辑大概率就在一个大型的 CarController 类里,这也是多数赛车 Demo 的常见写法。

2.2 CarController:WheelCollider 才是赛车手感的物理基础

赛车游戏和普通第三人称动作游戏最大的区别在于,车辆的移动不能靠直接修改 Transform 位置来实现。如果你在一个赛车源码里看到代码里写transform.Translate(),那这辆车基本告别真实物理了。正规做法是让 Rigidbody 承担车身受力,让四个轮子各自挂一个 WheelCollider,由引擎扭矩和刹车力矩驱动整体运动。

这个项目里最核心的脚本通常叫 CarController.cs,它的骨架长这样:

using UnityEngine; public class CarController : MonoBehaviour { [Header("轮子配置")] public WheelCollider frontLeft; public WheelCollider frontRight; public WheelCollider rearLeft; public WheelCollider rearRight; [Header("动力参数")] public float motorTorque = 1500f; // 电机扭矩 public float brakeTorque = 3000f; // 刹车扭矩 public float maxSteerAngle = 30f; // 最大转向角 private Rigidbody rb; void Start() { rb = GetComponent<Rigidbody>(); // 赛车类刚体必须压低重心,否则过弯必翻 rb.centerOfMass = new Vector3(0f, -0.5f, 0f); } void FixedUpdate() { float steer = Input.GetAxis("Horizontal"); float throttle = Input.GetAxis("Vertical"); // 前轮转向,四轮驱动或后轮驱动按项目设定 frontLeft.steerAngle = steer * maxSteerAngle; frontRight.steerAngle = steer * maxSteerAngle; // 电机扭矩施加到驱动轮 rearLeft.motorTorque = throttle * motorTorque; rearRight.motorTorque = throttle * motorTorque; // 按下空格或倒车时刹车 if (Input.GetKey(KeyCode.Space)) { ApplyBrakes(brakeTorque); } } void ApplyBrakes(float torque) { frontLeft.brakeTorque = torque; frontRight.brakeTorque = torque; rearLeft.brakeTorque = torque; rearRight.brakeTorque = torque; } }

这段脚本的逻辑说明:FixedUpdate 是物理帧回调,所有和 WheelCollider 相关的输入都必须放在这里,因为 WheelCollider 的受力计算依赖固定时间步长;刚体中心下沉是关键,如果不改 centerOfMass,所有赛车过弯都会像砖头一样侧翻。参数方面,motorTorque 决定了直线加速的猛不猛,maxSteerAngle 决定低速弯和高速弯的响应差异,brakeTorque 调太大时轻点空格就会抱死甩尾,这是后面调手感时最常动的三个值。

2.3 输入层:从旧输入管理器到新输入系统的兼容问题

很多 Unity 老项目用的是默认的 Input Manager,代码里写Input.GetAxis("Horizontal")拿方向键输入。King Of Racing 3D 这种带 3D 赛车玩法、又强调移植到移动端的项目,往往会在旧输入系统和新 Input System 之间纠结。如果源码是 2019 或 2020 年附近做的,大概率还是旧输入系统。

这里有一个实用判断方法:打开 Project Settings → Player → Active Input Handling,如果显示的是 Input Manager (Old),那所有Input.GetAxis都能直接跑;如果项目升级到了新版 Unity 并且切成了 Input System Package (New),那你需要在Project Settings → Player → Active Input Handling里选择 Both,否则项目会在运行时直接抛InvalidOperationException,方向盘完全不动。

我自己拿到源码后习惯先确认这一点,因为“车不动”这个问题有一半原因不是脚本逻辑,而是输入系统切换导致的事件分发失效。另外,移动端源码里常会看到Input.touchCount和虚拟摇杆脚本,这类代码在 PC 上测试时要额外接一套模拟输入,否则你点鼠标是没有反应的。

2.4 代码里的“黑匣子”:GameManager、检查点与圈速计时

除开车辆本身,赛车游戏源码里最容易被忽略的是 GameManager 和 TrackManager。它们负责从车辆脚本手里拿数据、判断圈数、计时、显示 UI。你看一辆车跑起来很简单,但“过了一个检查点、再过一个检查点、最后回到起点算一圈”这套逻辑,才是从技术 Demo 走向可玩赛车游戏的分水岭。

常见实现方式是:每个检查点是一个触发器 Collider,车辆进入时把当前 CheckpointIndex 加一;当车辆以正确顺序经过全部检查点并再次撞到起点线时,圈数加一。这里有个关键细节:检查点列表的顺序和方向必须正确,否则车辆第一次出发就算跑完整圈,圈数计数器也直接卡住。

using UnityEngine; public class CheckpointManager : MonoBehaviour { public Transform[] checkpoints; // 按顺序排列的检查点 public int totalLaps = 3; private int currentCheckpoint = 0; private int currentLap = 0; void OnTriggerEnter(Collider other) { if (!other.CompareTag("Player")) return; // 应用了“必须按顺序通过”的约束 if (transform == checkpoints[currentCheckpoint]) { currentCheckpoint++; if (currentCheckpoint >= checkpoints.Length) { currentCheckpoint = 0; currentLap++; Debug.Log("完成圈数:" + currentLap + "/" + totalLaps); if (currentLap >= totalLaps) { Debug.Log("比赛结束"); } } } } }

这段脚本的注意点:每个检查点物体都要单独挂一个同名脚本,并且把checkpoints数组统一指向同一个检查点序列对象。常见的错误是检查点顺序反向,导致玩家跑完一圈后没有触发完成判定;另一个坑是玩家抄近路跳过检查点,直接冲向终点线,所以完整项目里往往会记录“当前必须到达的下一个检查点”,而不是简单地判断“进过任何一个检查点都算数”。

3. 把源码跑起来:版本选型、场景导入与最小可玩链路

有了上一章的结构认知,现在把项目真正打开。这一步最大的障碍反而不是代码,而是 Unity 编辑器版本和项目管线不匹配。一旦编辑器给出几百条编译错误,新手很容易怀疑源码本身有问题,其实大多数时候只是版本不对。

3.1 用哪个 Unity 版本打开:内置管线优先,别一上来就升级

King Of Racing 3D 这类 3D 赛车项目的源码,最稳的打开方式是选择 Unity 2020.3 LTS 或 2021.3 LTS。原因有三:第一,内置渲染管线(Built-in Render Pipeline)在这两个版本里还在绝对主流位置,项目里默认的 Standard 材质不会出现一片粉色的情况;第二,C# 脚本 API 兼容性最好,WheelCollider、ParticleSystem的 API 没有被高版本打上过时标签;第三,旧版 Input Manager 在 2020/2021 里开箱即用,不用处理 Input System 的迁移。

如果手头只有 Unity 2022 或 Unity 6,也不是不能打开,但要做好几件事:先在菜单栏Edit → Project Settings → Player → Active Input Handling设置为 Both;遇到材质显示异常,把 Standard Shader 换成 URP 对应版本;遇到OnCollisionEnter等生命周期方法报错,大概率是从 Unity 5 时代升级过来的格式问题,重新保存脚本就能触发编译器刷新。

3.2 导入后的第一个动作:先跑原版场景,再谈改代码

拿到源码后不要直接拿自己的车模型替换进去。正确顺序是:首次打开场景后,完完整整地按下 Play 键,用默认键位开几圈,记录这辆车的原始手感——加速度、极速、过弯姿态、漂移幅度。这一步叫“基线测试”,后续你调的每一个参数,都是在和这个基线作对比。

跑原版场景时要注意观察三类对象:车辆轮子的转速和转向是否正常、相机跟随是否平滑、粒子特效出现在哪些时机。如果车辆在某条默认赛道上开得非常顺,说明物理参数和赛道摩擦配置是配套的;当你换了赛道后手感突变,问题往往不出在车,而出在赛道路面的 Physic Material 摩擦系数。

3.3 最小可玩链路:新建一个空场景,把一辆车从零拼出来

如果原版场景太复杂,或者你想把车辆脚本套用到自己的测试赛道上,最建议的做法是新建一个空场景,从 Prefabs 里拖出一辆车,再手动搭建一个最简单的平面赛道,跑通“起步—加速—转弯—刹车”的最小闭环。

using UnityEngine; // 最小测试脚本:挂到任意空物体上,用键盘控制一辆带 Rigidbody 的简易小车 public class MinimalDriver : MonoBehaviour { public WheelCollider frontLeft; public WheelCollider frontRight; public WheelCollider rearLeft; public WheelCollider rearRight; public float moveSpeed = 500f; public float turnSpeed = 25f; void FixedUpdate() { float v = Input.GetAxis("Vertical"); // 前进后退 float h = Input.GetAxis("Horizontal"); // 左右转向 frontLeft.steerAngle = h * turnSpeed; frontRight.steerAngle = h * turnSpeed; rearLeft.motorTorque = v * moveSpeed; rearRight.motorTorque = v * moveSpeed; } }

逻辑说明:这是一个不依赖 CarController 完整逻辑的“冒烟测试”脚本,专门用来验证车辆预制体的物理配置是否有效。把这个脚本挂到一辆车的父物体上,并且手动把四个 WheelCollider 拖进对应槽位,播放时车就能动。

参数说明:moveSpeed是电机扭矩,受车辆质量影响极大。一辆质量为 1000kg 的车,500 扭矩起步会比较肉,适合用来测试物理配置;如果车重达到 2000kg,需要将扭矩提到 1000 以上才能达到“赛车”的体感。而turnSpeed对应的是转向角,角度太大高速过弯时后轮会抓不住地。

3.4 场景搭建时的三个隐藏依赖:灯光、相机和赛车刚体

很多新手把车从 Prefabs 拖进空场景后,发现画面里全是灰色或黑色,第一反应是模型坏了。其实是因为新场景里没有正确配置灯光和相机。Unity 默认的新场景带了一个方向和一支相机,但如果你从别的项目拷贝过设置,这些基础对象可能被删掉了。

最少需要的三样东西:一盏 Directional Light 用来照亮场景,一辆有 Rigidbody 的车,一架使用 SmoothFollow 脚本的相机。其中相机脚本是赛车项目最容易翻车的地方——如果相机跟随脚本直接把位置锁死在车身坐标,那车辆漂移、侧滑时画面会像钉住了一样僵硬。下一章专门讲相机的调校。

另外还有个容易忽略的地方:车辆刚体的质量分布。默认 Rigidbody 的中心在模型几何中心,而赛车车身低矮,几何中心偏高,过弯必然侧倾严重。上一章 CarController 里那一行rb.centerOfMass = new Vector3(0f, -0.5f, 0f)就是对这个问题的修正。你在自己的场景里拖车出来时,一定要确认这行代码被执行了,或者手动在 Rigidbody 的 Center Of Mass 属性里填一个偏下的值。

4. 驾驶手感调优:把物理参数、相机与粒子特效从“能开”变成“好开”

跑通源码只是第一步。接下来这段是最吃经验的部分:同样是四个轮子、一辆车、一条直线赛道,为什么别人的演示视频里车贴地飞行,而自己跑起来像在冰面上旋转?答案全在 WheelCollider 的参数组合和相机响应曲线里。这部分的调参过程非常像玄学,但只要把握住受力分析,大部分问题都能收敛。

4.1 必调参数表:WheelCollider 的五个关键值

WheelCollider 不是简单给车轮一个碰撞体,它内部模拟了悬架、轮胎弹性和地面摩擦。只改其中一个参数往往没有效果,必须组合调整。我把最常用的参数整理成一张表,方便对照项目情况来改:

参数作用手感影响刹车/起步时的变化
Spring(悬架弹簧)车身弹跳刚度数值低则车身软,过弯侧倾大起步抬头、刹车点头明显
Damper(悬架阻尼)抑制弹簧余震太高则车身硬,太低则车身颠簸阻尼不足时刹停后车身上下晃
Forward Friction(前向摩擦)轮胎加速和刹车的抓地力数值低则起步打滑、刹车距离长踩满油门时轮胎空转拉烟
Sideways Friction(侧向摩擦)抵抗横向滑动数值低则过弯甩尾,数值高则推头高速弯道中容易测出差别
Motor Torque(电机扭矩)驱动轮上的发力决定加速推背感和极速配合车轮转速决定是否烧胎

调参时的推荐顺序:先把 Spring 和 Damper 调到车身不上下晃动,再调 forwardFriction 让直线加速不打滑,最后调 sidewaysFriction 控制过弯姿态。这三个过程都会直接影响“这辆车是不是赛车”的体感。

4.2 车轮摩擦曲线:为什么默认值总是让车“太滑”

WheelCollider 里的摩擦不是用一个数字表达的,而是一张“速度—摩擦系数”曲线。Unity 默认的前向摩擦曲线在速度极低时摩擦系数几乎为 0,这就解释了为什么很多源码刚打开时低速起步车会打滑、掉头时像抹了黄油。

调整方式有两种:一种是把曲线上的关键点全部拉平,让低速和高速的摩擦系数趋于一致;另一种是使用WheelFrictionCurve直接写脚本设定,适合想精确控制手感、不需要在 Inspector 里反复拖曲线的开发者。

using UnityEngine; public class TireFrictionSetup : MonoBehaviour { public WheelCollider targetWheel; void Awake() { WheelFrictionCurve fFriction = targetWheel.forwardFriction; fFriction.extremumSlip = 0.4f; // 达到最大抓地力时的滑移率 fFriction.extremumValue = 1.2f; // 最大抓地力系数 fFriction.asymptoteSlip = 0.8f; // 抓地力开始衰减的滑移率 fFriction.asymptoteValue = 0.8f; // 衰减后的抓地力 fFriction.stiffness = 1.0f; // 整体刚度倍率 targetWheel.forwardFriction = fFriction; WheelFrictionCurve sFriction = targetWheel.sidewaysFriction; sFriction.extremumSlip = 0.2f; sFriction.extremumValue = 1.0f; sFriction.asymptoteSlip = 0.5f; sFriction.asymptoteValue = 0.6f; sFriction.stiffness = 1.0f; targetWheel.sidewaysFriction = sFriction; } }

代码说明:extremumSlip是轮胎开始发挥最大抓地力的滑移率,数值越小说明轮胎越“早”进入抓地状态;extremumValue直接决定抓地力峰值;asymptoteSlip和asymptoteValue则是超过极限后衰减的数据。stiffness是整体倍率,乘以所有曲线值,适合做“雨地打滑”等全局变种手感。

如果要做一个偏娱乐向的漂移赛车,可以把 Sideways Friction 的 extremumValue 降到 0.6 左右,让车在高速急弯时后轮主动侧滑;如果要做拟真向,这个值要提高到 1.5 上下,否则赛道竞技玩家会觉得车“抓不住地”。

4.3 相机跟随:从“钉死”到“有呼吸感”的平滑跟随脚本

相机是赛车手感里最容易被低估的部分。一辆车物理调得再好,如果相机死板地锁在车尾,玩家会感觉整个画面像被固定住,速度感完全消失。常见做法是使用带阻尼的平滑跟随,让相机有“追”车的感觉而不是“焊”在车上。

using UnityEngine; public class SmoothFollowCamera : MonoBehaviour { public Transform target; // 要跟随的赛车 public float distance = 8f; // 相机距离车辆的水平距离 public float height = 3f; // 相机高度 public float smoothTime = 0.3f; // 平滑时间,越大越钝 private Vector3 velocity = Vector3.zero; void LateUpdate() { Vector3 targetPos = target.position - target.forward * distance + Vector3.up * height; transform.position = Vector3.SmoothDamp(transform.position, targetPos, ref velocity, smoothTime); transform.LookAt(target.position + Vector3.up * 1.2f); } }

逻辑说明:SmoothDamp相当于一个带阻尼的弹簧,smoothTime越大,相机越追不上车,画面在急加速时会有短暂但舒服的滞后感。LookAt看向略高于车顶的位置,避免画面中心总是对着车尾保险杠。

一个非常常见的翻车现象是相机穿模:车辆漂移后车身横过来,相机镜头直接穿进车门。解决办法是在相机和车辆之间打一条射线,当中间有障碍物时把相机拉近到障碍物之前。这个技巧在复杂赛道场景里几乎是必需品。

4.4 粒子特效与音效:把“炫酷”做出来还不掉帧

King Of Racing 3D 之所以看起来“炫酷”,很大程度靠的是氮气加速时的尾焰粒子、漂移时的轮胎烟雾、碰撞时的火花。这些 ParticleSystem 脚本看起来简单,实际运行时有一个隐患:如果在每次碰撞或漂移时都动态Instantiate一套粒子系统,而粒子系统的Stop没有配合Clear,它会持续占用资源直到粒子完全衰减,这在移动端尤其明显,跑完一局后会感到帧率断崖式下跌。

正确做法是对象池:启动时预先创建好若干套粒子系统并藏在一个空物体下,需要触发时就近取一套播放,播放完毕后再归还池子。另外要给主粒子组件设置合适的maxParticles上限,尾焰控制在 50 以内,烟雾控制在 100 以内,火花控制在 30 以内,整体特效数目的浮动范围会稳定很多,这也是我在粒子特效上最想强调的一点:项目的卡顿往往不是模型精度,而是粒子系统数量失控。

5. 赛车源码的五个常见坑:现象、原因与处理办法

这部分记录的是我在导入与调优过程中反复踩过的坑,全部按“现象 → 原因 → 解决”的结构来写。每一条背后都对应一个真实开发场景,新人遇到时可以直接对号入座。

5.1 场景打开后一片黑,但编辑器能选中物体

现象:按下 Play 后画面全黑,场景里的车和地图看不到,但 Hierarchy 面板物体都在。

原因:最常见的是渲染管线和光照不匹配。这个项目如果使用 Built-in 渲染管线制作,在新版本中被打包为 URP 项目时,Standard Shader 会无法显示,表现为模型呈暗红色或全黑,或者烘焙贴图丢失。另一个原因是场景里没有 Directional Light,新手从默认场景创建新场景时误删了灯光。

解决:先把 Light 组件补上,确认 Watch 窗口里能看到光照强度;再把项目的渲染管线切换为 Built-in。具体操作是Edit → Graphics → Scriptable Render Pipeline Settings置为 None。如果不想切换管线,就给所有材质重新指定 URP/Lit Shader。

5.2 方向盘不响应,但脚本没有报错

现象:Play 后按下方向键,轮子不转向,车辆也不动,Console 面板干净得让人怀疑人生。

原因:输入系统被切换成了新的 Input System,而代码用的还是Input.GetAxis。在新输入系统激活状态下,旧的Input静态方法默认不工作,不会报错,只是所有输入值为 0。

解决:打开Project Settings → Player → Active Input Handling,改成 Input Manager (Old) 或 Both,然后重启编辑器。这一步是对旧源码兼容性影响最大的一处,几乎每台机器第一次打开时都要做。

5.3 车辆过弯直接翻车,或一直原地抖个不停

现象:直线行驶正常,一到弯道就侧翻;停车状态下车身不停上下抖动,像在“抽搐”。

原因:前者通常是centerOfMass没有压低,或者Spring调太高、刚体质量没设置,导致抗侧倾能力不足;后者是悬架弹簧和阻尼比例不匹配,Spring 过大而 Damper 过小,车体像一个没有阻尼的弹簧在那里来回振荡。

解决:先确认 Rigidbody 的centerOfMass低于模型底部 0.3~0.5 个单位;再把 Spring 和 Damper 按“弹簧数值约为阻尼的 2~3 倍”的区间调整。如果抖动只发生在静止时,还需要检查四个轮子的 WheelCollider 是否都在同一平面高度,地面动态摩擦是否突然变化。

5.4 导入后报数百条编译错误:WheelCollider方法找不到

现象:打开工程后 Console 里刷出大量编译错误,错误信息集中在WheelCollider、ParticleSystem相关 API 上,代码里还有new WheelCollider()之类的写法。

原因:源码可能是用很老的 Unity 5.x 或 2017 写的,API 已经变了;也可能是项目文件里的.meta文件缺失或损坏,导致脚本和预制体关联错乱。

解决:先把编译错误逐条分类。如果是 API 迁移问题,用Obsolete建议替换成新写法;如果是.meta丢失导致的 GUID 冲突,常见做法是删掉 Assets 目录下所有.meta文件让 Unity 重新生成。这个操作会丢失场景里的预制体引用关系,强烈建议在改之前先备份整个项目。

5.5 换电脑或换系统后,贴图和材质全部丢失

现象:项目在 A 电脑打开正常,拷贝到 B 电脑后模型变成灰色或粉色,贴图全没了。

原因:美术资源的引用是相对路径 + GUID 的。直接拷贝项目文件夹一般没问题,但如果你只拷贝了部分文件夹,比如只拷贝了 Assets 而忘了 Packages 和 ProjectSettings,Unity 重新生成后会因找不到 GUID 而丢失所有引用。

解决:拷贝项目时整个根目录一起拷,不要只拷 Assets。如果是用 Git 管理,检查.gitignore是否把Library/排除在外,首次打开时会重新构建 Library,耗时较长但不会丢资源。遇到材质丢失时,也可以直接把材质和贴图重新拖拽到模型的对应槽位上,比重做整个场景更快。

6. 进阶:把源码改装成自己的作品——AI 对手、圈速记录与移动端收尾

这一章做三件源码里最常见也最费精力的扩展:给赛道加 AI 赛车,让 AI 有基本的避让逻辑;做圈速排名,用 PlayerPrefs 保存成绩;最后把粒子上限、画质档位和 Draw Call 调到一个移动端可运行的水平。

6.1 给赛道加一个能跑的 AI 对手

多数赛车源码里只有一个玩家车辆,如果想加 AI 对手,最稳妥的不是给车辆写一套复杂的寻路系统,而是让 AI 车走“检查点列表 + 前方射线检测”的简化方案。让 AI 沿着检查点数组里的位置逐点移动,检测到前方有障碍物时自动减速。

using UnityEngine; public class AIDriver : MonoBehaviour { public Transform[] waypoints; // 复用人赛道上的检查点 public WheelCollider frontLeft; public WheelCollider frontRight; public WheelCollider rearLeft; public WheelCollider rearRight; public float maxSpeed = 80f; private int next = 0; private Rigidbody rb; void Start() { rb = GetComponent<Rigidbody>(); } void FixedUpdate() { Transform target = waypoints[next]; Vector3 dir = target.position - transform.position; dir.y = 0; // 根据目标点偏移计算转向 float steer = Vector3.SignedAngle(transform.forward, dir, Vector3.up); steer = Mathf.Clamp(steer, -30f, 30f) / 30f; frontLeft.steerAngle = steer * 30f; frontRight.steerAngle = steer * 30f; // 前进时限制速度,避免AI全程满油门冲出赛道 float forwardSpeed = Vector3.Dot(rb.linearVelocity, transform.forward); float throttle = forwardSpeed < maxSpeed ? 1f : 0f; rearLeft.motorTorque = throttle * 800f; rearRight.motorTorque = throttle * 800f; // 到达检查点附近则切换下一个 if (Vector3.Distance(transform.position, target.position) < 8f) { next = (next + 1) % waypoints.Length; } } }

这个脚本说明:AI 不是用物理寻路,而是把赛道检查点当作一条“轨道路径”,车辆在运行中不断把车头转向下一个目标点。Vector3.SignedAngle得到带符号的转向角,正值右转、负值左转。forwardSpeed是车辆在前进方向上的速度分量,防止 AI 在弯道内全油门冲出赛道。这套方案在不超过 5 辆 AI 车的场景里表现不错,虽然不会攻击玩家,但能形成基本的比赛压力。

6.2 圈速记录:用 PlayerPrefs 保存最佳成绩

赛车项目非常依赖圈速。圈速数据的写入时机是:车辆经过终点线完成一整圈时,把总时间记录下来。这里有一个很容易踩的坑:如果直接用Time.time计时,在场景暂停、切换或时间缩放时,数据会被污染。所以进阶做法是用一个独立计时器,不受Time.timeScale影响。

using UnityEngine; public class LapTimer : MonoBehaviour { public float currentLapTime = 0f; public float bestLapTime = 0f; void FixedUpdate() { currentLapTime += Time.fixedDeltaTime; // 不受 timeScale 影响 } void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) { // 触发终点线时,把当前圈速写入存档 if (bestLapTime <= 0 || currentLapTime < bestLapTime) { bestLapTime = currentLapTime; PlayerPrefs.SetFloat("BestLap", bestLapTime); PlayerPrefs.Save(); } currentLapTime = 0f; } } }

Time.fixedDeltaTime在物理帧上是恒定的,不受Time.timeScale影响,适合做比赛计时。而PlayerPrefs只适合存少量数据,存圈速这种标量足够;如果要做排行榜或者回放数据,就要用 JSON 或 SQLite 了。我把bestLapTime和当前对比后写入存档,这样重启游戏后仍能看到最快圈速。

6.3 移动端收尾:把粒子上限、画质和 Draw Call 控制住

最后说移动端。King Of Racing 3D 这种项目拿到手机上看效果,首先要做的是为移动端准备一套低配预制体。我会把粒子系统的最大粒子数限制在 100 以内,关闭碰撞体积,关闭所有实时阴影,改用烘焙光贴图。画质档位上,在菜单中提供一个简单的分级选项,避免在低端机上直接进最高画质帧率崩成幻灯片。

车辆和场景的材质用尽量少的材质球数,同一个车身贴图尽量共用。Draw Call 是最容易忽视的优化点,一辆车轮子模型如果是独立材质,一个轮子一个 Draw Call,四个轮子加车漆加玻璃就是 6 个以上的 Draw Call,这么一辆车在全屏粒子下会让低端机非常吃力。常见做法是把车漆和玻璃合并到一张贴图上,轮毂用共享材质,场景道具用Static Batching来合并。

6.4 我的收尾习惯:先跑 10 分钟热循环验证再提交

这是我个人的习惯,也是踩过太多次热循环内存泄漏的教训总结。任何项目在提交前,我都会开着编辑器跑一个 10 分钟的自动循环测试:让车辆沿着赛道自动行驶,持续触发碰撞、漂移和氮气,观察内存曲线是否持续上涨。如果每次漂移都动态生成粒子系统,内存曲线会在 3 分钟内明显上扬,那就要回去整理粒子池了。如果你拿到这份源码后只做一件事,我建议也是跑一遍热循环测试,亲自看着内存曲线在 10 分钟内保持水平,再往它的代码里加自己的内容。

这套源码最值得参考的价值,就是把赛车游戏里最容易失控的物理和特效做成了可改的参数骨架。你不需要把整个项目理解透才能开始用,先让车跑起来,再用自己的美术和赛道逐步替换,慢慢就会摸清每个值背后对应到方向盘上的感受。希望这些从一线项目里磨出来的经验能帮你少走几段弯路,把这份源码改造成真正属于你的赛车作品。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询