using System; using System.Collections.Generic; using Ichni.RhythmGame.Beatmap; using UnityEngine; namespace Ichni.RhythmGame { /// /// Synchronizes this element's position to another element's world position. /// public partial class PositionSync : AnimationBase { #region [暴露属性字段与关联] Exposed Fields & References public TransformSubmodule targetTransformSubmodule; public GameElement syncTargetObject; public FlexibleBool enabling; #endregion #region [生命周期与工厂] Lifecycle & Factory public static PositionSync GenerateElement(string elementName, Guid id, List tags, bool isFirstGenerated, GameElement animatedObject, GameElement syncTargetObject, FlexibleBool enabling) { PositionSync sync = Instantiate(GameManager.Instance.basePrefabs.emptyObject).AddComponent(); sync.Initialize(elementName, id, tags, isFirstGenerated, animatedObject); sync.animatedObject = animatedObject; sync.syncTargetObject = syncTargetObject; sync.enabling = enabling ?? new FlexibleBool(); sync.animationReturnType = FlexibleReturnType.Before; sync.targetTransformSubmodule = (animatedObject as IHaveTransformSubmodule)?.transformSubmodule; return sync; } public override void SetDefaultSubmodules() { timeDurationSubmodule = new TimeDurationSubmodule(this); } public override void OnDelete() { base.OnDelete(); targetTransformSubmodule?.MarkPositionDirty(); } #endregion #region [核心动画逻辑] Core Animation Logic public override void ScheduledUpdate(UpdatePhase phase, float songTime) { base.ScheduledUpdate(phase, songTime); // 与 CreatorStudio 保持一致:这里在 Animation 阶段读取 target world position。 // 如果 target 是 TrackPercentPoint,当前帧 TrackFollower 尚未执行,因此读到的可能是上一帧位置。 // 这个一帧延后是有意保留的兼容行为;好处是随后 Apply 阶段能刷新被同步的 PathNode/Track, // 让本帧后续 TrackFollower/Note 使用更新后的轨道数据。 if (phase == UpdatePhase.Animation && enabling != null && enabling.value) { ApplyPositionSync(); } } protected override void UpdateAnimation(float songTime, bool forceUpdate) { if (enabling == null) { animationReturnType = FlexibleReturnType.None; return; } enabling.UpdateFlexibleBool(songTime); if (enabling.value) { animationReturnType = FlexibleReturnType.MiddleExecuting; targetTransformSubmodule?.MarkPositionDirty(); } else if (animationReturnType != FlexibleReturnType.MiddleInterval) { animationReturnType = FlexibleReturnType.MiddleInterval; targetTransformSubmodule?.MarkPositionDirty(); } } private void ApplyPositionSync() { if (targetTransformSubmodule == null || syncTargetObject == null) { return; } Transform self = targetTransformSubmodule.attachedGameElement.transform; Transform parent = self.parent; Vector3 targetWorldPosition = syncTargetObject.transform.position; Vector3 desiredLocalPosition = parent != null ? parent.InverseTransformPoint(targetWorldPosition) : targetWorldPosition; Vector3 syncOffset = desiredLocalPosition - targetTransformSubmodule.originalPosition; targetTransformSubmodule.positionOffset += syncOffset; targetTransformSubmodule.MarkPositionDirty(); } public override void ApplyTimeOffset(float offset) { base.ApplyTimeOffset(offset); enabling?.animations.ForEach(anim => anim.ApplyTimeOffset(offset)); } #endregion } }