1. 项目概述从“坐标点”到“游戏世界”的桥梁如果你刚开始接触Unity游戏开发可能会觉得“向量”这个词听起来既数学又抽象离我们想做的“让角色跑起来”、“让子弹飞出去”这些具体目标很远。但我想告诉你的是向量是游戏世界最底层的“通用语言”它定义了位置、方向和速度。不理解向量就像想盖房子却不认识砖头。这个项目就是要把这块“砖头”从数学课本里拿出来亲手砌进我们的游戏里。具体来说我们将聚焦于游戏开发中最基础也最核心的两个需求角色移动和碰撞检测。这几乎是所有互动游戏无论是2D平台跳跃还是3D开放世界的基石。我将全程使用Unity引擎和C#语言通过手写代码的方式带你一步步实现。你不会看到任何拖拽组件就完成的“魔法”而是理解每一行代码背后的向量运算逻辑。为什么不用现成的组件因为只有亲手实现过当角色卡在墙角、碰撞反应诡异时你才知道问题出在向量的哪个分量上才能施展真正的“调试魔法”。这篇文章适合有一定C#基础刚入门Unity对游戏逻辑实现充满好奇但又被数学概念困扰的开发者。我们将从零开始构建一个可运行的小场景你会看到向量如何从Vector3这个冷冰冰的变量变成屏幕上活灵活现的运动和交互。2. 核心思路拆解向量如何驱动游戏逻辑在动手写代码之前我们必须把思路理清楚。游戏中的每一个动态对象其状态都可以用向量来描述。我们的核心思路就是用向量运算来更新游戏对象的状态并用向量几何关系来判断对象间的交互。2.1 角色移动的本质速度向量与位置更新角色移动不是一个瞬间完成的“传送”而是一个随时间累积的过程。在计算机中时间是离散的我们通过每一帧Frame来模拟连续时间。移动的核心公式源于物理学新位置 当前位置 速度向量 * 时间增量这里的“速度向量”velocity是关键。它不是一个标量大小而是一个向量同时包含了大小速率和方向。例如velocity new Vector3(5f, 0f, 2f)表示物体每秒钟在X轴正方向移动5个单位在Z轴正方向移动2个单位。在Unity中Time.deltaTime代表了上一帧到当前帧的时间间隔。乘以它是为了确保无论玩家的电脑是每秒30帧还是120帧物体的移动速度都是恒定的。这是新手极易忽略但至关重要的点不加Time.deltaTime的移动帧率越高角色飞得越快。2.2 碰撞检测的基石边界与距离碰撞检测听起来高级其最基础的原理就是判断两个物体的边界是否发生了重叠。对于规则形状如方块、球体Unity提供了高效的Collider组件和物理引擎。但我们手写代码是为了理解本质。我们将实现两种基础的检测方法轴对齐包围盒AABB检测适用于不会旋转的方形物体。我们通过比较两个物体在X、Y、Z轴上的最大最小坐标来判断是否重叠。这是性能最高、实现最简单的检测方式。圆形球形检测适用于需要平滑碰撞或物体近似圆形的场景。其核心是计算两个圆心球心之间的距离并与两圆半径之和进行比较。距离小于半径和即发生碰撞。这里向量的作用就凸显出来了。计算距离本质上就是计算两点两个向量之间的差值向量的长度模。Vector3.Distance(a, b)这个便捷方法的背后就是(a - b).magnitude。2.3 方案选型为何不直接使用物理引擎Unity自带的Rigidbody和Collider物理系统非常强大对于复杂的物理模拟重力、摩擦力、弹力是首选。但我们选择手写代码基于以下几点考量学习价值物理引擎是黑盒出了问题难以调试。手写一遍能让你透彻理解运动与碰撞的底层逻辑这是成为高级游戏程序员的必经之路。性能与控制对于大量简单的游戏对象比如成千上万的子弹、粒子完整的物理引擎开销巨大。轻量级的自定义移动和碰撞检测可以极大提升性能。特定需求游戏逻辑需要的碰撞反馈往往不是真实的物理反馈。比如角色碰到敌人后掉血而非被弹开碰到金币后金币消失而非发生碰撞。自定义代码可以完全按照游戏规则来响应碰撞事件。我们的方案是在Update或FixedUpdate中用代码管理角色的移动通过每帧计算物体边界信息来进行自定义的碰撞查询。这给了我们最大的灵活性和控制精度。3. 环境准备与项目搭建理论清晰后我们开始搭建实战环境。请确保你已安装Unity Hub和任意一个较新的Unity LTS版本如2022.3 LTS。3.1 创建新项目与场景设置打开Unity Hub新建一个3D核心模板项目命名为VectorMagicDemo。进入项目后进行以下初始设置场景清理删除默认场景中的Main Camera和Directional Light我们将创建自己控制的相机。创建地板在层级窗口右键 - 3D Object - Plane重命名为Ground。将其Scale设置为(10, 1, 10)使其成为一个广阔的地面。创建玩家角色右键 - 3D Object - Cube重命名为Player。将其位置Position设为(0, 0.5, 0)这样它的底部刚好贴合地面Cube默认高度为1中心点在几何中心。创建相机右键 - Camera重命名为FollowCamera。调整其位置如(0, 5, -10)和旋转如X轴旋转25度使其能俯视Player。我们稍后会为它写一个简单的跟随脚本。创建测试障碍物再创建几个Cube分别重命名为Obstacle1、Obstacle2随意摆放在Player周围。再创建一个Sphere重命名为Enemy作为圆形检测的测试目标。注意在3D项目中确保你的游戏视图Game View的纵横比合适。我建议暂时使用Free Aspect并窗口化运行以便观察。3.2 创建核心脚本与文件夹结构清晰的项目结构有助于管理。在Project窗口的Assets文件夹下创建Scripts文件夹。在Scripts文件夹内创建PlayerController.cs玩家移动与控制、CustomCollider.cs自定义碰撞体基类、AABBCollider.csAABB碰撞体、SphereCollider.cs球形碰撞体和SimpleCameraFollow.cs相机跟随。接下来我们将逐一实现这些脚本。先从玩家的移动开始这是最直接的向量应用。4. 实现角色移动向量驱动的运动系统为Player对象挂载PlayerController脚本。双击打开脚本我们将实现一个基于输入和速度向量的移动控制器。4.1 定义移动参数与组件引用using UnityEngine; public class PlayerController : MonoBehaviour { [Header(移动参数)] public float moveSpeed 5f; // 移动速度 public float jumpForce 7f; // 跳跃力 public float gravity -20f; // 自定义重力加速度 [Header(地面检测)] public Transform groundCheck; // 用于检测地面的空物体 public float groundDistance 0.4f; // 检测距离 public LayerMask groundMask; // 指定哪些层是地面 private CharacterController controller; // Unity内置的角色控制器比Rigidbody更轻量 private Vector3 velocity; // 当前速度向量 (x:水平速度, y:垂直速度) private bool isGrounded; // 是否在地面 void Start() { // 尝试获取CharacterController组件若没有则自动添加 controller GetComponentCharacterController(); if (controller null) { controller gameObject.AddComponentCharacterController(); } // 如果未指定groundCheck则创建一个子物体 if (groundCheck null) { GameObject checkObj new GameObject(GroundCheck); checkObj.transform.parent transform; checkObj.transform.localPosition Vector3.down * (controller.height / 2); // 放在脚底 groundCheck checkObj.transform; } } }这里我选择了CharacterController组件而非Rigidbody。CharacterController是专门为第一/第三人称角色移动设计的它提供了Move方法能很好地处理与网格碰撞体的互动且不会受到真实的物理力影响更符合我们“代码控制”的初衷。重力我们也选择自己模拟以便完全掌控。4.2 在Update中处理每帧逻辑移动逻辑需要每帧执行我们将其放在Update中。核心分为三步检测地面、处理输入、应用重力与移动。void Update() { // 1. 地面检测 (使用球形检测比射线检测更稳定) isGrounded Physics.CheckSphere(groundCheck.position, groundDistance, groundMask); // 重置垂直速度如果在地面且正在下落将垂直速度归零防止重力累积 if (isGrounded velocity.y 0) { velocity.y -2f; // 一个很小的负值确保角色紧贴地面 } // 2. 获取水平移动输入 float horizontal Input.GetAxis(Horizontal); // A/D 或 左右箭头范围[-1, 1] float vertical Input.GetAxis(Vertical); // W/S 或 上下箭头范围[-1, 1] // 3. 构建水平移动向量 // 注意TransformDirection将本地方向转换为世界方向。 // 假设玩家面朝Z轴正方向Input.GetAxis获取的是基于玩家自身的“前后左右”。 Vector3 moveDirection transform.right * horizontal transform.forward * vertical; // 4. 使用CharacterController移动已包含帧时间补偿 controller.Move(moveDirection * moveSpeed * Time.deltaTime); // 5. 处理跳跃 if (Input.GetButtonDown(Jump) isGrounded) { velocity.y Mathf.Sqrt(jumpForce * -2f * gravity); // 由物理公式 v sqrt(2gh) 推导 } // 6. 应用重力 velocity.y gravity * Time.deltaTime; // 7. 应用垂直速度跳跃和重力 controller.Move(velocity * Time.deltaTime); }关键点解析Input.GetAxis返回一个平滑的输入值非常适合角色移动。GetAxisRaw则返回非平滑的-1 0 1适合格斗游戏。transform.right和transform.forward是玩家自身的右方向和前方向向量单位向量。将它们与输入值相乘再相加就得到了基于玩家自身坐标系的世界空间移动方向向量。这是实现“按W向前走按A向左走”的关键。CharacterController.Move()方法内部会处理与场景碰撞体的碰撞但不会产生物理反馈。它比直接修改Transform.position更可靠。跳跃速度的计算使用了物理学公式确保无论重力值如何设置跳跃高度都由jumpForce直观控制。回到Unity编辑器将Player对象的Layer设为Player为Ground对象创建一个新层Ground并赋值。在PlayerController组件的Ground Mask中选择Ground层。运行游戏你现在应该可以用WASD控制方块移动用空格键跳跃了。5. 构建自定义碰撞检测系统现在角色可以动了但会穿过我们的障碍物。接下来我们抛开Unity的物理碰撞自己实现一套简单的碰撞检测系统。5.1 设计碰撞体基类首先创建CustomCollider.cs这是一个抽象基类定义了所有自定义碰撞体的共同接口。using UnityEngine; public abstract class CustomCollider : MonoBehaviour { public bool isTrigger false; // 是否为触发器仅检测不阻挡 public Vector3 offset; // 碰撞体相对于物体中心的偏移 // 关键方法检查与另一个CustomCollider是否碰撞 public abstract bool CheckCollision(CustomCollider other); // 在Scene视图中绘制碰撞体轮廓Gizmos便于调试 protected abstract void OnDrawGizmosSelected(); // 获取碰撞体在世界空间中的中心点 public Vector3 Center { get { return transform.position transform.rotation * offset; } } }5.2 实现AABB轴对齐包围盒碰撞体创建AABBCollider.cs。AABB的特点是它的边始终与世界坐标轴平行因此计算极其高效。using UnityEngine; public class AABBCollider : CustomCollider { public Vector3 size Vector3.one; // 碰撞体的大小半尺寸 // 获取世界空间下的最小点和最大点坐标 public Vector3 Min { get { Vector3 center Center; Vector3 halfSize size * 0.5f; return center - halfSize; } } public Vector3 Max { get { Vector3 center Center; Vector3 halfSize size * 0.5f; return center halfSize; } } public override bool CheckCollision(CustomCollider other) { if (other is AABBCollider otherAABB) { // AABB vs AABB 检测在所有轴上都有重叠才为真 bool overlapX Min.x otherAABB.Max.x Max.x otherAABB.Min.x; bool overlapY Min.y otherAABB.Max.y Max.y otherAABB.Min.y; bool overlapZ Min.z otherAABB.Max.z Max.z otherAABB.Min.z; return overlapX overlapY overlapZ; } else if (other is SphereCollider otherSphere) { // AABB vs Sphere 检测找到AABB上距离球心最近的点计算该点到球心的距离 Vector3 closestPoint Vector3.zero; closestPoint.x Mathf.Clamp(otherSphere.Center.x, Min.x, Max.x); closestPoint.y Mathf.Clamp(otherSphere.Center.y, Min.y, Max.y); closestPoint.z Mathf.Clamp(otherSphere.Center.z, Min.z, Max.z); float distance Vector3.Distance(closestPoint, otherSphere.Center); return distance otherSphere.radius; } // 可以继续扩展与其他类型碰撞体的检测... return false; } protected override void OnDrawGizmosSelected() { Gizmos.color isTrigger ? Color.green : Color.red; Gizmos.matrix Matrix4x4.TRS(Center, Quaternion.identity, size); Gizmos.DrawWireCube(Vector3.zero, Vector3.one); } }核心逻辑解读Mathf.Clamp函数是这里的魔法。它将球心的每个坐标分量“钳制”在AABB的对应轴的最小值和最大值之间。这样得到的closestPoint就是AABB这个立方体上距离球心最近的那个点。计算closestPoint到球心的距离如果小于球半径则发生碰撞。这个算法高效且准确是游戏编程中非常经典的检测方法。5.3 实现球形碰撞体创建SphereCollider.cs。using UnityEngine; public class SphereCollider : CustomCollider { public float radius 0.5f; public override bool CheckCollision(CustomCollider other) { if (other is SphereCollider otherSphere) { // Sphere vs Sphere 检测距离与半径和比较 float distance Vector3.Distance(Center, otherSphere.Center); return distance (radius otherSphere.radius); } else if (other is AABBCollider otherAABB) { // Sphere vs AABB 检测复用AABB中的逻辑检测是对称的 return otherAABB.CheckCollision(this); } return false; } protected override void OnDrawGizmosSelected() { Gizmos.color isTrigger ? Color.cyan : Color.yellow; Gizmos.DrawWireSphere(Center, radius); } }球形检测是最简单的核心就是Vector3.Distance。注意在Sphere vs AABB检测中我们直接调用了otherAABB.CheckCollision(this)利用了已经写好的检测逻辑避免了代码重复。5.4 创建碰撞管理器与响应我们需要一个管理器来收集场景中所有的CustomCollider并在每帧检查它们之间的碰撞。创建一个名为CollisionManager.cs的单例脚本。using System.Collections.Generic; using UnityEngine; public class CollisionManager : MonoBehaviour { public static CollisionManager Instance; private ListCustomCollider allColliders new ListCustomCollider(); void Awake() { if (Instance ! null Instance ! this) { Destroy(this.gameObject); } else { Instance this; } } public void Register(CustomCollider collider) { if (!allColliders.Contains(collider)) { allColliders.Add(collider); } } public void Unregister(CustomCollider collider) { allColliders.Remove(collider); } void Update() { // 简单的两两检测对于对象很多时需优化如空间划分 for (int i 0; i allColliders.Count; i) { for (int j i 1; j allColliders.Count; j) { CustomCollider colA allColliders[i]; CustomCollider colB allColliders[j]; if (colA.CheckCollision(colB)) { // 触发碰撞事件 colA.SendMessage(OnCustomCollision, colB, SendMessageOptions.DontRequireReceiver); colB.SendMessage(OnCustomCollision, colA, SendMessageOptions.DontRequireReceiver); // 如果不是触发器则进行简单的弹开反应示例 if (!colA.isTrigger !colB.isTrigger) { HandleCollisionResponse(colA, colB); } } } } } private void HandleCollisionResponse(CustomCollider a, CustomCollider b) { // 一个非常简单的响应将两个物体沿中心连线方向推开 Vector3 dir (b.Center - a.Center).normalized; float pushForce 0.1f; // 推开力度 // 这里只是简单移动Transform在实际项目中可能需要更复杂的物理响应 a.transform.position - dir * pushForce; b.transform.position dir * pushForce; } }然后在每个CustomCollider的OnEnable和OnDisable方法中注册和注销自己。// 在AABBCollider和SphereCollider类中添加 void OnEnable() { if (CollisionManager.Instance ! null) CollisionManager.Instance.Register(this); } void OnDisable() { if (CollisionManager.Instance ! null) CollisionManager.Instance.Unregister(this); }最后在场景中创建一个空物体挂载CollisionManager脚本。6. 整合测试与高级运动优化现在让我们将自定义碰撞体应用到物体上并优化玩家的移动体验。6.1 为物体添加碰撞体并测试为Player对象添加AABBCollider组件Size设置为(1, 1, 1)。为Obstacle1和Obstacle2添加AABBCollider根据Cube大小调整Size。为Enemy球体添加SphereCollider组件Radius设置为0.5。运行游戏。现在控制玩家撞向障碍物或敌人你会看到它们被非常生硬地推开。这就是我们自定义碰撞检测与简单响应在起作用6.2 优化玩家移动与碰撞响应目前的PlayerController使用CharacterController它会与Unity的物理碰撞体相互作用但与我们自定义的碰撞体无关。为了让玩家也受我们自定义系统影响我们需要修改PlayerController。首先移除Player对象上的CharacterController组件。然后修改PlayerController.cs脚本用我们自己的移动和碰撞逻辑。我们将为玩家也添加一个AABBCollider并在移动前进行预检测防止穿墙。public class PlayerController : MonoBehaviour { // ... 保留之前的移动参数、跳跃、重力等变量 ... private AABBCollider myCollider; private Vector3 horizontalVelocity; // 水平速度向量 private Vector3 verticalVelocity; // 垂直速度向量 void Start() { myCollider GetComponentAABBCollider(); if (myCollider null) { Debug.LogError(Player 需要 AABBCollider 组件); } // 不再需要CharacterController } void Update() { // 地面检测可以用自定义碰撞体实现这里为简化仍用Physics isGrounded Physics.CheckSphere(groundCheck.position, groundDistance, groundMask); if (isGrounded verticalVelocity.y 0) verticalVelocity.y -2f; // 处理输入 float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 desiredMove (transform.right * horizontal transform.forward * vertical).normalized; // 计算期望的水平位移 Vector3 horizontalDisplacement desiredMove * moveSpeed * Time.deltaTime; // **关键碰撞预检测与解决** horizontalDisplacement ResolveCollision(horizontalDisplacement); // 应用水平位移 transform.position horizontalDisplacement; // 跳跃与重力 if (Input.GetButtonDown(Jump) isGrounded) { verticalVelocity.y Mathf.Sqrt(jumpForce * -2f * gravity); } verticalVelocity.y gravity * Time.deltaTime; // 垂直方向的位移同样可以加入碰撞检测这里简化处理 transform.position verticalVelocity * Time.deltaTime; } private Vector3 ResolveCollision(Vector3 displacement) { // 获取移动后AABB的预估位置 Vector3 predictedMin myCollider.Min displacement; Vector3 predictedMax myCollider.Max displacement; // 遍历所有其他碰撞体这里从CollisionManager获取简化示例 AABBCollider[] allColliders FindObjectsOfTypeAABBCollider(); foreach (var other in allColliders) { if (other myCollider || other.isTrigger) continue; // 检查在X、Y、Z轴上是否有重叠 bool overlapX predictedMin.x other.Max.x predictedMax.x other.Min.x; bool overlapY predictedMin.y other.Max.y predictedMax.y other.Min.y; bool overlapZ predictedMin.z other.Max.z predictedMax.z other.Min.z; if (overlapX overlapY overlapZ) { // 发生碰撞计算解决向量 // 分别计算在X和Z轴上需要修正的位移Y轴暂不考虑由地面检测和跳跃处理 float resolveX 0f; float resolveZ 0f; // 判断从哪边碰撞并修正位移 if (displacement.x 0) // 向右移动时撞到左侧 resolveX other.Min.x - predictedMax.x; else if (displacement.x 0) // 向左移动时撞到右侧 resolveX other.Max.x - predictedMin.x; if (displacement.z 0) // 向前移动时撞到后面 resolveZ other.Min.z - predictedMax.z; else if (displacement.z 0) // 向后移动时撞到前面 resolveZ other.Max.z - predictedMin.z; // 应用修正阻止穿透 displacement.x resolveX; displacement.z resolveZ; // 修正后重新计算预测边界进行下一次检测简单处理理想情况应循环直到无碰撞 predictedMin myCollider.Min displacement; predictedMax myCollider.Max displacement; } } return displacement; } }这个ResolveCollision函数是一个简化的碰撞解决Collision Resolution示例。它预测移动后的位置检查是否与其他AABB相交如果相交则在相应的轴X或Z上抵消一部分位移使玩家刚好停在障碍物边缘。这是一个非常基础的实现真实的碰撞解决要处理滑动、夹角碰撞等复杂情况但原理相通。7. 常见问题、调试技巧与性能优化在实际编码和测试中你肯定会遇到各种奇怪的问题。下面是我在实现这类系统时积累的一些经验和排查思路。7.1 碰撞检测失灵或时好时坏问题现象角色有时会穿过物体有时又会被卡住。排查步骤检查Gizmos在Scene视图中确保碰撞体的Gizmos通过OnDrawGizmosSelected绘制大小和位置正确。这是最直观的调试手段。检查更新顺序确保CollisionManager的Update在所有物体的位置更新之后执行。在Unity中脚本执行顺序可以通过Edit - Project Settings - Script Execution Order来调整将CollisionManager的执行顺序设晚一些。检查单帧位移过大如果角色移动速度极快moveSpeed值很大一帧的位移可能超过碰撞体的尺寸导致从物体一侧“穿越”到另一侧而未被检测到。这就是所谓的“子弹穿过纸张”问题Tunneling。解决方案连续碰撞检测CCD对于高速移动的物体不能只检测起点和终点。一种方法是进行“扫描体”检测即检测物体从上一帧到当前帧的整个运动轨迹所形成的体积。另一种更简单的方法是使用Raycast或CapsuleCast进行预测。限制最大速度给高速物体如子弹一个合理的速度上限。增加碰撞体尺寸适当增大碰撞体特别是运动方向上的尺寸作为“安全边界”。7.2 碰撞响应不自然或抖动问题现象物体碰撞后剧烈抖动、被吸入墙体或弹飞轨迹奇怪。排查步骤检查响应逻辑在HandleCollisionResponse中打印出碰撞法线方向dir和推开力度。确保方向计算正确从A指向B。检查每帧多次响应可能在单帧内碰撞检测和响应被触发了多次导致位置被反复修正。可以在碰撞体对之间添加一个短暂的“冷却期”或者确保一次碰撞只在第一帧处理响应。浮点数精度问题当两个物体非常接近时由于浮点数精度可能下一帧又检测为碰撞导致来回推挤。可以在解决碰撞时加入一个微小的偏移量Epsilon如0.001f。解决方案实现更精确的投影法上述简单的反向推开方法很粗糙。更成熟的方法是计算穿透深度Penetration Depth然后沿着最小穿透深度的轴将物体移出。这需要更复杂的几何计算。引入物理材质为碰撞体添加bounciness弹性和friction摩擦力属性让碰撞响应更符合物理直觉。7.3 性能瓶颈与优化策略当场景中有成百上千个活动碰撞体时两两检测的O(n²)复杂度会立刻导致帧率下降。基础优化空间划分Spatial Partitioning网格法Grid将世界划分为均匀的网格。每个碰撞体根据其位置注册到所在的网格中。检测时只检测同一网格及相邻网格内的碰撞体。这是实现最简单、对均匀分布物体最有效的优化。四叉树/八叉树Quadtree/Octree递归地将空间划分为四个2D或八个3D子区域直到每个区域内的物体数量少于某个阈值。适合物体分布不均匀的场景。包围盒层次结构BVH动态地将物体组织成一棵树树的每个节点都是一个能包围其所有子节点的AABB。从根节点开始检测如果两个节点的AABB不相交则其所有子节点都不需要检测。常用于物理引擎和光线追踪。一个简单的网格法优化示例// 在CollisionManager中 public float gridSize 5f; // 网格大小 private DictionaryVector3Int, ListCustomCollider spatialGrid new DictionaryVector3Int, ListCustomCollider(); private Vector3Int GetGridKey(Vector3 position) { int x Mathf.FloorToInt(position.x / gridSize); int y Mathf.FloorToInt(position.y / gridSize); int z Mathf.FloorToInt(position.z / gridSize); return new Vector3Int(x, y, z); } public void UpdateGridPosition(CustomCollider collider, Vector3 oldPos, Vector3 newPos) { Vector3Int oldKey GetGridKey(oldPos); Vector3Int newKey GetGridKey(newPos); if (oldKey ! newKey) { // 从旧网格移除添加到新网格 if (spatialGrid.ContainsKey(oldKey)) spatialGrid[oldKey].Remove(collider); if (!spatialGrid.ContainsKey(newKey)) spatialGrid[newKey] new ListCustomCollider(); spatialGrid[newKey].Add(collider); } } // 在检测时只遍历目标网格及其26个相邻网格中的碰撞体7.4 向量运算的精度与归一化陷阱零向量归一化对Vector3.zero调用Normalize()或访问.normalized属性会导致除以零得到(NaN, NaN, NaN)后续所有计算都会出错。在归一化前务必检查向量长度是否大于一个极小值如Mathf.Epsilon。浮点数比较不要直接用比较两个浮点数向量是否相等。应使用Vector3.Distance(a, b) tolerance或(a - b).sqrMagnitude tolerance*tolerance平方距离比较更快。向量运算的顺序transform.TransformDirection()和transform.TransformPoint()有区别。前者只旋转向量方向后者同时旋转和位移向量点。在将本地坐标转换到世界坐标时要注意区分。手写一套完整的碰撞检测与响应系统是一个庞大的工程本文带你走完了最核心的路径。从理解向量的加减乘除到用它们描述运动再到用它们定义的几何关系进行碰撞判断最后处理碰撞后的反应。每一步都离不开向量的身影。这套系统虽然简单但已经包含了商业引擎中复杂物理系统的核心思想。当你再使用Unity的Rigidbody时你就能明白它内部大概在做什么以及当出现诡异物理现象时应该从哪个方向去思考和调试。这才是“向量魔法”的真正意义——它不是什么黑科技而是让你真正理解并掌控虚拟世界运行规律的工具。