1.新增了SplineAnchor的旋转跟随2.修复了IInteraction交互物体的乱序错误

This commit is contained in:
2025-10-08 21:47:18 +08:00
parent 0c11c917c4
commit 943c86d5f5
55 changed files with 329 additions and 555 deletions

View File

@@ -19,46 +19,96 @@ namespace Demo.Game
item.SetupDuration(new(VisibleDuration.x, VisibleDuration.y), GetTimelineItemColor());
}
[Serializable]
public enum DurationStats
{
BeforeBegin = 0,
BeforeEnd,
After,
Judgement
}
public const float IdentifiableJudgementTerminal = 0.16f;
[Content, SerializeField, Header("Cache")]
public float MyCacheUpdateTick = 0;
[Header(nameof(VisibleDuration))]
[Content, SerializeField] private Vector2 VisibleDuration;
public static float DefaultVisibleLength = IdentifiableJudgementTerminal * 15;
[Content, SerializeField] public UnityEvent VisibleDurationBeforeEvent = new(),
VisibleDurationBeginEvent = new(), VisibleDurationEndEvent = new();
[Content, SerializeField] private DurationStats VisibleDurationStats = default;
[Header(nameof(InteractiveDuration))]
[Content, SerializeField] private Vector2 InteractiveDuration;
public static float DefaultInteractiveLength = IdentifiableJudgementTerminal * 9;
[Content, SerializeField] public UnityEvent InteractiveDurationBeforeEvent = new(),
InteractiveDurationBeginEvent = new(), InteractiveDurationEndEvent = new();
[Content, SerializeField] private DurationStats InteractiveDurationStats = default;
[Header(nameof(InteractableScoreInterval))]
[Content, SerializeField] private Vector2 InteractableScoreInterval;
public static float DefaultInteractableScoreIntervalLength = IdentifiableJudgementTerminal * 6;
[Content, SerializeField] public UnityEvent InteractableScoreIntervalBeforeEvent = new(),
InteractableScoreIntervalBeginEvent = new(), InteractableScoreIntervalEndEvent = new();
[Content, SerializeField] private DurationStats InteractableScoreIntervalStats = default;
[Header(nameof(InteractableIntervalThatCanScoreBest))]
[Content, SerializeField] private Vector2 InteractableIntervalThatCanScoreBest;
public static float DefaultInteractableIntervalLengthThatCanScoreBest = IdentifiableJudgementTerminal * 3;
[Content, SerializeField] public UnityEvent InteractableIntervalThatCanScoreBestBeforeEvent = new(),
InteractableIntervalThatCanScoreBestBeginEvent = new(), InteractableIntervalThatCanScoreBestEndEvent = new();
[Content, SerializeField] private DurationStats InteractableIntervalThatCanScoreBestStats = default;
[Flags,Serializable]
public enum UpdatePhaseComponent
{
OutsideVisibleDuration = 0b00000000,
//
InsideVisibleDuration = 0b00000001,
InsideInteractiveDuration = 0b00000010,
InsideInteractableScoreInterval = 0b00000100,
InsideInteractableIntervalThatCanScoreBest = 0b00001000,
//
EnterVisibleDuration = 0b00010000,
EnterInteractiveDuration = 0b00100000,
EnterInteractableScoreInterval = 0b01000000,
EnterInteractableIntervalThatCanScoreBest = 0b10000000,
}
[Flags, Serializable]
public enum UpdatePhase
{
// 进入可视区间之前
BeforeVisibleDuration
= UpdatePhaseComponent.OutsideVisibleDuration,
// 进入不可判定但可视的3级判定区间
EnterVisibleDuration
= UpdatePhaseComponent.InsideVisibleDuration | UpdatePhaseComponent.EnterVisibleDuration,
// 进入可判定为失误的2级判定区间
EnterInteractiveDuration
= UpdatePhaseComponent.InsideInteractiveDuration | UpdatePhaseComponent.EnterInteractiveDuration | UpdatePhase.EnterVisibleDuration,
// 进入可得分的1级判定区间
EnterInteractableScoreInterval
= UpdatePhaseComponent.InsideInteractableScoreInterval | UpdatePhaseComponent.EnterInteractableScoreInterval | UpdatePhase.EnterInteractiveDuration,
// 进入可得满分的0级判定区间
EnterInteractableIntervalThatCanScoreBest
= UpdatePhaseComponent.InsideInteractableIntervalThatCanScoreBest| UpdatePhaseComponent.EnterInteractableIntervalThatCanScoreBest
| UpdatePhase.EnterInteractableScoreInterval,
// 退出0级判定区间
AfterInteractableIntervalThatCanScoreBest
= UpdatePhase.EnterInteractableIntervalThatCanScoreBest&(~UpdatePhaseComponent.InsideInteractableIntervalThatCanScoreBest),
// 退出1级判定区间
AfterInteractableScoreInterval
= UpdatePhase.AfterInteractableIntervalThatCanScoreBest & (~UpdatePhaseComponent.InsideInteractableScoreInterval),
// 退出2级判断区间
AfterInteractiveDuration
= UpdatePhase.AfterInteractableScoreInterval & (~UpdatePhaseComponent.InsideInteractiveDuration),
// 退出3级判断区间, 已不可视
AfterVisibleDuration
= UpdatePhase.AfterInteractiveDuration & (~UpdatePhaseComponent.InsideVisibleDuration)
}
public enum JudgementLevel
{
None = -2,//No Judge
Default = -1,
BestLevel = 0,//Level0
ScoreLevel = 1,//Level1
Bad = 128,
}
[Header("Judgement")]
[Content, SerializeField] private JudgementLevel MyJudgementLevel = JudgementLevel.None;
[Content, SerializeField] private UpdatePhase MyUpdatePhase = default;
[Content, SerializeField] private float BestJudgementTimePoint = -1;
[Content, SerializeField] public UnityEvent<JudgementLevel> JudgementEvent = new();
@@ -67,14 +117,14 @@ namespace Demo.Game
return BestJudgementTimePoint;
}
public bool IsInInteractiveDuration()
{
return InteractiveDurationStats == DurationStats.BeforeEnd;
}
public bool IsInInteractiveDuration(float time)
{
return time > InteractiveDuration.x && time < InteractiveDuration.y;
}
public bool IsInInteractiveDuration()
{
return IsInInteractiveDuration(MyCacheUpdateTick);
}
public const int JudgementLevelCount = 3;
@@ -105,92 +155,126 @@ namespace Demo.Game
{
if (level == JudgementLevel.None)
return;
// 进入已判定状态
MyJudgementLevel = level;
JudgementEvent.Invoke(level);
VisibleDurationStats = DurationStats.Judgement;
VisibleDurationEndEvent.Invoke();
InteractiveDurationStats = DurationStats.Judgement;
InteractableScoreIntervalStats = DurationStats.Judgement;
InteractableIntervalThatCanScoreBestStats = DurationStats.Judgement;
InteractiveDurationEndEvent.Invoke();
InteractableScoreIntervalEndEvent.Invoke();
InteractableIntervalThatCanScoreBestEndEvent.Invoke();
}
private static DurationStats UpdateDurationStats(float begin, UnityEvent beforeEvent, UnityEvent beginEvent, float end, UnityEvent endEvent,
float current, DurationStats currentStats)
{
if (currentStats == DurationStats.After)
return DurationStats.After;
else if (current < begin)
// 触发还未触发的退出事件
{
if (currentStats != DurationStats.BeforeBegin)
beforeEvent.Invoke();
return DurationStats.BeforeBegin;
if (((int)MyUpdatePhase & (int)UpdatePhaseComponent.EnterInteractableIntervalThatCanScoreBest) != 0)
{
InteractableIntervalThatCanScoreBestEndEvent.Invoke();
}
if (((int)MyUpdatePhase & (int)UpdatePhaseComponent.EnterInteractableScoreInterval) != 0)
{
InteractableScoreIntervalEndEvent.Invoke();
}
if (((int)MyUpdatePhase & (int)UpdatePhaseComponent.EnterInteractiveDuration) != 0)
{
InteractiveDurationEndEvent.Invoke();
}
if (((int)MyUpdatePhase & (int)UpdatePhaseComponent.EnterVisibleDuration) != 0)
{
VisibleDurationEndEvent.Invoke();
}
}
else if (current < end)
{
if (currentStats == DurationStats.BeforeBegin)
beginEvent.Invoke();
return DurationStats.BeforeEnd;
}
else if (currentStats == DurationStats.BeforeEnd)
{
endEvent.Invoke();
}
return DurationStats.After;
}
public enum JudgementLevel
{
None = -2,//No Judge
Default = -1,
BestLevel = 0,//Level0
ScoreLevel = 1,//Level1
Bad = 128,
}
public abstract JudgementLevel JudgementBehaviour(float timePoint);
#region Update Judgement Stats
private void DoUpdateJudgementStats(float currentTime)
{
// 可见区间
VisibleDurationStats = UpdateDurationStats(VisibleDuration.x, VisibleDurationBeforeEvent,
VisibleDurationBeginEvent, VisibleDuration.y, VisibleDurationEndEvent,
currentTime, VisibleDurationStats);
if (VisibleDurationStats != DurationStats.BeforeEnd)
return;
// 可判定区间
InteractiveDurationStats = UpdateDurationStats(InteractiveDuration.x, InteractiveDurationBeforeEvent,
InteractiveDurationBeginEvent, InteractiveDuration.y, InteractiveDurationEndEvent,
currentTime, InteractiveDurationStats);
if (InteractiveDurationStats != DurationStats.BeforeEnd)
return;
// 1级判定区间
InteractableScoreIntervalStats = UpdateDurationStats(InteractableScoreInterval.x, InteractableScoreIntervalBeforeEvent,
InteractableScoreIntervalBeginEvent, InteractableScoreInterval.y, InteractableScoreIntervalEndEvent,
currentTime, InteractableScoreIntervalStats);
if (InteractableScoreIntervalStats != DurationStats.BeforeEnd)
return;
// 0级判定区间
InteractableIntervalThatCanScoreBestStats = UpdateDurationStats(InteractableIntervalThatCanScoreBest.x, InteractableIntervalThatCanScoreBestBeforeEvent,
InteractableIntervalThatCanScoreBestBeginEvent, InteractableIntervalThatCanScoreBest.y, InteractableIntervalThatCanScoreBestEndEvent,
currentTime, InteractableIntervalThatCanScoreBestStats);
// 重置
if (MyCacheUpdateTick > currentTime)
{
MyUpdatePhase = UpdatePhase.BeforeVisibleDuration;
VisibleDurationBeforeEvent.Invoke();
InteractiveDurationBeforeEvent.Invoke();
InteractableScoreIntervalBeforeEvent.Invoke();
InteractableIntervalThatCanScoreBestBeforeEvent.Invoke();
MyJudgementLevel = JudgementLevel.None;
}
// 状态更新
{
// 在当前区间进行判断, 当前时间落入下一个区间时, 就进入下一个区间
switch (MyUpdatePhase)
{
case UpdatePhase.BeforeVisibleDuration:
if (currentTime > VisibleDuration.x)
{
MyUpdatePhase = UpdatePhase.EnterVisibleDuration;
VisibleDurationBeginEvent.Invoke();
}
break;
case UpdatePhase.EnterVisibleDuration:
if (currentTime > InteractiveDuration.x)
{
MyUpdatePhase = UpdatePhase.EnterInteractiveDuration;
InteractiveDurationBeginEvent.Invoke();
}
break;
case UpdatePhase.EnterInteractiveDuration:
if (currentTime > InteractableScoreInterval.x)
{
MyUpdatePhase = UpdatePhase.EnterInteractableScoreInterval;
InteractableScoreIntervalBeginEvent.Invoke();
}
break;
case UpdatePhase.EnterInteractableScoreInterval:
if (currentTime > InteractableIntervalThatCanScoreBest.x)
{
MyUpdatePhase = UpdatePhase.EnterInteractableIntervalThatCanScoreBest;
InteractableIntervalThatCanScoreBestBeginEvent.Invoke();
}
break;
// ---------------------------------------------------------
case UpdatePhase.EnterInteractableIntervalThatCanScoreBest:
if (currentTime > InteractableIntervalThatCanScoreBest.y)
{
MyUpdatePhase = UpdatePhase.AfterInteractableIntervalThatCanScoreBest;
InteractableIntervalThatCanScoreBestEndEvent.Invoke();
}
break;
case UpdatePhase.AfterInteractableIntervalThatCanScoreBest:
if (currentTime > InteractableScoreInterval.y)
{
MyUpdatePhase = UpdatePhase.AfterInteractableScoreInterval;
InteractableScoreIntervalEndEvent.Invoke();
}
break;
case UpdatePhase.AfterInteractableScoreInterval:
if (currentTime > InteractiveDuration.y)
{
MyUpdatePhase = UpdatePhase.AfterInteractiveDuration;
InteractiveDurationEndEvent.Invoke();
}
break;
case UpdatePhase.AfterInteractiveDuration:
if (currentTime > VisibleDuration.y)
{
MyUpdatePhase = UpdatePhase.AfterVisibleDuration;
VisibleDurationEndEvent.Invoke();
}
break;
default:
break;
}
}
// 记录
MyCacheUpdateTick = currentTime;
}
public override void ResetEnterGameStatus()
{
base.ResetEnterGameStatus();
DoUpdateJudgementStats(Mathf.NegativeInfinity);
}
#endregion
protected override void UpdateTicks(float currentTime, float deltaTime, TickType tickType)
{
base.UpdateTicks(currentTime, deltaTime, tickType);
// 预判断
if (VisibleDurationStats == DurationStats.Judgement)
return;
// 检定
InvokeJudgement(JudgementBehaviour(currentTime));
if (MyJudgementLevel == JudgementLevel.None)
{
InvokeJudgement(JudgementBehaviour(currentTime));
}
// 更新状态
DoUpdateJudgementStats(currentTime);
}

View File

@@ -33,6 +33,10 @@ namespace Demo.Game
public abstract Vector3 EvaluateClipToPosition(float time);
public abstract SplineSample EvaluateClipFrom(float time);
public abstract SplineSample EvaluateClipTo(float time);
public override IEnumerator LoadScript(string script)
{
yield return base.LoadScript(script);
@@ -188,5 +192,15 @@ namespace Demo.Game
{
return MySplineCore.MySplineComputer.EvaluatePosition(Evaluate(time).ClipTo);
}
public override SplineSample EvaluateClipFrom(float time)
{
return MySplineCore.MySplineComputer.Evaluate(Evaluate(time).ClipFrom);
}
public override SplineSample EvaluateClipTo(float time)
{
return MySplineCore.MySplineComputer.Evaluate(Evaluate(time).ClipTo);
}
}
}

View File

@@ -50,23 +50,52 @@ namespace Demo.Game
/// <summary>
/// 绑定到样条线渲染器上(必须已经加载),
/// 并设置位置为指定时间的时刻渲染器所生成的头部位置
/// 并设置跟随指定时间的时刻渲染器所生成的头部
/// </summary>
/// <param name="path">对象路径, 不存在时则立刻加载</param>
/// <param name="time">时刻</param>
/// <param name="isFollowPosition">是否跟随位置, 默认开启</param>
/// <param name="isFollowRotation">是否跟随旋转, 默认开启</param>
[ScriptableCall(@"
<summary>
绑定到样条线渲染器上(必须已经加载),
并设置位置为指定时间的时刻渲染器所生成的头部位置
并设置跟随指定时间的时刻渲染器所生成的头部
</summary>
<param name=""path"">对象路径, 不存在时则立刻加载</param>
<param name=""time"">时刻</param>
<param name=""isFollowPosition"">是否跟随位置, 默认开启</param>
<param name=""isFollowRotation"">是否跟随旋转, 默认开启</param>
")]
public void LoadSplineRenderer(string path, string time)
public void LoadSplineRenderer(string path, string time, string isFollowPosition = "true", string isFollowRotation = "true")
{
MySplineRenderer = this.LoadSplineRendererTool(path);
MySplineOffset = Parse(time);
Updater =()=> transform.position = MySplineRenderer.EvaluateClipToPosition(MySplineOffset);
bool bIsFollowPosition = ConvertValue<bool>(isFollowPosition);
bool bIsFollowRotation = ConvertValue<bool>(isFollowRotation);
if (bIsFollowPosition && bIsFollowRotation)
{
Updater = () =>
{
var result = MySplineRenderer.EvaluateClipTo(MySplineOffset);
transform.position = result.position;
transform.rotation = result.rotation;
};
}
else if (bIsFollowPosition)
{
Updater = () =>
{
transform.position = MySplineRenderer.EvaluateClipToPosition(MySplineOffset);
};
}
else
{
Updater = () =>
{
var result = MySplineRenderer.EvaluateClipTo(MySplineOffset);
transform.rotation = result.rotation;
};
}
}
}
}