游戏开发实战:用Unity3D实现2D坐标系旋转与平移(附完整代码)
游戏开发实战用Unity3D实现2D坐标系旋转与平移附完整代码在2D游戏开发中角色和物体的移动、旋转是最基础也最频繁使用的功能。很多开发者会直接使用Unity提供的Transform组件但当遇到需要精确控制坐标变换时——比如制作自定义的物理系统、实现特殊镜头效果或开发编辑器工具——理解底层数学原理就变得至关重要。本文将带你从游戏开发视角重新审视坐标系变换用C#脚本实现可复用的坐标转换模块并解决实际开发中常见的精灵旋转锚点偏移问题。1. 理解游戏中的坐标系系统1.1 世界坐标与局部坐标在Unity的2D场景中每个游戏对象都有两种坐标表示世界坐标系(World Space)整个场景的绝对坐标系原点(0,0)通常是场景中心局部坐标系(Local Space)相对于父对象或自身锚点的相对坐标系当我们需要让一个角色以自身为基准向前移动无论当前朝向如何或者让UI元素跟随相机但保持相对位置时就需要在这两种坐标系间转换。1.2 变换矩阵的本质Unity底层使用4x4矩阵表示变换但对于2D我们只需要关注3x3部分| cosθ -sinθ dx | | sinθ cosθ dy | | 0 0 1 |这个矩阵同时包含了旋转和平移信息。理解这一点很重要因为矩阵乘法不满足交换律——先旋转后平移 ≠ 先平移后旋转我们可以通过矩阵分解单独获取旋转角度或位移量2. 实现基础坐标变换2.1 纯旋转变换当我们需要让一个物体围绕某点旋转时比如行星绕太阳可以使用以下C#方法// 将point绕pivot旋转angle度逆时针 Vector2 RotateAround(Vector2 point, Vector2 pivot, float angle) { float rad angle * Mathf.Deg2Rad; float cos Mathf.Cos(rad); float sin Mathf.Sin(rad); Vector2 dir point - pivot; return new Vector2( dir.x * cos - dir.y * sin pivot.x, dir.x * sin dir.y * cos pivot.y ); }注意Unity的Mathf三角函数使用弧度制而Inspector中角度显示为度数需要转换2.2 纯平移变换平移相对简单但要注意坐标系方向// 世界坐标→局部坐标 Vector2 WorldToLocal(Vector2 worldPos, Transform localSpace) { return localSpace.InverseTransformPoint(worldPos); } // 局部坐标→世界坐标 Vector2 LocalToWorld(Vector2 localPos, Transform localSpace) { return localSpace.TransformPoint(localPos); }3. 复合变换实战应用3.1 实现一个跟随鼠标的炮台假设我们需要制作一个炮台底座固定在世界坐标(2,3)位置炮管应该始终指向鼠标位置开火时子弹需要从炮口沿正确方向射出public class Turret : MonoBehaviour { public Transform baseTransform; public Transform barrelPivot; public float barrelLength 1.5f; void Update() { // 获取鼠标世界坐标 Vector2 mouseWorldPos Camera.main.ScreenToWorldPoint(Input.mousePosition); // 计算炮管朝向角度 Vector2 dir mouseWorldPos - (Vector2)baseTransform.position; float angle Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; // 设置旋转仅Z轴 barrelPivot.rotation Quaternion.Euler(0, 0, angle); // 计算炮口位置 Vector2 barrelEnd baseTransform.position barrelPivot.right * barrelLength; // 可视化调试 Debug.DrawLine(baseTransform.position, barrelEnd, Color.red); if (Input.GetMouseButtonDown(0)) { Fire(barrelEnd, dir.normalized); } } void Fire(Vector2 spawnPos, Vector2 direction) { // 实例化子弹并设置初速度 // ... } }3.2 解决Sprite旋转时的锚点问题Unity的Sprite默认锚点是中心点这会导致旋转时出现位置偏移。我们可以通过以下方式修正修改Pivot在图片导入设置中调整Pivot或使用Sprite Editor自定义锚点代码动态调整public class FixedPivotRotator : MonoBehaviour { public Vector2 customPivot new Vector2(0.5f, 0.5f); // 标准化坐标 private SpriteRenderer spriteRenderer; private Vector3 originalPosition; void Awake() { spriteRenderer GetComponentSpriteRenderer(); originalPosition transform.position; // 计算实际偏移量 Bounds bounds spriteRenderer.bounds; Vector3 pivotOffset new Vector3( bounds.size.x * (customPivot.x - 0.5f), bounds.size.y * (customPivot.y - 0.5f), 0 ); transform.position pivotOffset; } void Update() { // 旋转逻辑... // 旋转时会以customPivot为轴心 } }4. 高级应用制作自定义2D物理4.1 实现一个非刚体绳索通过坐标变换可以创建逼真的绳索物理public class RopeSegment : MonoBehaviour { public Transform connectedTo; public float segmentLength 0.5f; void Update() { if (connectedTo null) return; // 保持与连接对象的固定距离 Vector2 targetDir (connectedTo.position - transform.position).normalized; Vector2 targetPos (Vector2)connectedTo.position - targetDir * segmentLength; transform.position targetPos; // 计算旋转使线段指向连接对象 float angle Mathf.Atan2(targetDir.y, targetDir.x) * Mathf.Rad2Deg; transform.rotation Quaternion.Euler(0, 0, angle); } }4.2 2D镜头跟随的高级控制实现一个平滑的镜头跟随同时限制在边界内public class AdvancedCameraFollow : MonoBehaviour { public Transform target; public Vector2 minBounds, maxBounds; public float smoothTime 0.3f; private Vector3 velocity Vector3.zero; void LateUpdate() { if (target null) return; // 计算目标位置保持z轴不变 Vector3 targetPos new Vector3( Mathf.Clamp(target.position.x, minBounds.x, maxBounds.x), Mathf.Clamp(target.position.y, minBounds.y, maxBounds.y), transform.position.z ); // 平滑过渡 transform.position Vector3.SmoothDamp( transform.position, targetPos, ref velocity, smoothTime ); } void OnDrawGizmos() { // 绘制相机边界 Gizmos.color Color.green; Vector3 center (minBounds maxBounds) / 2f; Vector3 size maxBounds - minBounds; Gizmos.DrawWireCube(center, size); } }5. 性能优化与最佳实践5.1 避免每帧计算三角函数对于频繁调用的旋转逻辑可以缓存计算结果// 优化前的写法每帧计算三角函数 void Update() { transform.rotation Quaternion.Euler(0, 0, Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg); } // 优化后的写法 private float lastAngle; private Quaternion targetRotation; void Update() { float newAngle Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; if (!Mathf.Approximately(newAngle, lastAngle)) { lastAngle newAngle; targetRotation Quaternion.Euler(0, 0, newAngle); } transform.rotation targetRotation; }5.2 使用Unity原生方法对比操作类型自定义实现Unity原生方法适用场景世界→局部坐标手动矩阵计算Transform.InverseTransformPoint推荐使用原生方法局部→世界坐标手动矩阵计算Transform.TransformPoint推荐使用原生方法绕点旋转需要自定义实现无直接对应需要精确控制时使用方向计算Mathf.Atan2Transform.right/up等根据是否需要世界空间选择5.3 常见问题排查问题1旋转后对象位置偏移检查锚点设置确认父对象的缩放是否为(1,1,1)检查是否有其他脚本在修改位置问题2坐标转换结果不正确确认传入的是世界坐标还是局部坐标检查矩阵乘法顺序是否正确使用Debug.DrawLine可视化坐标轴问题3角度计算出现翻转确保所有角度使用统一单位度或弧度检查Atan2的参数顺序(y,x)考虑使用Quaternion.LookRotation处理3D情况