Unity Mathf函数避坑指南:Clamp、Round与角度转换实战解析
1. 项目概述那些年我们与Mathf函数“相爱相杀”的日子在Unity开发的日常里Mathf这个静态类就像空气一样无处不在。旋转、移动、缩放、插值、物理计算……几乎每个脚本里都能看到它的身影。它封装了最基础的数学运算初衷是让我们这些开发者能更专注于游戏逻辑而不是去重新发明轮子。然而用久了你会发现Mathf里的一些函数尤其是Clamp和Round系列它们的行为有时会和你预想的“不太一样”甚至可以说是“暗藏玄机”。比如你想用Mathf.Clamp限制一个摄像机的俯仰角在-70度到70度之间结果摄像机一到0度就“啪”地一下弹回-70度玩家体验瞬间崩坏。又或者你用Mathf.Round处理金币数值却发现有时会得到意想不到的舍入结果导致UI显示和实际数值对不上。这些坑我几乎每一个都亲自踩过从早期的项目到现在的生产环境每一次“踩坑”都伴随着深夜的调试和顿悟。今天我就把这些年积累的关于Mathf.Clamp、Mathf.Round以及Deg2Rad的经验、坑点和解决方案系统地梳理出来。无论你是刚接触Unity的新手还是已经有一定经验的开发者相信这篇指南都能帮你避开这些“经典”陷阱写出更健壮、更可靠的代码。我们不仅要会用这些函数更要懂它们背后的逻辑知其然更要知其所以然。2. Mathf.Clamp的“负值”陷阱与欧拉角的爱恨情仇Mathf.Clamp函数本身逻辑极其简单给定一个值value一个最小值min和一个最大值max如果value小于min就返回min大于max就返回max否则返回value本身。看起来人畜无害对吧问题就出在我们常常把它用在了错误的数据上——Unity的欧拉角Euler Angles。2.1 问题重现为什么Clamp会“跳变”让我们直接看一个最经典的错误案例这也是开头引用的Unity社区帖子里的核心问题。开发者想实现一个自由视角相机用A/S/W/D控制旋转并将X轴的旋转角度限制在-70到70度之间。void Update () { // ... 处理按键输入累加transform.parent.transform.eulerAngles ... // 试图直接Clamp欧拉角的x分量 float angleX Mathf.Clamp(transform.parent.localEulerAngles.x, clampXmin, clampXmax); transform.parent.transform.eulerAngles new Vector3 (angleX, transform.parent.eulerAngles.y, transform.parent.eulerAngles.z); }这段代码的意图很清晰每一帧都获取当前欧拉角的x值用Clamp限制它然后再设置回去。但实际运行起来当相机向上旋转试图让x角从负值向0度增长时一旦触及或超过0度视角会瞬间“弹回”到下限-70度。反之向下旋转则可能正常。核心原因在于transform.eulerAngles的返回值范围是[0, 360)而不是[-180, 180]或任意值。当你通过累加eulerAngles这是一个非常不推荐的做法官方文档明确警告不要递增欧拉角来旋转物体时其x值可能从-1度“绕回”到359度。此时你用一个期望范围是[-70, 70]的Clamp去限制一个可能是359的值结果自然是359被限制到最大值70。当你把70度设置回eulerAngles时Unity内部会将其规范化为一个在[0,360)内的等效角度视觉上就产生了从“向上看一点”突然“向下看很多”的剧烈跳变。注意永远不要通过类似eulerAngles new Vector3(...)的方式来递增旋转。Unity官方文档明确指出欧拉角应仅用于读取和设置绝对角度值。递增操作在角度超过360度时会失效。正确的做法是操作一个独立的浮点变量或者使用四元数Quaternion进行旋转叠加。2.2 解决方案分离变量与角度规范化解决这个问题的根本思路是将用于逻辑计算的旋转角度与Transform组件上实际的欧拉角表示分离开来。方案一使用独立的浮点变量记录角度这是最清晰、最推荐的做法。我们维护一个独立的float变量例如rotationX来记录我们逻辑上的旋转角度。在这个变量上进行加减和Clamp操作最后再通过Quaternion.Euler或直接设置eulerAngles应用到物体上。public class FreeLookCamera : MonoBehaviour { public float sensitivity 2f; public float minVerticalAngle -70f; public float maxVerticalAngle 70f; private float currentRotationX 0f; // 逻辑上的X轴旋转角度 void Update() { // 获取输入例如鼠标Y轴 float mouseY Input.GetAxis(Mouse Y) * sensitivity; // 在逻辑变量上累加 currentRotationX - mouseY; // 注意鼠标向上移动相机应向下看所以是减 // 对逻辑变量进行Clamp currentRotationX Mathf.Clamp(currentRotationX, minVerticalAngle, maxVerticalAngle); // 将逻辑角度应用到物体的局部旋转上 // 方法A通过欧拉角设置简单直接 transform.localEulerAngles new Vector3(currentRotationX, transform.localEulerAngles.y, 0); // 方法B通过四元数设置更规范避免万向节锁时出现问题 // transform.localRotation Quaternion.Euler(currentRotationX, transform.localEulerAngles.y, 0); } }这种方法完全避开了欧拉角范围的问题因为currentRotationX始终是你控制范围内的一个普通浮点数。方案二自定义角度规范化函数有些时候你可能不得不从一个现有的欧拉角值比如从其他系统获取开始并需要对其进行限制。这时就需要一个能将任意角度规范到特定范围如[-180,180]然后再进行Clamp的函数。社区里流传着多个版本下面这个是比较通用和健壮的一个public static float ClampAngle(float angle, float min, float max) { // 1. 将角度规范到(-360, 360)区间避免超大值 angle Mathf.Repeat(angle, 360f); // 效果等同于 angle % 360但处理负数更友好 // 2. 将角度转换到[-180, 180]区间这是Clamp最直观的范围 if (angle 180f) angle - 360f; else if (angle -180f) angle 360f; // 实际上Mathf.Repeat后angle不会小于-360这步是防御性编程 // 3. 执行Clamp return Mathf.Clamp(angle, min, max); }使用这个函数你可以安全地对一个欧拉角值进行限制float rawEulerX transform.eulerAngles.x; float clampedX ClampAngle(rawEulerX, -70f, 70f); transform.eulerAngles new Vector3(clampedX, transform.eulerAngles.y, transform.eulerAngles.z);方案三巧用取模运算的“偏置”法在引用的社区回复中用户gonzoOO7提供了一个非常巧妙的思路通过加/减90度将目标范围平移到Clamp函数更容易处理的[0, 180]区间。float teA transform.eulerAngles.x; // 假设要限制在 -80度 到 80度 float minVal -80f; float maxVal 80f; // 核心操作90将范围平移到[10, 170]Clamp后再-90移回来 float clampedX Mathf.Clamp((teA 90) % 360f, minVal 90, maxVal 90) - 90;这个方法很聪明但理解起来需要绕个弯而且要注意取模%操作对负数的处理在C#中-10 % 360结果是-10不是350。为了代码清晰我个人更倾向于使用方案一或方案二。2.3 实操心得何时用欧拉角何时用四元数使用欧拉角的场景当你需要设置一个明确的、人类可读的旋转角度时。例如设置一个门从0度旋转到90度或者设置一个UI元素的固定倾斜角度。此时直接赋值transform.eulerAngles new Vector3(0, 90, 0)是最直观的。使用四元数的场景当需要进行旋转叠加、插值Slerp/Lerp或复杂旋转组合时。例如实现平滑的相机跟随、角色朝向插值、从当前旋转到目标旋转。四元数能完美避免万向节锁并且旋转叠加是数学上正确的。绝对禁止不要读取eulerAngles对其进行数学运算加减乘除然后再写回eulerAngles。因为从四元数到欧拉角的转换不是唯一的同一个旋转可能对应多个欧拉角表示你的运算可能会得到一个非预期的、但等价的欧拉角导致跳变。3. Mathf.Round取整家族的“五朵金花”与精度迷思Mathf提供了五个主要的取整函数Round,Ceil,Floor,CeilToInt,FloorToInt。Round是其中最常用也最容易让人困惑的一个因为它涉及到“银行家舍入法”。3.1 Mathf.Round的“银行家舍入法”在大多数人的数学常识里四舍五入指的是小数点后第一位大于等于5则入小于5则舍。然而C#的Math.Round和Unity的Mathf.Round默认采用的是“四舍六入五成双”的规则也称为“银行家舍入法”。规则解析当舍去位数的第一位小于5时直接舍去。例如1.4 - 1当舍去位数的第一位大于5时进位。例如1.6 - 2当舍去位数的第一位等于5时分两种情况如果5后面还有非零数字则进位。例如1.51 - 2如果5后面没有数字或全是0则看保留的末位数如果末位数是奇数则进位是偶数则舍去。例如1.5 - 2 2.5 - 2 3.5 - 4为什么这么设计在大量统计计算中传统的“四舍五入”规则会带来统计偏差因为“舍”的数字1,2,3,4只有四个而“入”的数字5,6,7,8,9有五个长期累加会导致结果偏大。银行家舍入法让5一半情况舍一半情况入从概率上减少了系统误差。在Unity中的影响假设你有一个金币显示系统数值是float类型计算后是10.5f你希望显示为11。float coins 10.5f; int displayedCoins Mathf.RoundToInt(coins); // 结果是 10如果你不了解这个规则这里就会产生一个Bug玩家明明有10.5个金币UI却显示10个那0.5个金币“不翼而飞”了。3.2 其他取整函数的行为Mathf.Ceil/CeilToInt向上取整。返回大于或等于该浮点数的最小整数。Mathf.Ceil(10.1f)返回11.0fMathf.CeilToInt(10.1f)返回11。常用于计算“需要多少个格子才能装下某物”。Mathf.Floor/FloorToInt向下取整。返回小于或等于该浮点数的最大整数。Mathf.Floor(10.9f)返回10.0f。常用于计算“当前能装满多少个格子”。Mathf.RoundToInt与Mathf.Round逻辑一致但直接返回int类型。Mathf.Truncate截断取整。直接去掉小数部分向零取整。Mathf.Truncate(10.9f)返回10.0fMathf.Truncate(-10.9f)返回-10.0f。注意与Floor的区别对负数Floor(-10.9)是-11。3.3 如何实现传统的“四舍五入”如果你确实需要传统的“四舍五入”行为例如显示评分、处理货币等对用户感知敏感的场景可以自己实现一个函数public static int RoundToIntTraditional(float value) { // 方法一利用MidpointRounding枚举仅适用于C#的Math.RoundUnity的Mathf.Round不支持此参数 // return (int)Math.Round(value, MidpointRounding.AwayFromZero); // 方法二手动实现适用于Mathf return (int)Mathf.Floor(value 0.5f); }注意上面手动实现的方法对于负数需要特别处理因为Mathf.Floor(-1.5 0.5)等于Mathf.Floor(-1.0)等于-1而传统四舍五入-1.5应该得到-2。一个更健壮的版本是public static int RoundToIntTraditional(float value) { if (value 0) return (int)Mathf.Floor(value 0.5f); else return (int)Mathf.Ceil(value - 0.5f); }3.4 浮点数精度问题与取整即使你搞清楚了舍入规则浮点数本身的精度问题也可能带来意外。例如float a 0.1f * 10f; // 理论上等于1.0 float b 0f; for (int i 0; i 10; i) { b 0.1f; } // 累加10次0.1 Debug.Log($a: {a}, RoundToInt(a): {Mathf.RoundToInt(a)}); // 大概率输出 1 Debug.Log($b: {b}, RoundToInt(b): {Mathf.RoundToInt(b)}); // 有可能输出 0 或 1因为b可能略小于1.0由于浮点数表示误差b的实际值可能是0.9999999银行家舍入法会将其舍为0。因此在对浮点数进行取整比较或显示时一个常见的技巧是加上一个极小的偏移量epsilonint roundedValue Mathf.RoundToInt(someFloat Mathf.Epsilon);Mathf.Epsilon是一个非常小的浮点数用于抵消精度误差避免因“几乎等于”但“略小于”临界值而导致的错误舍入。4. 弧度与角度的永恒话题Deg2Rad的万能公式与使用场景在游戏开发中旋转和三角函数无处不在。Unity的Transform组件使用角度Degrees但C#的数学库Mathf.Sin,Mathf.Cos,Mathf.Atan2等以及许多物理公式如向心力、角速度都使用弧度Radians。Mathf.Deg2Rad和Mathf.Rad2Deg就是这两个世界之间的桥梁。4.1 为什么是弧度弧度是一个无量纲的单位定义为弧长与半径的比值。在数学和物理计算中使用弧度能使公式变得非常简洁。例如三角函数sin(θ)的导数cos(θ)这个简洁的关系仅在θ为弧度时才成立。如果你用角度公式里会多出一个π/180的系数非常麻烦。4.2 “万能公式”与常见误区所谓的“万能公式”其实很简单但理解其背后的“为什么”至关重要角度转弧度弧度 角度 * Mathf.Deg2Rad(等价于角度 * π / 180)弧度转角度角度 弧度 * Mathf.Rad2Deg(等价于弧度 * 180 / π)最常见的误区是忘记转换。我见过很多新手代码这样写// 错误Mathf.Sin需要弧度但这里给了角度。 float wave Mathf.Sin(Time.time * 90f); // 本意可能是每秒振荡90度正确的写法应该是// 正确。将角度90度/秒转换为弧度/秒。 float frequencyInRadPerSec 90f * Mathf.Deg2Rad; float wave Mathf.Sin(Time.time * frequencyInRadPerSec);4.3 必须使用Deg2Rad/Rad2Deg的典型场景使用Mathf三角函数时Sin,Cos,Tan,Asin,Acos,Atan,Atan2。它们的输入和输出都是弧度。// 计算一个方向向量与X轴夹角为30度 float angleInDegrees 30f; float angleInRadians angleInDegrees * Mathf.Deg2Rad; Vector2 direction new Vector2(Mathf.Cos(angleInRadians), Mathf.Sin(angleInRadians));使用Mathf.LerpAngle时这个函数非常有用它能在两个角度间进行线性插值并自动处理360度环绕。注意它的参数是角度。float currentAngle 10f; float targetAngle 350f; // 从10度到350度如果直接Lerp会逆时针转340度而不是顺时针转20度。 float smoothedAngle Mathf.LerpAngle(currentAngle, targetAngle, Time.deltaTime * 5f); // 正确会顺时针平滑过渡20度。物理计算涉及角速度时Rigidbody.angularVelocity的单位是弧度/秒。如果你有一个“每秒旋转180度”的概念赋值时需要转换。Rigidbody rb GetComponentRigidbody(); float rotationSpeedInDegPerSec 180f; rb.angularVelocity Vector3.up * rotationSpeedInDegPerSec * Mathf.Deg2Rad; // 正确使用Quaternion.AngleAxis创建旋转时这个函数的第一个参数是角度。// 创建一个绕Y轴旋转90度的四元数 Quaternion rotation Quaternion.AngleAxis(90f, Vector3.up);向量旋转Vector3.RotateTowards这个函数的角度参数也是角度。Vector3 currentDir transform.forward; Vector3 targetDir (player.position - transform.position).normalized; // 每帧最多转向5度 Vector3 newDir Vector3.RotateTowards(currentDir, targetDir, 5f * Mathf.Deg2Rad * Time.deltaTime, 0f); transform.rotation Quaternion.LookRotation(newDir);这里尤其容易混淆Vector3.RotateTowards的第三个参数maxRadiansDelta需要的是弧度但我们在思维上常常用“度”来思考最大转向角。所以公式是maxDegreesPerSecond * Time.deltaTime * Mathf.Deg2Rad。4.4 一个实用的“角度/弧度”思维框架为了避免混淆我建议在代码中遵循以下约定变量命名所有表示角度的浮点数变量如果其单位是角度在名称后加上Deg或InDegrees。如果是弧度则加上Rad或InRadians。float fieldOfViewDeg 60f; float angularSpeedRadPerSec 3.14f; float currentRotationDeg transform.eulerAngles.y;即时转换在调用需要弧度参数的函数时就地转换不要先转换再存储到一个命名不清的变量里。// 清晰 float sineValue Mathf.Sin(angleInDeg * Mathf.Deg2Rad); // 模糊angleInRad这个临时变量意义不明 float angleInRad angleInDeg * Mathf.Deg2Rad; float sineValue Mathf.Sin(angleInRad);常量定义如果你经常使用某些特定角度如90度180度可以定义成已转换的弧度常量。private const float HALF_PI Mathf.PI * 0.5f; // 90度 in Rad private const float PI Mathf.PI; // 180度 in Rad private const float TWO_PI Mathf.PI * 2f; // 360度 in Rad5. 其他Mathf常用函数的“坑”与最佳实践除了上述三大重点Mathf里还有一些函数在使用时需要注意细节。5.1 Mathf.Lerp的“缓动”误解与正确用法Mathf.Lerp(a, b, t)是最常用的线性插值函数。最大的误解在于第三个参数t。很多人以为把它设为Time.deltaTime * speed就能实现平滑移动。// 一个常见的错误用法追求平滑移动 transform.position Mathf.Lerp(transform.position, targetPosition, Time.deltaTime * 5f);这段代码的问题在于Lerp的t是插值比例不是速度。上面的写法会导致移动速度在开始时很快越接近目标越慢而且永远无法真正到达目标因为是指数衰减。Time.deltaTime * 5f这个值通常很小如0.08意味着每一帧只完成剩余距离的8%。正确的平滑移动应该使用Vector3.MoveTowards线性速度或自己计算插值// 方法一使用MoveTowards恒定速度 transform.position Vector3.MoveTowards(transform.position, targetPosition, moveSpeed * Time.deltaTime); // 方法二使用Lerp实现平滑缓入缓出需要记录起始点和时间 private Vector3 startPos; private float journeyLength; private float startTime; public float speed 1.0f; void StartMove(Vector3 target) { startPos transform.position; journeyLength Vector3.Distance(startPos, target); startTime Time.time; } void Update() { if (journeyLength 0) { float distCovered (Time.time - startTime) * speed; float fractionOfJourney distCovered / journeyLength; transform.position Vector3.Lerp(startPos, targetPos, fractionOfJourney); } }Mathf.Lerp最适合的场景是静态插值比如根据一个0到1的进度值如血量比例、加载进度来设置UI填充、颜色或位置。5.2 Mathf.Clamp01的便利性Mathf.Clamp01(value)等价于Mathf.Clamp(value, 0f, 1f)。它非常常用尤其是在处理比例、透明度、规范化参数时。直接使用Clamp01能让代码意图更清晰。5.3 Mathf.Approximately与浮点数相等比较永远不要用直接比较两个浮点数是否相等因为精度误差可能导致两个逻辑上应该相等的数并不严格相等。float a 0.1f * 10f; float b 1.0f; if (a b) // 这可能返回false { // ... }应该使用Mathf.Approximately(a, b)。它会在一个极小的误差范围内Mathf.Epsilon比较两个浮点数对于游戏中的大多数比较来说都是安全的。if (Mathf.Approximately(a, b)) { // 可以认为相等 }对于比较是否接近一个特定值比如判断旋转是否完成更好的做法是if (Mathf.Abs(currentAngle - targetAngle) 0.1f) // 允许0.1度的误差 { // 认为已经到达目标角度 }6. 综合案例构建一个健壮的自由视角相机让我们把上面所有的知识点融会贯通写一个真正健壮、无坑的自由视角相机脚本。这个相机将支持鼠标控制视角旋转并有上下角度限制。using UnityEngine; public class RobustFreeLookCamera : MonoBehaviour { [Header(鼠标灵敏度)] public float sensitivityX 2.0f; public float sensitivityY 2.0f; [Header(垂直视角限制)] public float minimumY -60.0f; // 最低能看多低度 public float maximumY 60.0f; // 最高能看多高度 [Header(平滑阻尼)] public float smoothTime 0.1f; // 旋转平滑时间 // 用于记录逻辑旋转角度的变量与Transform分离 private float rotationX 0.0f; private float rotationY 0.0f; // 用于平滑阻尼的当前速度 private float currentRotationXVelocity 0.0f; private float currentRotationYVelocity 0.0f; // 平滑后的目标角度 private float smoothedRotationX 0.0f; private float smoothedRotationY 0.0f; void Start() { // 初始化将相机当前的欧拉角转换为我们逻辑上的旋转角度。 // 注意eulerAngles范围是[0,360)我们需要将其映射到[-180,180]以便于计算。 Vector3 currentEuler transform.localEulerAngles; rotationY NormalizeAngle(currentEuler.y); // 水平旋转 rotationX NormalizeAngle(currentEuler.x); // 垂直旋转 smoothedRotationX rotationX; smoothedRotationY rotationY; // 可选锁定并隐藏鼠标指针 Cursor.lockState CursorLockMode.Locked; Cursor.visible false; } void Update() { // 1. 获取鼠标输入 float mouseX Input.GetAxis(Mouse X); float mouseY Input.GetAxis(Mouse Y); // 2. 根据灵敏度计算角度增量注意鼠标Y轴移动影响X轴旋转即俯仰 rotationY mouseX * sensitivityX; rotationX - mouseY * sensitivityY; // 减号是因为鼠标向上移动相机应该向下看 // 3. 使用Mathf.Clamp限制垂直视角。这里直接Clamp逻辑变量安全无副作用。 rotationX Mathf.Clamp(rotationX, minimumY, maximumY); // 4. 可选应用平滑阻尼让相机运动更柔和 smoothedRotationX Mathf.SmoothDampAngle(smoothedRotationX, rotationX, ref currentRotationXVelocity, smoothTime); smoothedRotationY Mathf.SmoothDampAngle(smoothedRotationY, rotationY, ref currentRotationYVelocity, smoothTime); // 注意SmoothDampAngle会自动处理360度环绕非常适用于角度平滑。 // 5. 将逻辑角度应用为相机的局部旋转。使用四元数创建旋转。 transform.localRotation Quaternion.Euler(smoothedRotationX, smoothedRotationY, 0); } // 辅助函数将[0, 360)的角度规范到[-180, 180]区间 private float NormalizeAngle(float angle) { // 使用取余操作将角度规范到[0, 360) angle % 360f; // 将大于180度的角度转换到负值区间 if (angle 180f) angle - 360f; // 将小于-180度的角度转换到正值区间防御性编程通常不会发生 else if (angle -180f) angle 360f; return angle; } // 提供一个外部调用的重置视角方法例如按某个键 public void ResetView() { rotationX 0f; rotationY 0f; // 平滑变量也重置避免从当前角度平滑回0 smoothedRotationX 0f; smoothedRotationY 0f; currentRotationXVelocity 0f; currentRotationYVelocity 0f; } }这个脚本的避坑要点总结分离逻辑与显示使用独立的rotationX和rotationY变量存储角度完全规避了直接操作eulerAngles带来的范围问题。正确使用Clamp在逻辑变量上直接使用Mathf.Clamp简单安全。使用Quaternion.Euler通过四元数设置最终旋转这是Unity推荐的方式。使用Mathf.SmoothDampAngle进行平滑这个函数专为角度设计自动处理360度环绕比手动Lerp更省心。提供了角度规范化函数NormalizeAngle函数在初始化时很有用可以将任意欧拉角转换到我们逻辑计算使用的[-180,180]范围。7. 常见问题排查速查表在实际开发中遇到Mathf相关的问题可以按这个思路快速排查问题现象可能原因解决方案相机旋转到某个角度突然跳变直接对transform.eulerAngles进行Mathf.Clamp且角度值发生了360度环绕。使用独立的浮点变量记录旋转角度在该变量上进行Clamp最后用Quaternion.Euler或设置eulerAngles应用。Mathf.Round结果不符合“四舍五入”预期使用了默认的银行家舍入法。如需传统四舍五入使用自定义函数(int)Mathf.Floor(value 0.5f)仅正数或完整版。物体旋转速度时快时慢永远到不了终点错误地将Mathf.Lerp的插值因子t当作速度使用导致指数衰减。对于匀速运动使用Vector3.MoveTowards。对于平滑缓动计算基于时间的路程比例fractionOfJourney distCovered / journeyLength再作为t。浮点数比较失效浮点数存在精度误差两个理论上相等的数二进制表示可能略有不同。使用Mathf.Approximately(a, b)或判断绝对值差Mathf.Abs(a-b) epsilon。使用Mathf.Sin/Cos得到奇怪结果向三角函数传入的参数是角度而不是弧度。记住Mathf三角函数要弧度传入前乘以Mathf.Deg2Rad。Vector3.RotateTowards旋转速度不对误将角度值直接作为maxRadiansDelta参数传入。maxRadiansDelta需要弧度。公式应为maxDegreesPerSecond * Time.deltaTime * Mathf.Deg2Rad。Rigidbody.angularVelocity旋转飞快或不动赋值时单位是角度/秒但angularVelocity需要弧度/秒。赋值时乘以Mathf.Deg2Rad。例如rb.angularVelocity Vector3.up * 180f * Mathf.Deg2Rad;。最后关于Deg2Rad和Rad2Deg我的个人习惯是在代码中任何看到角度数字的地方都条件反射地问自己一句“这个地方需要弧度吗” 尤其是在调用Mathf的三角函数、Vector3.RotateTowards以及设置物理角速度时这个检查步骤能避免一大半因单位混淆导致的Bug。把这些细节处理好你的Unity代码在数学计算上的稳定性和可读性都会提升一个档次。