问题考点
为什么做这个项目?
做这个赛车项目,主要是因为它很适合作为 Unity 入门到进阶的小型综合项目。
它能练到很多核心内容:车辆控制、物理碰撞、摄像机跟随、赛道设计、UI 显示、计时/得分、游戏胜负逻辑等。相比只做一个静态场景,赛车项目有明确目标和交互反馈,玩家能直接感受到操作、速度和挑战。
如果写成项目说明,可以这样说:
本项目以赛车竞速为主题,旨在通过 Unity 实现一个具有基础驾驶、碰撞检测、赛道场景和游戏交互的赛车游戏。
通过该项目,可以综合练习 Unity 场景搭建、物理系统、脚本编程、用户输入和游戏逻辑设计,提高对游戏开发流程的理解和实践能力。
简单说:这个项目好玩、目标清楚,而且能把 Unity 的很多关键知识点串起来。
程序化赛道生成具体是怎么实现的?
程序化赛道主要在 WorldGenerator.cs 里实现。它不是用 Unity Terrain,而是运行时自己创建 Mesh,把一段段圆筒隧道拼起来。
核心流程
- 开局生成两段赛道
Start()里创建beginPoints,然后生成 2 个World piece,放进pieces[0]和pieces[1]。 位置间隔是:
dimensions.y * scale * Mathf.PI对应 WorldGenerator.cs (line 34) 和 WorldGenerator.cs (line 76)。
每段赛道是一个圆筒网格
CreateCylinder()创建一个空物体,添加MeshFilter、MeshRenderer、MeshCollider,然后把Generate()生成的网格塞进去。 见 WorldGenerator.cs (line 131)。顶点按圆筒分布 在
CreateShape()中,外层x表示圆周方向,内层z表示前进方向。 每个点的位置大概是:
float angle = x * Mathf.PI * 2f / xCount;
vertices[index] = new Vector3(
Mathf.Cos(angle) * radius,
Mathf.Sin(angle) * radius,
z * scale * Mathf.PI
);也就是把一排排顶点绕成圆筒,代码在 WorldGenerator.cs (line 179)。
用 Perlin Noise 做隧道起伏
初始圆筒太规整,所以它对每个顶点采样 Mathf.PerlinNoise(),然后沿着“指向圆筒中心”的方向把顶点往内推:
vertices[index] += (center - vertices[index]).normalized
* Mathf.PerlinNoise(pX, pZ)
* waveHeight;perlinScale 控制噪声密度,waveHeight 控制起伏强度,offset 控制每段赛道的噪声偏移。对应 [WorldGenerator.cs (line 206)](/E:/Unity/UnityProjects/ClassCode/Racing car/Assets/Procedural Racing/Scripts/WorldGenerator.cs:206)。
两段赛道平滑衔接
它用 beginPoints 保存上一段赛道最后一排顶点。生成新赛道开头几排时,会把“上一段末尾形状”和“新噪声形状”做线性混合:
vertices[index] =
(perlinPercentage * vertices[index])
+ ((1f - perlinPercentage) * beginPoint);这样不会在两段圆筒之间出现明显断层。对应 WorldGenerator.cs (line 217)。
- 三角面拼网格 顶点生成后,代码按每个小方格生成 2 个三角形,也就是每格 6 个顶点索引。 这一段在 WorldGenerator.cs (line 246)。
- 无限赛道刷新 游戏不是真的让车往前跑很远,而是赛道块向玩家移动。 当第二段赛道移动到
z <= 0,就销毁第一段,把第二段变成第一段,再生成新的第二段接在后面。 对应 WorldGenerator.cs (line 44) 和 WorldGenerator.cs (line 86)。 - 随机生成障碍和得分门 在生成顶点时,会随机决定是否在该顶点位置刷道具:
if(Random.Range(0, startObstacleChance) == 0)
CreateItem(vertices[index], x);CreateItem() 再随机选择 Gate 或障碍物,并让它朝向圆筒中心,最后挂到当前赛道块下面一起移动。见 WorldGenerator.cs (line 237) 和 WorldGenerator.cs (line 292)。
一个小细节:startObstacleChance 会随着生成新赛道逐渐减小,所以 Random.Range(0, startObstacleChance) == 0 的概率会变大,游戏越跑障碍越密。
物理碰撞是怎么做的?
NOTE
这个项目的物理碰撞主要是靠 Unity 自带物理系统 做的,不是自己手写碰撞公式。
核心做法是:
赛车对象挂了
Rigidbody和WheelCollider在 Car.cs (line 8) 里可以看到:CAUTION
Rigidbody rb:让赛车参与物理计算WheelCollider[] wheelColliders:负责车轮和地面的接触Physics.Raycast:检测车轮下面有没有地面,用来显示草地效果/刹车痕rb.AddForceAtPosition(...):车尾离地时加一个向下的力,让车更稳定
撞到障碍物会触发失败 在 Obstacle.cs (line 15) 里:
void OnCollisionEnter(Collision other){
if(other.gameObject.transform.root.CompareTag("Player"))
manager.GameOver();
}意思是:当赛车和障碍物发生真实物理碰撞时,如果撞到的是玩家赛车,就调用 GameOver()。
WARNING
- 穿过得分门用的是触发器 在 Gate.cs (line 19) 里用
OnTriggerEnter,它不是“撞上去挡住”,而是“穿过去检测到”。 - 翻车检测也用触发器 CarGameOverTrigger.cs (line 15) 判断车顶是否碰到地面,如果车顶碰到
"World piece",说明赛车翻了,也会游戏结束。
可以写成答辩说明:
本项目通过 Unity 的 Rigidbody、Collider、WheelCollider 和 Trigger 实现物理碰撞。赛车使用 Rigidbody 参与物理运算,车轮使用 WheelCollider 与地面接触;障碍物通过 OnCollisionEnter 检测真实碰撞并触发游戏失败;得分门和翻车检测使用 OnTriggerEnter 判断玩家是否通过或车辆是否翻倒。碰撞后由 GameManager 统一处理游戏结束、车辆散架和场景停止等逻辑。
项目难点是什么?
NOTE
这个项目的难点主要在于 赛车物理和游戏逻辑的配合。
可以从这几个方面说:
车辆控制不容易稳定 赛车要能转向、前进、贴合地面,还不能一转弯就翻车。项目里用了
Rigidbody、WheelCollider和额外的向下力来保持车身稳定。碰撞检测要准确 障碍物碰撞、穿过得分门、车顶触地翻车,这些情况不能混在一起。项目里分别用了
OnCollisionEnter和OnTriggerEnter来处理不同事件。游戏结束逻辑要统一 撞到障碍物或翻车后,要同时完成车辆散架、停止赛道移动、播放音效、显示失败界面等操作,所以需要
GameManager统一管理。赛道和车辆表现要协调 车在动,赛道也在运动和旋转,摄像机还要跟随。如果这些速度、角度、位置没有配好,游戏体验会很怪。
细节反馈比较多 比如轮胎痕迹、草地效果、得分音效、失败动画等,这些虽然不是核心功能,但能明显提升游戏完整度。
Perlin Noise(柏林噪声)算法 来做程序化无限赛道生成。
在 WorldGenerator.cs (line 213) 里:
Mathf.PerlinNoise(pX, pZ)它用柏林噪声改变圆柱赛道网格顶点的位置,让赛道表面有起伏变化。项目还用了“两段赛道循环”的方式:前一段离开视野后销毁,再生成新的一段接到后面,并用 beginPoints 做平滑过渡,所以看起来像无限赛道。
可以写成项目说明:
本项目的主要难点在于赛车物理控制、碰撞检测和游戏状态管理。赛车需要在高速移动和转向时保持稳定,同时要准确判断障碍物碰撞、得分门触发和翻车情况。项目通过 Rigidbody、WheelCollider、Collision 和 Trigger 等 Unity 物理机制实现检测,并由 GameManager 统一处理得分、失败、车辆散架和场景停止等逻辑。