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 }