This commit is contained in:
2026-10-14 15:20:37 +08:00
parent 383abd67bc
commit 4fd5a3bd45
3 changed files with 1347 additions and 56 deletions
File diff suppressed because it is too large Load Diff
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem; // Pointer.current:鼠标拖动 / 屏幕滑动统一采样
/// <summary>战神跟手旋转的旋转轴(3D 模型按轴转身,各战神模型朝向不同,面板可选)</summary>
/// <summary>战神跟手旋转的旋转轴(世界坐标 X/Y/Z,绕世界轴转不受父节点朝向影响)</summary>
public enum ZhanShenRotateAxis
{
X = 0,
@@ -33,9 +33,9 @@ public class ZhanShenTurretData : TurretData
[Tooltip("战神普通开火音效(未锁定时):不锁定开炮时逐发播放;为空则回退普通炮台音效 bulletClip")]
public AudioClip bulletAudio;
[Tooltip("旋转目标对象:跟手旋转时被旋转的节点(只转它 rotateAxis 那一个轴,其他轴和位移都不动)。为空回退战神实例自身")]
[Tooltip("旋转目标对象:跟手旋转时被旋转的节点(按世界轴旋转,其它轴和位移都不动)。为空回退战神实例自身")]
public Transform rotateTargetNode;
[Tooltip("跟手旋转绕哪个轴:3D 模型转身一般是 Y,部分模型/特效用 X 或 Z")]
[Tooltip("跟手旋转绕哪个【世界坐标】轴:Y=竖直轴转身(默认),X/Z 同理。用世界轴不受父节点朝向和本地姿态影响")]
public ZhanShenRotateAxis rotateAxis = ZhanShenRotateAxis.Y;
[Tooltip("跟手灵敏度:指针每滑动 1 像素累积多少度旋转。正数=向右划模型向右转、向左划向左转;想反方向就填负数")]
public float rotateSensitivity = 0.4f;
@@ -49,17 +49,17 @@ public class ZhanShenTurretData : TurretData
/// <summary>是否正在跟手旋转(由 Timeline 事件 RotateZhanShen 开始、StopRotateZhanShen 结束)</summary>
private bool _rotateFollowing;
private Coroutine _rotateRoutine;
/// <summary>累积的目标角度(原始值,可超过 360°,保证连续拖动不绕圈回跳)</summary>
/// <summary>累积的目标角度(增量,可超过 360°,保证连续拖动不绕圈回跳)</summary>
private float _rotateTargetAngle;
/// <summary>平滑显示用的角度(原始值,与 _rotateTargetAngle 同坐标系)</summary>
/// <summary>平滑显示用的角度(相对基准的增量,与 _rotateTargetAngle 同坐标系)</summary>
private float _rotateShownAngle;
/// <summary>上一帧指针位置:按帧间位移量累加角度,所以按下时也要先把位置记准</summary>
private Vector2 _rotateLastPointerPos;
/// <summary>已经真正叠加到节点上的角度:每帧只补"还差多少",不直接覆盖节点的旋转</summary>
private float _rotateAppliedAngle;
/// <summary>
/// ⭐ 旋转战神(Timeline 事件调用="开始点"):开始持续跟手旋转。
/// 调用后每帧按指针(鼠标点击拖动 / 屏幕滑动)的横向位移量累加到目标角度,
/// 节点再按 rotateLerpSpeed 平滑绕 rotateAxis 转到该角度;累加从节点当前角度起步,不会跳变。
/// 节点再按 rotateLerpSpeed 平滑绕【世界坐标】rotateAxis 轴旋转到该角度。
/// 重复触发(Timeline 上多个事件)不会叠加协程。
/// </summary>
public void RotateZhanShen()
@@ -73,9 +73,10 @@ public class ZhanShenTurretData : TurretData
if (node == null) return;
_rotateFollowing = true;
_rotateTargetAngle = ReadAxisAngle(node.localEulerAngles); // 从当前角度开始累加
_rotateShownAngle = _rotateTargetAngle;
_rotateLastPointerPos = ReadPointerPos();
// 增量从 0 起算:不缓存任何"基准朝向/基准位置",每帧只在节点当前旋转上补差值 → 起步不跳、松手不回弹
_rotateTargetAngle = 0f;
_rotateShownAngle = 0f;
_rotateAppliedAngle = 0f;
_rotateRoutine = StartCoroutine(FollowRotateRoutine());
}
@@ -99,44 +100,82 @@ public class ZhanShenTurretData : TurretData
var node = ResolveRotateTargetNode();
if (node == null) break;
// ⭐ 只累加"本帧滑动距离",不记录、不比较任何指针位置:
// Pointer.delta 是输入系统给的本帧位移量(鼠标/触摸通用),按下就按位移累加,
// 松手 delta 自然为 0。没有"初始位置"这种东西,所以点一下(没滑动)一定不会转。
var pointer = Pointer.current;
bool pressed = false;
float deltaX = 0f;
if (pointer != null)
{
Vector2 cur = pointer.position.ReadValue();
// 按住(拖动中)才累加:松手保持当前角度
// ⚠ 这里用 -= 而不是 +=:Unity 绕 Y 轴的正角度在屏幕上看是"向左转"(相机朝 -Z 看时
pressed = pointer.press.isPressed;
deltaX = pointer.delta.ReadValue().x;
// ⚠ 这里用 -= 而不是 +=:Unity 绕 +Y 的正角度在屏幕上看是"向左转"(相机朝 -Z 看时
// +θ 把模型的正面转向 -X),所以"向右划=向右转"必须把右移量反号。改回 += 方向就会反。
if (pointer.press.isPressed)
_rotateTargetAngle -= (cur.x - _rotateLastPointerPos.x) * rotateSensitivity;
// ⚠ 不管有没有按住都记录位置:否则按下那一帧会用"开始跟手时的旧位置"算出巨大跳变
_rotateLastPointerPos = cur;
if (pressed)
_rotateTargetAngle -= deltaX * rotateSensitivity;
}
_rotateShownAngle = rotateLerpSpeed > 0f
? Mathf.Lerp(_rotateShownAngle, _rotateTargetAngle, Mathf.Clamp01(rotateLerpSpeed * Time.deltaTime))
: _rotateTargetAngle;
// 只改 rotateAxis 那一个轴:其余两轴沿用节点当前值(模型只绕该轴转身)
Vector3 e = node.localEulerAngles;
switch (rotateAxis)
// ⭐ 绕【世界坐标】轴旋转,但**增量式叠加**,绝不覆盖节点的朝向:
// ① 世界轴先换算到节点所在父级坐标系(axisLocal);父级跟着瞄准转时,这跟轴也跟着转,
// 于是"绕世界竖直轴"在任何父级朝向下都成立;
// ② 把差值(本帧该补多少度)左乘在当前 localRotation 上 —— 节点本来什么姿态就叠在什么上面,
// 不缓存基准、不锁世界朝向,所以:点一下不滑动 = 差值 0 = 完全不动;
// 父级/动画改写了该节点的旋转也不会被顶回去(这就是上一版"点一下就转了个圈"的原因:
// 上一版直接写 node.rotation = AngleAxis(角度) × 缓存基准,把世界朝向锁死了)。
float step = _rotateShownAngle - _rotateAppliedAngle;
if (Mathf.Abs(step) > 1e-4f)
{
case ZhanShenRotateAxis.X: e.x = _rotateShownAngle; break;
case ZhanShenRotateAxis.Z: e.z = _rotateShownAngle; break;
default: e.y = _rotateShownAngle; break;
Vector3 worldAxis = GetWorldAxis();
Vector3 axisLocal = node.parent != null
? node.parent.InverseTransformDirection(worldAxis)
: worldAxis;
node.localRotation = Quaternion.AngleAxis(step, axisLocal) * node.localRotation;
_rotateAppliedAngle = _rotateShownAngle;
}
node.localEulerAngles = e;
#if UNITY_EDITOR
DebugRotateStep(node, pressed, deltaX, step);
#endif
}
_rotateRoutine = null;
}
/// <summary>取当前旋转轴上的角度(跟手累加的起点)</summary>
private float ReadAxisAngle(Vector3 euler)
#if UNITY_EDITOR
/// <summary>跟手旋转诊断计时(编辑器排查用,0.5 秒一条 + 大跳变立即报)</summary>
private float _rotateLogTimer = -99f;
/// <summary>
/// 编辑器诊断:用它定死"转一圈"到底是不是本脚本干的。
/// 判读方法:
/// - 只点击没拖动时如果日志里 步进≈0、累计≈0,模型却自己转 → 不是这套代码,是 Timeline/Animator(或别的系统)在转;
/// - 如果日志出现很大的"步进"(几十~几百度)且时间点正好对齐你看到的转圈 → 才是这里算错了。
/// </summary>
private void DebugRotateStep(Transform node, bool pressed, float deltaX, float step)
{
_rotateLogTimer += Time.deltaTime;
bool bigJump = Mathf.Abs(step) > 5f;
if (_rotateLogTimer < 0.5f && !bigJump) return;
_rotateLogTimer = 0f;
Debug.Log($"[战神跟手旋转] 节点={node.name} 按住={pressed} 本帧滑动={deltaX:F1}px " +
$"本帧步进={step:F2}° 累计={_rotateShownAngle:F2}° 已叠加={_rotateAppliedAngle:F2}° " +
$"节点本地Euler={node.localEulerAngles} 节点世界Euler={node.eulerAngles}");
}
#endif
/// <summary>面板选的轴转成世界坐标方向向量</summary>
private Vector3 GetWorldAxis()
{
switch (rotateAxis)
{
case ZhanShenRotateAxis.X: return euler.x;
case ZhanShenRotateAxis.Z: return euler.z;
default: return euler.y;
case ZhanShenRotateAxis.X: return Vector3.right;
case ZhanShenRotateAxis.Z: return Vector3.forward;
default: return Vector3.up;
}
}
@@ -145,12 +184,6 @@ public class ZhanShenTurretData : TurretData
return rotateTargetNode != null ? rotateTargetNode : transform;
}
private static Vector2 ReadPointerPos()
{
var pointer = Pointer.current;
return pointer != null ? pointer.position.ReadValue() : Vector2.zero;
}
private void OnDisable()
{
// 战神实例被回收/隐藏时停止跟手,避免协程挂在已停用的对象上继续跑