@@ -0,0 +1,199 @@
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
public partial class ElementFolder
|
||||
{
|
||||
private void QuickCopy(Vector3 unitPositionOffset, Vector3 unitRotationOffset, Vector3 unitScaleOffset, float unitTimeOffset, int iteration = 1)
|
||||
{
|
||||
if (iteration <= 0) return;
|
||||
|
||||
if (parentElement == null)
|
||||
{
|
||||
LogWindow.Log("Folder QuickCopy requires a parent element.", Color.red);
|
||||
return;
|
||||
}
|
||||
|
||||
CopyPasteDeleteModule cpd = EditorManager.instance.operationManager.CopyPasteDeleteModule;
|
||||
cpd.CopyElement(this);
|
||||
|
||||
for (int i = 1; i <= iteration; i++)
|
||||
{
|
||||
ElementFolder newFolder = cpd.PasteElementRaw(this, parentElement) as ElementFolder;
|
||||
if (newFolder == null) continue;
|
||||
|
||||
Vector3 positionOffset = unitPositionOffset * i;
|
||||
Vector3 rotationOffset = unitRotationOffset * i;
|
||||
Vector3 scaleOffset = unitScaleOffset * i;
|
||||
float timeOffset = unitTimeOffset * i;
|
||||
|
||||
newFolder.ApplyFolderPositionOffset(positionOffset);
|
||||
newFolder.ApplyFolderRotationOffset(rotationOffset);
|
||||
newFolder.ApplyFolderScaleOffset(scaleOffset);
|
||||
newFolder.ApplyElementFolderTimeOffsetRecursive(timeOffset, true);
|
||||
newFolder.Refresh();
|
||||
}
|
||||
|
||||
cpd.pastedElementList?.Clear();
|
||||
}
|
||||
|
||||
private void ApplyFolderPositionOffset(Vector3 positionOffset)
|
||||
{
|
||||
if (transformSubmodule == null) return;
|
||||
|
||||
transformSubmodule.originalPosition += positionOffset;
|
||||
transformSubmodule.Refresh();
|
||||
}
|
||||
|
||||
private void ApplyFolderRotationOffset(Vector3 rotationOffset)
|
||||
{
|
||||
if (transformSubmodule == null) return;
|
||||
|
||||
transformSubmodule.originalEulerAngles += rotationOffset;
|
||||
transformSubmodule.Refresh();
|
||||
}
|
||||
|
||||
private void ApplyFolderScaleOffset(Vector3 scaleOffset)
|
||||
{
|
||||
if (transformSubmodule == null) return;
|
||||
|
||||
transformSubmodule.originalScale += scaleOffset;
|
||||
transformSubmodule.Refresh();
|
||||
}
|
||||
|
||||
private void ApplyElementFolderTimeOffsetRecursive(float offset, bool affectNotes)
|
||||
{
|
||||
List<GameElement> elements = GetAllGameElementsFromThis();
|
||||
for (int i = 0; i < elements.Count; i++)
|
||||
{
|
||||
ApplyQuickCopyTimeOffsetToElement(elements[i], offset, affectNotes);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyQuickCopyTimeOffsetToElement(GameElement element, float offset, bool affectNotes)
|
||||
{
|
||||
if (element == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
switch (element)
|
||||
{
|
||||
case AnimationBase animation:
|
||||
animation.ApplyTimeOffset(offset);
|
||||
animation.Refresh();
|
||||
return;
|
||||
|
||||
case NoteBase note:
|
||||
if (!affectNotes) return;
|
||||
|
||||
note.exactJudgeTime += offset;
|
||||
if (note is Hold hold)
|
||||
{
|
||||
hold.holdEndTime += offset;
|
||||
}
|
||||
ApplyDurationOffset(note, offset, true);
|
||||
note.UpdateNoteInTrack(EditorManager.instance.songInformation.songTime);
|
||||
note.AddinNoteManager(false);
|
||||
note.Refresh();
|
||||
return;
|
||||
|
||||
case Track track:
|
||||
ApplyDurationOffset(track, offset, true);
|
||||
if (track.trackTimeSubmodule is TrackTimeSubmoduleMovable movable)
|
||||
{
|
||||
movable.trackStartTime += offset;
|
||||
movable.trackEndTime += offset;
|
||||
movable.Refresh();
|
||||
}
|
||||
track.Refresh();
|
||||
return;
|
||||
|
||||
case TrackPercentPoint trackPercentPoint:
|
||||
trackPercentPoint.trackPercent?.ApplyTimeOffset(offset);
|
||||
ApplyDurationOffset(trackPercentPoint, offset, true);
|
||||
trackPercentPoint.Refresh();
|
||||
return;
|
||||
|
||||
case CrossTrackPoint crossTrackPoint:
|
||||
ApplyFlexibleIntTimeOffset(crossTrackPoint.trackSwitch, offset);
|
||||
crossTrackPoint.trackPercent?.ApplyTimeOffset(offset);
|
||||
ApplyDurationOffset(crossTrackPoint, offset, true);
|
||||
crossTrackPoint.Refresh();
|
||||
return;
|
||||
|
||||
case TimeEffectsCollection timeEffectsCollection:
|
||||
timeEffectsCollection.time += offset;
|
||||
timeEffectsCollection.Refresh();
|
||||
return;
|
||||
|
||||
case ParticleEmitter particleEmitter:
|
||||
particleEmitter.playTime += offset;
|
||||
particleEmitter.stopTime += offset;
|
||||
ApplyDurationOffset(particleEmitter, offset, true);
|
||||
particleEmitter.Refresh();
|
||||
return;
|
||||
|
||||
case ParticleTracker particleTracker:
|
||||
particleTracker.playTime += offset;
|
||||
particleTracker.stopTime += offset;
|
||||
particleTracker.Refresh();
|
||||
return;
|
||||
|
||||
case ObjectTracker objectTracker:
|
||||
objectTracker.playTime += offset;
|
||||
objectTracker.stopTime += offset;
|
||||
objectTracker.Refresh();
|
||||
return;
|
||||
|
||||
case SkyboxSubsetter skyboxSubsetter:
|
||||
if (skyboxSubsetter.blendTimeList != null)
|
||||
{
|
||||
for (int i = 0; i < skyboxSubsetter.blendTimeList.Count; i++)
|
||||
{
|
||||
skyboxSubsetter.blendTimeList[i] += offset;
|
||||
}
|
||||
}
|
||||
skyboxSubsetter.Refresh();
|
||||
return;
|
||||
|
||||
case DTMTrail dtmTrail:
|
||||
dtmTrail.ApplyTimeOffset(offset);
|
||||
dtmTrail.Refresh();
|
||||
return;
|
||||
|
||||
case IHaveTimeDurationSubmodule hasDuration:
|
||||
ApplyDurationOffset(hasDuration, offset, true);
|
||||
element.Refresh();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[ElementFolderQuickCopy] Time offset failed on '{element.elementName}' ({element.GetType().Name}): {ex.Message}\n{ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyDurationOffset(IHaveTimeDurationSubmodule element, float offset, bool onlyWhenOverriding)
|
||||
{
|
||||
TimeDurationSubmodule duration = element.timeDurationSubmodule;
|
||||
if (duration == null) return;
|
||||
if (onlyWhenOverriding && !duration.isOverridingDuration) return;
|
||||
|
||||
duration.startTime += offset;
|
||||
duration.endTime += offset;
|
||||
}
|
||||
|
||||
private static void ApplyFlexibleIntTimeOffset(FlexibleInt flexibleInt, float offset)
|
||||
{
|
||||
if (flexibleInt?.animations == null) return;
|
||||
|
||||
for (int i = 0; i < flexibleInt.animations.Count; i++)
|
||||
{
|
||||
flexibleInt.animations[i].ApplyTimeOffset(offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ce75de045ac4fdaa86370a04faa919f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -53,6 +53,42 @@ namespace Ichni.RhythmGame
|
||||
})
|
||||
|
||||
.Section("Tools", sectionOrder: 40)
|
||||
.Button("QuickCopy", () =>
|
||||
{
|
||||
var inspectorMain = EditorManager.instance.uiManager.inspector;
|
||||
IHaveInspection qcWindow = inspectorMain.GenerateSecondaryWindow(this, elementName + "'s Quick Copy");
|
||||
var qcContainer = qcWindow.GenerateContainer();
|
||||
|
||||
var positionSub = qcContainer.GenerateSubcontainer(3);
|
||||
var xField = qcWindow.GenerateInputField(positionSub, "X offset", "0");
|
||||
var yField = qcWindow.GenerateInputField(positionSub, "Y offset", "0");
|
||||
var zField = qcWindow.GenerateInputField(positionSub, "Z offset", "0");
|
||||
|
||||
var rotationSub = qcContainer.GenerateSubcontainer(3);
|
||||
var rxField = qcWindow.GenerateInputField(rotationSub, "RX offset", "0");
|
||||
var ryField = qcWindow.GenerateInputField(rotationSub, "RY offset", "0");
|
||||
var rzField = qcWindow.GenerateInputField(rotationSub, "RZ offset", "0");
|
||||
|
||||
var scaleSub = qcContainer.GenerateSubcontainer(3);
|
||||
var sxField = qcWindow.GenerateInputField(scaleSub, "SX offset", "0");
|
||||
var syField = qcWindow.GenerateInputField(scaleSub, "SY offset", "0");
|
||||
var szField = qcWindow.GenerateInputField(scaleSub, "SZ offset", "0");
|
||||
|
||||
var timeSub = qcContainer.GenerateSubcontainer(3);
|
||||
var timeField = qcWindow.GenerateInputField(timeSub, "Time offset", "0");
|
||||
var iterationField = qcWindow.GenerateInputField(timeSub, "Iteration", "1");
|
||||
|
||||
qcWindow.GenerateButton(this, timeSub, "Copy", () =>
|
||||
{
|
||||
Vector3 positionOffset = new Vector3(xField.GetValue<float>(), yField.GetValue<float>(), zField.GetValue<float>());
|
||||
Vector3 rotationOffset = new Vector3(rxField.GetValue<float>(), ryField.GetValue<float>(), rzField.GetValue<float>());
|
||||
Vector3 scaleOffset = new Vector3(sxField.GetValue<float>(), syField.GetValue<float>(), szField.GetValue<float>());
|
||||
float timeOffset = timeField.GetValue<float>();
|
||||
int iteration = iterationField.GetValue<int>();
|
||||
QuickCopy(positionOffset, rotationOffset, scaleOffset, timeOffset, iteration);
|
||||
});
|
||||
})
|
||||
.Span(3)
|
||||
.Button("Time Shift Tool", () =>
|
||||
{
|
||||
GeneralSecondaryWindow timeShiftWindow = UnityEngine.Object.Instantiate(
|
||||
@@ -114,81 +150,9 @@ namespace Ichni.RhythmGame
|
||||
|
||||
public void ApplyTimeShiftRecursive(bool affectNotes, float offset)
|
||||
{
|
||||
ApplyTimeShiftToElement(this, affectNotes, offset);
|
||||
ApplyElementFolderTimeOffsetRecursive(offset, affectNotes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归偏移元素及其所有子元素的时间参数。
|
||||
/// 处理顺序:先偏移当前元素,再递归处理子元素。
|
||||
/// </summary>
|
||||
private void ApplyTimeShiftToElement(GameElement element, bool affectNotes, float offset)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (element == null) return;
|
||||
|
||||
switch (element)
|
||||
{
|
||||
case AnimationBase anim:
|
||||
anim.ApplyTimeOffset(offset);
|
||||
anim.Refresh();
|
||||
break;
|
||||
|
||||
case NoteBase note when affectNotes:
|
||||
note.exactJudgeTime += offset;
|
||||
if (note is Hold hold)
|
||||
{
|
||||
hold.holdEndTime += offset;
|
||||
}
|
||||
note.UpdateNoteInTrack(EditorManager.instance.songInformation.songTime);
|
||||
note.AddinNoteManager(false);
|
||||
note.Refresh();
|
||||
break;
|
||||
|
||||
case Track track:
|
||||
// 偏移 Track 自身的 timeDurationSubmodule
|
||||
if (track.timeDurationSubmodule != null && track.timeDurationSubmodule.isOverridingDuration)
|
||||
{
|
||||
track.timeDurationSubmodule.startTime += offset;
|
||||
track.timeDurationSubmodule.endTime += offset;
|
||||
}
|
||||
// 偏移 TrackTimeSubmodule 中的轨道起止时间
|
||||
if (track.trackTimeSubmodule is TrackTimeSubmoduleMovable movable)
|
||||
{
|
||||
movable.trackStartTime += offset;
|
||||
movable.trackEndTime += offset;
|
||||
movable.Refresh();
|
||||
}
|
||||
track.Refresh();
|
||||
break;
|
||||
|
||||
case DTMTrail dtmTrail:
|
||||
dtmTrail.ApplyTimeOffset(offset);
|
||||
dtmTrail.Refresh();
|
||||
break;
|
||||
|
||||
// 泛型兜底:处理 TemporaryObject、ParticleEmitter、GameCamera、TimeEffectsCollection 等
|
||||
// 所有实现了 IHaveTimeDurationSubmodule 但不属于上述类型的元素
|
||||
case IHaveTimeDurationSubmodule hasDuration:
|
||||
hasDuration.ApplyTimeOffset(offset);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[TimeShift] 处理元素 '{element?.elementName}' ({element?.GetType().Name}) 时出错: {ex.Message}\n{ex.StackTrace}");
|
||||
}
|
||||
|
||||
// 使用快照副本进行安全迭代,防止子操作意外修改集合
|
||||
if (element.childElementList != null && element.childElementList.Count > 0)
|
||||
{
|
||||
List<GameElement> snapshot = new List<GameElement>(element.childElementList);
|
||||
for (int i = 0; i < snapshot.Count; i++)
|
||||
{
|
||||
ApplyTimeShiftToElement(snapshot[i], affectNotes, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Ichni.RhythmGame
|
||||
/// <summary>
|
||||
/// 将物体的z轴指向目标物体,注意,LookAt的启用期间,物体的旋转将被锁定
|
||||
/// </summary>
|
||||
public partial class LookAt : AnimationBase
|
||||
public partial class LookAt : AnimationBase, IPastedReferenceResolver
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
public GameCamera gameCamera;
|
||||
@@ -98,6 +98,22 @@ namespace Ichni.RhythmGame
|
||||
enabling.animations.ForEach(anim => anim.ApplyTimeOffset(offset));
|
||||
}
|
||||
|
||||
public void ResolvePastedReferences(IReadOnlyDictionary<Guid, GameElement> pastedElementBySourceGuid)
|
||||
{
|
||||
if (lookAtObject == null) return;
|
||||
|
||||
if (pastedElementBySourceGuid.TryGetValue(lookAtObject.elementGuid, out GameElement pastedTarget))
|
||||
{
|
||||
lookAtObject = pastedTarget;
|
||||
targetTransformSubmodule = (animatedObject as IHaveTransformSubmodule)?.transformSubmodule;
|
||||
if (targetTransformSubmodule != null)
|
||||
{
|
||||
targetTransformSubmodule.lookAt = this;
|
||||
targetTransformSubmodule.eulerAnglesDirtyMark = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
targetTransformSubmodule.eulerAnglesDirtyMark = true;
|
||||
@@ -112,4 +128,4 @@ namespace Ichni.RhythmGame
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Ichni.RhythmGame
|
||||
/// Synchronizes this element's world position to another transform-bearing element.
|
||||
/// The target world position is converted back into this element parent's local space.
|
||||
/// </summary>
|
||||
public partial class PositionSync : AnimationBase
|
||||
public partial class PositionSync : AnimationBase, IPastedReferenceResolver
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
public TransformSubmodule targetTransformSubmodule;
|
||||
@@ -114,6 +114,21 @@ namespace Ichni.RhythmGame
|
||||
enabling?.animations.ForEach(anim => anim.ApplyTimeOffset(offset));
|
||||
}
|
||||
|
||||
public void ResolvePastedReferences(IReadOnlyDictionary<Guid, GameElement> pastedElementBySourceGuid)
|
||||
{
|
||||
if (syncTargetObject == null) return;
|
||||
|
||||
if (pastedElementBySourceGuid.TryGetValue(syncTargetObject.elementGuid, out GameElement pastedTarget))
|
||||
{
|
||||
syncTargetObject = pastedTarget;
|
||||
targetTransformSubmodule = (animatedObject as IHaveTransformSubmodule)?.transformSubmodule;
|
||||
if (targetTransformSubmodule != null)
|
||||
{
|
||||
targetTransformSubmodule.positionDirtyMark = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (targetTransformSubmodule != null)
|
||||
|
||||
Reference in New Issue
Block a user