624 lines
17 KiB
C#
624 lines
17 KiB
C#
using DG.Tweening;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 模型拆装动画控制器
|
||
/// 功能:实现模型部件从中心点向外平移远离的拆装动画
|
||
/// 特点:使用DOTween实现平滑动画,部件从中心点向外平移,不旋转
|
||
/// </summary>
|
||
public class ModelSpreadController : MonoBehaviour
|
||
{
|
||
/// <summary>
|
||
/// 部件信息结构
|
||
/// </summary>
|
||
[System.Serializable]
|
||
public class PartInfo
|
||
{
|
||
public Transform partTransform; // 部件变换组件
|
||
[HideInInspector] public Vector3 assembledWorldPosition; // 组装时的世界位置
|
||
[HideInInspector] public Quaternion assembledWorldRotation; // 组装时的世界旋转
|
||
[HideInInspector] public Vector3 detachedWorldPosition; // 拆开后的世界位置
|
||
[HideInInspector] public Quaternion detachedWorldRotation; // 拆开后的世界旋转
|
||
}
|
||
|
||
[Header("动画参数")]
|
||
[SerializeField] private float animationDuration = 1.0f; // 动画总时长
|
||
[SerializeField] private float spreadDistance = 3.0f; // 散开距离
|
||
[SerializeField] private Ease moveEase = Ease.OutCubic; // 移动动画缓动类型
|
||
|
||
[Header("部件信息")]
|
||
[SerializeField] private List<PartInfo> parts = new List<PartInfo>(); // 部件列表
|
||
[Header("部件收集设置")]
|
||
[SerializeField] private bool includeInactive = false; // 是否包含非激活的物体
|
||
[SerializeField] private bool skipEmptyTransforms = true; // 是否跳过空变换节点
|
||
|
||
// 状态控制
|
||
private bool isSplit = false; // 当前是否已拆分
|
||
private Sequence currentAnimationSequence; // 当前动画序列
|
||
private float lastOperationTime = 0f; // 上次操作时间
|
||
private const float CLICK_COOLDOWN = 0.1f; // 操作冷却时间
|
||
private Vector3 centerPoint = Vector3.zero; // 动画中心点
|
||
private Transform targetParent; // 目标父节点
|
||
|
||
#region 公共接口
|
||
|
||
/// <summary>
|
||
/// 设置目标父节点
|
||
/// 参数:parent - 包含所有要拆装的部件的父节点
|
||
/// </summary>
|
||
public void SetTarget(Transform parent)
|
||
{
|
||
if (parent == null)
|
||
{
|
||
Debug.LogError("SetTarget: 父节点不能为空");
|
||
return;
|
||
}
|
||
|
||
// 停止当前动画
|
||
StopCurrentAnimation();
|
||
|
||
// 清空现有部件
|
||
parts.Clear();
|
||
|
||
targetParent = parent;
|
||
|
||
// 收集所有直接子物体
|
||
CollectAllChildren(parent);
|
||
//CollectAllChildrenRecursive(parent);
|
||
|
||
if (parts.Count == 0)
|
||
{
|
||
Debug.LogWarning($"SetTarget: 目标父节点 {parent.name} 下没有找到部件");
|
||
return;
|
||
}
|
||
|
||
// 初始化部件数据
|
||
InitializeParts();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 拆分所有部件
|
||
/// 动画流程:所有部件从各自当前位置向外平移远离中心点
|
||
/// </summary>
|
||
public void Split()
|
||
{
|
||
if (parts.Count == 0)
|
||
{
|
||
Debug.LogError("Split: 请先调用 SetTarget 设置目标");
|
||
return;
|
||
}
|
||
|
||
if (isSplit)
|
||
{
|
||
Debug.Log("Split: 已处于拆分状态");
|
||
return;
|
||
}
|
||
|
||
if (IsInCooldown())
|
||
{
|
||
Debug.Log("Split: 操作过于频繁");
|
||
return;
|
||
}
|
||
|
||
StopCurrentAnimation();
|
||
lastOperationTime = Time.time;
|
||
|
||
// 计算中心点
|
||
CalculateCenterPoint();
|
||
|
||
// 计算远离中心点的位置
|
||
CalculateDetachedPositions();
|
||
|
||
// 使用DOTween创建动画序列
|
||
currentAnimationSequence = DOTween.Sequence();
|
||
|
||
int activeParts = 0;
|
||
|
||
Debug.Log($"{parts.Count}");
|
||
|
||
foreach (PartInfo part in parts)
|
||
{
|
||
if (part.partTransform == null || !part.partTransform.gameObject.activeInHierarchy)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!IsTransformValid(part.partTransform))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
activeParts++;
|
||
|
||
// 创建向外平移的动画
|
||
var moveTween = part.partTransform.DOMove(
|
||
part.detachedWorldPosition,
|
||
animationDuration
|
||
).SetEase(moveEase);
|
||
|
||
// 添加到序列
|
||
currentAnimationSequence.Join(moveTween);
|
||
}
|
||
|
||
if (activeParts == 0)
|
||
{
|
||
Debug.LogError("Split: 没有有效的部件可以拆分");
|
||
currentAnimationSequence = null;
|
||
return;
|
||
}
|
||
|
||
// 设置动画完成回调
|
||
currentAnimationSequence.OnComplete(() =>
|
||
{
|
||
isSplit = true;
|
||
currentAnimationSequence = null;
|
||
});
|
||
|
||
currentAnimationSequence.Play();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 合并所有部件
|
||
/// 动画流程:所有部件从当前位置平移回到组装位置
|
||
/// </summary>
|
||
public void Merge()
|
||
{
|
||
if (parts.Count == 0)
|
||
{
|
||
Debug.LogError("Merge: 请先调用 SetTarget 设置目标");
|
||
return;
|
||
}
|
||
|
||
if (!isSplit)
|
||
{
|
||
Debug.Log("Merge: 已处于合并状态");
|
||
return;
|
||
}
|
||
|
||
if (IsInCooldown())
|
||
{
|
||
Debug.Log("Merge: 操作过于频繁");
|
||
return;
|
||
}
|
||
|
||
StopCurrentAnimation();
|
||
lastOperationTime = Time.time;
|
||
|
||
// 使用DOTween创建动画序列
|
||
currentAnimationSequence = DOTween.Sequence();
|
||
|
||
int activeParts = 0;
|
||
foreach (PartInfo part in parts)
|
||
{
|
||
if (part.partTransform == null || !part.partTransform.gameObject.activeInHierarchy)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!IsTransformValid(part.partTransform))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
activeParts++;
|
||
|
||
// 创建回到组装位置的动画
|
||
var moveTween = part.partTransform.DOMove(
|
||
part.assembledWorldPosition,
|
||
animationDuration
|
||
).SetEase(moveEase);
|
||
|
||
// 添加到序列
|
||
currentAnimationSequence.Join(moveTween);
|
||
}
|
||
|
||
if (activeParts == 0)
|
||
{
|
||
Debug.LogError("Merge: 没有有效的部件可以合并");
|
||
currentAnimationSequence = null;
|
||
return;
|
||
}
|
||
|
||
// 设置动画完成回调
|
||
currentAnimationSequence.OnComplete(() =>
|
||
{
|
||
isSplit = false;
|
||
currentAnimationSequence = null;
|
||
});
|
||
|
||
currentAnimationSequence.Play();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换拆分/合并状态
|
||
/// 如果当前是合并状态则拆分,如果是拆分状态则合并
|
||
/// </summary>
|
||
public void Toggle()
|
||
{
|
||
if (IsInCooldown())
|
||
{
|
||
Debug.Log("Toggle: 操作过于频繁");
|
||
return;
|
||
}
|
||
|
||
if (isSplit)
|
||
{
|
||
Merge();
|
||
}
|
||
else
|
||
{
|
||
Split();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止当前动画
|
||
/// 立即停止所有正在进行的动画
|
||
/// </summary>
|
||
public void StopCurrentAnimation()
|
||
{
|
||
if (currentAnimationSequence != null && currentAnimationSequence.IsActive())
|
||
{
|
||
currentAnimationSequence.Kill();
|
||
currentAnimationSequence = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 立即完成拆分
|
||
/// 不播放动画,直接跳转到拆分状态
|
||
/// </summary>
|
||
public void InstantSplit()
|
||
{
|
||
StopCurrentAnimation();
|
||
|
||
CalculateCenterPoint();
|
||
CalculateDetachedPositions();
|
||
|
||
int movedParts = 0;
|
||
foreach (PartInfo part in parts)
|
||
{
|
||
if (part.partTransform == null || !part.partTransform.gameObject.activeInHierarchy) continue;
|
||
|
||
if (!IsTransformValid(part.partTransform)) continue;
|
||
|
||
part.partTransform.position = part.detachedWorldPosition;
|
||
part.partTransform.rotation = part.detachedWorldRotation;
|
||
movedParts++;
|
||
}
|
||
|
||
isSplit = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 立即完成合并
|
||
/// 不播放动画,直接跳转到合并状态
|
||
/// </summary>
|
||
public void InstantMerge()
|
||
{
|
||
StopCurrentAnimation();
|
||
|
||
int movedParts = 0;
|
||
foreach (PartInfo part in parts)
|
||
{
|
||
if (part.partTransform == null || !part.partTransform.gameObject.activeInHierarchy) continue;
|
||
|
||
if (!IsTransformValid(part.partTransform)) continue;
|
||
|
||
part.partTransform.position = part.assembledWorldPosition;
|
||
part.partTransform.rotation = part.assembledWorldRotation;
|
||
movedParts++;
|
||
}
|
||
|
||
isSplit = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前状态
|
||
/// 返回:true-已拆分,false-已合并
|
||
/// </summary>
|
||
public bool GetCurrentState()
|
||
{
|
||
return isSplit;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否正在播放动画
|
||
/// 返回:true-动画播放中,false-无动画播放
|
||
/// </summary>
|
||
public bool IsAnimating()
|
||
{
|
||
return currentAnimationSequence != null && currentAnimationSequence.IsActive();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置动画总时长
|
||
/// 参数:duration - 动画时长(秒),最小值0.1
|
||
/// </summary>
|
||
public void SetAnimationDuration(float duration)
|
||
{
|
||
animationDuration = Mathf.Max(0.1f, duration);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置散开距离
|
||
/// 参数:distance - 散开距离,最小值0.5
|
||
/// </summary>
|
||
public void SetSpreadDistance(float distance)
|
||
{
|
||
spreadDistance = Mathf.Max(0.5f, distance);
|
||
|
||
if (parts.Count > 0 && targetParent != null)
|
||
{
|
||
CalculateDetachedPositions();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取部件数量
|
||
/// 返回:收集到的部件数量
|
||
/// </summary>
|
||
public int GetPartCount()
|
||
{
|
||
return parts.Count;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 内部方法
|
||
|
||
/// <summary>
|
||
/// 收集所有直接子物体
|
||
/// 将目标父节点的所有直接子物体收集为部件
|
||
/// </summary>
|
||
private void CollectAllChildren(Transform parent)
|
||
{
|
||
if (parent == null) return;
|
||
|
||
// 收集所有直接子物体
|
||
foreach (Transform child in parent)
|
||
{
|
||
if (child == null) continue;
|
||
|
||
PartInfo part = new PartInfo
|
||
{
|
||
partTransform = child
|
||
};
|
||
parts.Add(part);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归收集所有有效的部件
|
||
/// 参数:parent - 开始收集的父节点
|
||
/// </summary>
|
||
private void CollectAllChildrenRecursive(Transform parent)
|
||
{
|
||
if (parent == null) return;
|
||
|
||
// 检查当前节点是否需要跳过
|
||
if (ShouldSkipTransform(parent))
|
||
{
|
||
// 跳过当前节点,但递归检查其子节点
|
||
foreach (Transform child in parent)
|
||
{
|
||
if (!includeInactive && !child.gameObject.activeSelf) continue;
|
||
CollectAllChildrenRecursive(child);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 当前节点是有效的部件
|
||
PartInfo part = new PartInfo
|
||
{
|
||
partTransform = parent
|
||
};
|
||
parts.Add(part);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 判断是否应该跳过此变换节点
|
||
/// 返回:true-应该跳过,false-应该作为部件收集
|
||
/// </summary>
|
||
private bool ShouldSkipTransform(Transform transform)
|
||
{
|
||
if (transform == null) return true;
|
||
|
||
// 如果设置了跳过空变换节点,则检查当前节点是否为空
|
||
if (skipEmptyTransforms && IsEmptyTransform(transform))
|
||
{
|
||
return true; // 跳过空节点
|
||
}
|
||
|
||
// 检查是否有Renderer组件
|
||
Renderer renderer = transform.GetComponent<Renderer>();
|
||
if (renderer != null)
|
||
{
|
||
return false; // 有Renderer,不跳过
|
||
}
|
||
|
||
// 检查是否有MeshFilter或SkinnedMeshRenderer
|
||
MeshFilter meshFilter = transform.GetComponent<MeshFilter>();
|
||
SkinnedMeshRenderer skinnedMeshRenderer = transform.GetComponent<SkinnedMeshRenderer>();
|
||
|
||
if (meshFilter != null || skinnedMeshRenderer != null)
|
||
{
|
||
return false; // 有模型组件,不跳过
|
||
}
|
||
|
||
// 如果没有Renderer但有子节点,则可能是一个容器节点
|
||
if (transform.childCount > 0)
|
||
{
|
||
return true; // 有子节点且自身没有渲染,跳过
|
||
}
|
||
|
||
// 没有Renderer,没有子节点,检查是否有其他重要的3D组件
|
||
Collider collider = transform.GetComponent<Collider>();
|
||
Rigidbody rigidbody = transform.GetComponent<Rigidbody>();
|
||
|
||
if (collider != null || rigidbody != null)
|
||
{
|
||
return false; // 有其他3D组件,不跳过
|
||
}
|
||
|
||
// 默认不跳过
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断变换节点是否为空
|
||
/// 空节点:没有Renderer,没有MeshFilter,没有SkinnedMeshRenderer
|
||
/// </summary>
|
||
private bool IsEmptyTransform(Transform transform)
|
||
{
|
||
if (transform == null) return true;
|
||
|
||
// 检查是否有Renderer组件
|
||
if (transform.GetComponent<Renderer>() != null) return false;
|
||
|
||
// 检查是否有MeshFilter
|
||
if (transform.GetComponent<MeshFilter>() != null) return false;
|
||
|
||
// 检查是否有SkinnedMeshRenderer
|
||
if (transform.GetComponent<SkinnedMeshRenderer>() != null) return false;
|
||
|
||
// 检查是否有Terrain
|
||
if (transform.GetComponent<Terrain>() != null) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查变换组件是否有效
|
||
/// 参数:t - 要检查的变换组件
|
||
/// 返回:true-有效,false-无效
|
||
/// </summary>
|
||
private bool IsTransformValid(Transform t)
|
||
{
|
||
if (t == null) return false;
|
||
|
||
// 检查变换是否被销毁
|
||
if (t.Equals(null)) return false;
|
||
|
||
// 检查游戏对象是否有效
|
||
if (t.gameObject == null) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算动画中心点
|
||
/// 中心点为所有部件组装位置的平均值
|
||
/// </summary>
|
||
private void CalculateCenterPoint()
|
||
{
|
||
if (targetParent == null || parts.Count == 0)
|
||
{
|
||
centerPoint = Vector3.zero;
|
||
return;
|
||
}
|
||
|
||
// 使用所有部件的平均位置作为中心点
|
||
Vector3 sum = Vector3.zero;
|
||
int count = 0;
|
||
|
||
foreach (PartInfo part in parts)
|
||
{
|
||
if (part.partTransform == null) continue;
|
||
|
||
sum += part.assembledWorldPosition;
|
||
count++;
|
||
}
|
||
|
||
if (count > 0)
|
||
{
|
||
centerPoint = sum / count;
|
||
}
|
||
else
|
||
{
|
||
centerPoint = targetParent.position;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化部件数据
|
||
/// 记录部件的初始位置和旋转
|
||
/// </summary>
|
||
private void InitializeParts()
|
||
{
|
||
if (parts.Count == 0) return;
|
||
|
||
foreach (PartInfo part in parts)
|
||
{
|
||
if (part.partTransform == null) continue;
|
||
|
||
part.assembledWorldPosition = part.partTransform.position;
|
||
part.assembledWorldRotation = part.partTransform.rotation;
|
||
}
|
||
|
||
// 计算中心点
|
||
CalculateCenterPoint();
|
||
|
||
// 计算远离中心点的位置
|
||
CalculateDetachedPositions();
|
||
|
||
isSplit = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算远离中心点的位置
|
||
/// 每个部件沿着从中心点到部件当前位置的方向向外移动固定距离
|
||
/// </summary>
|
||
private void CalculateDetachedPositions()
|
||
{
|
||
if (parts.Count == 0) return;
|
||
|
||
for (int i = 0; i < parts.Count; i++)
|
||
{
|
||
if (parts[i].partTransform == null) continue;
|
||
|
||
// 计算从中心点到部件当前位置的方向
|
||
Vector3 direction = (parts[i].assembledWorldPosition - centerPoint).normalized;
|
||
|
||
// 如果方向为零向量(部件就在中心点),则使用随机方向
|
||
if (direction == Vector3.zero)
|
||
{
|
||
direction = Random.onUnitSphere;
|
||
}
|
||
|
||
// 计算远离中心点的位置
|
||
parts[i].detachedWorldPosition = centerPoint + direction * spreadDistance;
|
||
|
||
// 保持部件原有的旋转
|
||
parts[i].detachedWorldRotation = parts[i].assembledWorldRotation;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查是否在操作冷却时间内
|
||
/// 防止频繁点击导致动画冲突
|
||
/// 返回:true-冷却中,false-可操作
|
||
/// </summary>
|
||
private bool IsInCooldown()
|
||
{
|
||
float timeSinceLastOperation = Time.time - lastOperationTime;
|
||
return timeSinceLastOperation < CLICK_COOLDOWN;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 生命周期方法
|
||
|
||
/// <summary>
|
||
/// 销毁时清理资源
|
||
/// 停止所有动画,防止内存泄漏
|
||
/// </summary>
|
||
private void OnDestroy()
|
||
{
|
||
StopCurrentAnimation();
|
||
}
|
||
|
||
#endregion
|
||
} |