with one click
game-math
写移动/碰撞/相机等游戏逻辑时使用。向量、插值、帧率无关。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
写移动/碰撞/相机等游戏逻辑时使用。向量、插值、帧率无关。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
做 Cocos Creator 多机型/多分辨率适配时使用。Canvas、Widget、安全区。
Cocos Creator 用 AssetBundle 做分包/远程资源时使用。加载、释放、依赖、缓存。
优化 Cocos Creator 渲染性能时使用。合批、图集、动静分离、Label。
给 Cocos Creator 原生包做热更新时使用。version manifest、增量、校验、回滚。
写 Cocos Creator 动效/动画时使用。tween、Animation、Spine、性能与清理。
做 Cocos Creator 大量条目列表时使用。虚拟列表、节点复用。
| name | game-math |
| description | 写移动/碰撞/相机等游戏逻辑时使用。向量、插值、帧率无关。 |
| category | gamedev |
| tags | ["数学","向量","插值"] |
规则: 角色位移、速度积分、冷却计时等所有随时间变化的量,必须乘以当前帧的 deltaTime;物理模拟(刚体、碰撞)必须在固定步长的 FixedUpdate(或等效接口)中执行,不能放在可变帧率的 Update 里。
为什么: 最经典的错误:transform.position += speed * Vector3.forward——忘了乘 deltaTime。60fps 时移动速度正常,玩家开了高刷屏(144fps)变成 2.4 倍速,开了垂直同步卡到 30fps 就像踩了泥。更隐蔽的版本:冷却计数器 cooldown -= 1(每帧减 1),在 60fps 下 1 秒冷却感觉正确,在 120fps 下只有 0.5 秒——平衡性被帧率破坏。物理放在 Update 里的后果:帧率波动导致碰撞检测不稳定,高速物体(子弹)会穿过薄墙(Tunneling)。
怎么做:
// Unity 示例
void Update() {
// ✅ 位移乘 deltaTime,帧率无关
transform.position += velocity * Time.deltaTime;
// ✅ 冷却计时乘 deltaTime
if (cooldown > 0) cooldown -= Time.deltaTime;
// ❌ 不乘 deltaTime,帧率越高移动越快
// transform.position += velocity;
}
void FixedUpdate() {
// ✅ 物理/碰撞在固定步长中处理
rb.AddForce(jumpForce * Vector3.up);
}
dt(如 1/60s),渲染循环用实际帧间隔;物理步长和渲染步长解耦。规则: 向量运算(归一化、点积、叉积、投影)全部使用引擎提供的 Vector API,禁止自己手写 sqrt 计算距离再归一化;坐标空间转换(世界 ↔ 本地 ↔ 屏幕)必须明确,不能混用。
为什么: 手写归一化的经典 bug:float len = sqrt(x*x + y*y); dir = (x/len, y/len)——当向量长度接近零时除以近零值,结果爆成 NaN 或 Infinity,角色瞬间飞到无穷远处。坐标空间混用更隐蔽:把角色的 transform.forward(世界坐标方向)直接当本地坐标方向使用,角色一旋转就方向全乱。
怎么做:
// 反例 — 手写归一化,零向量时 NaN
float len = Mathf.Sqrt(dir.x * dir.x + dir.y * dir.y);
Vector2 normalized = new Vector2(dir.x / len, dir.y / len); // ❌ len=0 时 NaN
// 正例 — 使用引擎 API,内置零向量保护
Vector2 normalized = dir.normalized; // ✅ 零向量时返回 Vector2.zero
// 点积判断是否在前方
float dot = Vector3.Dot(transform.forward, toTarget.normalized);
bool isInFront = dot > 0f; // ✅ 点积 > 0 表示在正前方半球
// 叉积判断左右
Vector3 cross = Vector3.Cross(transform.forward, toTarget);
bool isOnRight = cross.y > 0f; // ✅ 叉积 y 分量判断左右(世界上轴为 Y)
// 坐标空间转换要显式
Vector3 worldDir = transform.TransformDirection(localDir); // 本地 → 世界
Vector3 localDir = transform.InverseTransformDirection(worldDir); // 世界 → 本地
规则: 相机跟随、角色朝向平滑、UI 弹窗动画等需要平滑过渡的场景,必须用 Lerp、Slerp 或 SmoothDamp 实现,禁止写直接赋值的"瞬移";同时要注意帧率无关写法,Lerp(a, b, t * dt) 和 Lerp(a, b, 0.1f) 的帧率行为完全不同。
为什么: 最常见的手感问题:相机直接 transform.position = targetPos,玩家转头时相机像被人抽了一巴掌。用了 Lerp 但写成 Lerp(current, target, 0.1f) 每帧固定插值,在 60fps 和 30fps 下平滑速度不一样——30fps 时相机变慢,60fps 时变快,高刷屏玩家和低刷屏玩家手感不同。SmoothDamp 是跟随类场景的最佳选择,内置帧率无关和速度限制。
怎么做:
// 相机跟随 — 推荐 SmoothDamp
Vector3 camVelocity = Vector3.zero; // 必须是成员变量,SmoothDamp 内部维护
void LateUpdate() {
// ✅ 帧率无关,有最大速度限制,手感最好
transform.position = Vector3.SmoothDamp(
transform.position,
target.position + offset,
ref camVelocity,
smoothTime: 0.15f
);
}
// 旋转平滑 — 用 Slerp 而非 Lerp(Lerp 对四元数会走直线不走球面)
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
rotSpeed * Time.deltaTime // ✅ 乘 deltaTime 帧率无关
);
// 帧率无关的指数平滑(比 Lerp(a,b,0.1f) 更正确)
float alpha = 1f - Mathf.Pow(1f - 0.1f, Time.deltaTime * 60f);
value = Mathf.Lerp(value, target, alpha);
规则: 需要碰撞检测时优先使用引擎的 Raycast、OverlapSphere、BoxCast 等 API;确需自写几何检测时(如服务端无物理引擎),必须处理浮点精度(加 epsilon)和边界情况(向量长度为零、完全重合的两点)。
为什么: 常见 bug:自写的"子弹是否打中圆形敌人"检测用 distance < radius,在距离恰好等于 radius 时因为浮点误差随机成功或失败,导致边缘命中不稳定。更严重的:自写 AABB 碰撞没有处理「一帧内高速穿透」(Tunneling),子弹速度一快就穿墙——引擎的 SphereCast 把整个运动轨迹做扫描,天然解决这个问题。
怎么做:
// 高速子弹用 SphereCast / 扫描射线,而非 OverlapSphere(点采样)
float castRadius = 0.1f;
if (Physics.SphereCast(
origin: prevPos,
radius: castRadius,
direction: (currPos - prevPos).normalized,
out RaycastHit hit,
maxDistance: Vector3.Distance(prevPos, currPos),
layerMask: enemyLayer))
{
OnBulletHit(hit); // ✅ 扫描整段轨迹,不会穿墙
}
// 自写 2D 圆形碰撞(带 epsilon 防边界抖动)
const float EPSILON = 0.001f;
bool CircleOverlap(Vector2 a, float ra, Vector2 b, float rb) {
float distSq = (a - b).sqrMagnitude; // sqrMagnitude 比 magnitude 快
float radiusSum = ra + rb + EPSILON;
return distSq < radiusSum * radiusSum; // ✅ 避免开方,加 epsilon 处理边界
}
规则: 3D 旋转全程使用四元数(Quaternion)而非欧拉角(Euler),只在面向用户的 Inspector 或配置文件中用欧拉角显示;任何角度累加逻辑执行后必须归一化,防止浮点误差积累导致旋转矩阵退化。
为什么: 万向锁(Gimbal Lock)是欧拉角在特定姿态下损失一个自由度的现象,飞行游戏和第一人称相机都会碰到:俯仰角到达 ±90° 时,偏航和横滚轴重合,旋转操作失效,相机"锁死"抖动。用四元数就不会有这个问题。另一个常见 bug:每帧累加一个小角度 euler.y += 0.1f,几千帧后浮点误差积累,Quaternion.Euler(euler) 产生的旋转矩阵不再是单位正交矩阵,物体开始拉伸变形。
怎么做:
// 反例 — 欧拉角累加,万向锁风险 + 误差积累
transform.eulerAngles += new Vector3(pitch, yaw, 0f); // ❌
// 正例 — 四元数乘法表示增量旋转
Quaternion deltaRot = Quaternion.Euler(pitch * Time.deltaTime, yaw * Time.deltaTime, 0f);
transform.rotation = transform.rotation * deltaRot;
transform.rotation = Quaternion.Normalize(transform.rotation); // ✅ 定期归一化防误差积累
// 平滑看向目标
Quaternion lookRot = Quaternion.LookRotation(toTarget, Vector3.up);
transform.rotation = Quaternion.RotateTowards(
transform.rotation,
lookRot,
maxDegreesPerSecond * Time.deltaTime // ✅ 帧率无关的最大旋转速度
);
// 只读欧拉角、不写欧拉角
float currentYaw = transform.eulerAngles.y; // 读取用于 UI 显示 OK
// transform.eulerAngles = new Vector3(x, y, z); // ❌ 避免直接写
// 反例 — 不乘 deltaTime,帧率越高越快
public class PlayerMove : MonoBehaviour {
float speed = 5f;
void Update() {
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
transform.position += new Vector3(h, 0, v) * speed; // ❌ 60fps 正常,144fps 快 2.4 倍
}
}
// 正例 — 乘 deltaTime,任何帧率下速度一致
public class PlayerMove : MonoBehaviour {
float speed = 5f;
void Update() {
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
transform.position += new Vector3(h, 0, v) * speed * Time.deltaTime; // ✅
}
}
// 反例 — 欧拉角累加,俯仰到 90° 时偏航失效
float pitch, yaw;
void Update() {
pitch += Input.GetAxis("Mouse Y") * sensitivity;
yaw += Input.GetAxis("Mouse X") * sensitivity;
transform.eulerAngles = new Vector3(-pitch, yaw, 0f); // ❌ pitch=90 时万向锁
}
// 正例 — 四元数分轴旋转,clamp 俯仰角,无万向锁
float pitch, yaw;
void Update() {
pitch = Mathf.Clamp(pitch - Input.GetAxis("Mouse Y") * sensitivity, -89f, 89f);
yaw += Input.GetAxis("Mouse X") * sensitivity;
Quaternion pitchRot = Quaternion.AngleAxis(pitch, Vector3.right);
Quaternion yawRot = Quaternion.AngleAxis(yaw, Vector3.up);
transform.rotation = yawRot * pitchRot; // ✅ 四元数组合,无万向锁
}
deltaTime,没有裸 += speed 或 += 1。FixedUpdate(或固定步长循环)中,不在可变帧率的 Update 里。.normalized),没有手写 sqrt 且未处理零向量情况。SmoothDamp 或帧率无关的 Lerp 写法,而非固定系数 Lerp(a,b,0.1f)。