using DG.Tweening;
using System.Collections.Generic;
using UnityEngine;
///
/// 模型拆装动画控制器
/// 功能:实现模型部件从中心点向外平移远离的拆装动画
/// 特点:使用DOTween实现平滑动画,部件从中心点向外平移,不旋转
///
public class ModelSpreadController : MonoBehaviour
{
///
/// 部件信息结构
///
[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 parts = new List(); // 部件列表
[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 公共接口
///
/// 设置目标父节点
/// 参数:parent - 包含所有要拆装的部件的父节点
///
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();
}
///
/// 拆分所有部件
/// 动画流程:所有部件从各自当前位置向外平移远离中心点
///
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();
}
///
/// 合并所有部件
/// 动画流程:所有部件从当前位置平移回到组装位置
///
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();
}
///
/// 切换拆分/合并状态
/// 如果当前是合并状态则拆分,如果是拆分状态则合并
///
public void Toggle()
{
if (IsInCooldown())
{
Debug.Log("Toggle: 操作过于频繁");
return;
}
if (isSplit)
{
Merge();
}
else
{
Split();
}
}
///
/// 停止当前动画
/// 立即停止所有正在进行的动画
///
public void StopCurrentAnimation()
{
if (currentAnimationSequence != null && currentAnimationSequence.IsActive())
{
currentAnimationSequence.Kill();
currentAnimationSequence = null;
}
}
///
/// 立即完成拆分
/// 不播放动画,直接跳转到拆分状态
///
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;
}
///
/// 立即完成合并
/// 不播放动画,直接跳转到合并状态
///
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;
}
///
/// 获取当前状态
/// 返回:true-已拆分,false-已合并
///
public bool GetCurrentState()
{
return isSplit;
}
///
/// 是否正在播放动画
/// 返回:true-动画播放中,false-无动画播放
///
public bool IsAnimating()
{
return currentAnimationSequence != null && currentAnimationSequence.IsActive();
}
///
/// 设置动画总时长
/// 参数:duration - 动画时长(秒),最小值0.1
///
public void SetAnimationDuration(float duration)
{
animationDuration = Mathf.Max(0.1f, duration);
}
///
/// 设置散开距离
/// 参数:distance - 散开距离,最小值0.5
///
public void SetSpreadDistance(float distance)
{
spreadDistance = Mathf.Max(0.5f, distance);
if (parts.Count > 0 && targetParent != null)
{
CalculateDetachedPositions();
}
}
///
/// 获取部件数量
/// 返回:收集到的部件数量
///
public int GetPartCount()
{
return parts.Count;
}
#endregion
#region 内部方法
///
/// 收集所有直接子物体
/// 将目标父节点的所有直接子物体收集为部件
///
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);
}
}
///
/// 递归收集所有有效的部件
/// 参数:parent - 开始收集的父节点
///
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);
}
///
/// 判断是否应该跳过此变换节点
/// 返回:true-应该跳过,false-应该作为部件收集
///
private bool ShouldSkipTransform(Transform transform)
{
if (transform == null) return true;
// 如果设置了跳过空变换节点,则检查当前节点是否为空
if (skipEmptyTransforms && IsEmptyTransform(transform))
{
return true; // 跳过空节点
}
// 检查是否有Renderer组件
Renderer renderer = transform.GetComponent();
if (renderer != null)
{
return false; // 有Renderer,不跳过
}
// 检查是否有MeshFilter或SkinnedMeshRenderer
MeshFilter meshFilter = transform.GetComponent();
SkinnedMeshRenderer skinnedMeshRenderer = transform.GetComponent();
if (meshFilter != null || skinnedMeshRenderer != null)
{
return false; // 有模型组件,不跳过
}
// 如果没有Renderer但有子节点,则可能是一个容器节点
if (transform.childCount > 0)
{
return true; // 有子节点且自身没有渲染,跳过
}
// 没有Renderer,没有子节点,检查是否有其他重要的3D组件
Collider collider = transform.GetComponent();
Rigidbody rigidbody = transform.GetComponent();
if (collider != null || rigidbody != null)
{
return false; // 有其他3D组件,不跳过
}
// 默认不跳过
return false;
}
///
/// 判断变换节点是否为空
/// 空节点:没有Renderer,没有MeshFilter,没有SkinnedMeshRenderer
///
private bool IsEmptyTransform(Transform transform)
{
if (transform == null) return true;
// 检查是否有Renderer组件
if (transform.GetComponent() != null) return false;
// 检查是否有MeshFilter
if (transform.GetComponent() != null) return false;
// 检查是否有SkinnedMeshRenderer
if (transform.GetComponent() != null) return false;
// 检查是否有Terrain
if (transform.GetComponent() != null) return false;
return true;
}
///
/// 检查变换组件是否有效
/// 参数:t - 要检查的变换组件
/// 返回:true-有效,false-无效
///
private bool IsTransformValid(Transform t)
{
if (t == null) return false;
// 检查变换是否被销毁
if (t.Equals(null)) return false;
// 检查游戏对象是否有效
if (t.gameObject == null) return false;
return true;
}
///
/// 计算动画中心点
/// 中心点为所有部件组装位置的平均值
///
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;
}
}
///
/// 初始化部件数据
/// 记录部件的初始位置和旋转
///
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;
}
///
/// 计算远离中心点的位置
/// 每个部件沿着从中心点到部件当前位置的方向向外移动固定距离
///
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;
}
}
///
/// 检查是否在操作冷却时间内
/// 防止频繁点击导致动画冲突
/// 返回:true-冷却中,false-可操作
///
private bool IsInCooldown()
{
float timeSinceLastOperation = Time.time - lastOperationTime;
return timeSinceLastOperation < CLICK_COOLDOWN;
}
#endregion
#region 生命周期方法
///
/// 销毁时清理资源
/// 停止所有动画,防止内存泄漏
///
private void OnDestroy()
{
StopCurrentAnimation();
}
#endregion
}