From 86290ff2356ac42da16acf64f64f9fbd785b14cb Mon Sep 17 00:00:00 2001 From: TipeSor Date: Wed, 30 Sep 2026 19:01:06 +0300 Subject: [PATCH] feat(tweens): struct support --- SharpEngine.Core/Utils/Tween/TweenData.cs | 119 +++---- SharpEngine.Core/Utils/Tween/TweenStep.cs | 408 +++++++++++++++++----- Testing/MyScene.cs | 15 +- 3 files changed, 372 insertions(+), 170 deletions(-) diff --git a/SharpEngine.Core/Utils/Tween/TweenData.cs b/SharpEngine.Core/Utils/Tween/TweenData.cs index 5953f1a..c2a4bfa 100644 --- a/SharpEngine.Core/Utils/Tween/TweenData.cs +++ b/SharpEngine.Core/Utils/Tween/TweenData.cs @@ -1,84 +1,65 @@ -using SharpEngine.Core.Manager; using System; -using System.Linq.Expressions; -using System.Reflection; using JetBrains.Annotations; namespace SharpEngine.Core.Utils.Tween { - internal class TweenData( - object obj, - Expression> property, - TD from, - TD to, - float duration, - bool useFrom = true) - where TD : notnull - { - [UsedImplicitly] - public object Obj { get; set; } = obj; + public abstract class ITweenData + { + public abstract void Launch(); + public abstract void Update(float t); + } - [UsedImplicitly] - public Expression> Property { get; set; } = property; + public abstract class TweenData : ITweenData + { + [UsedImplicitly] + public Func Getter { get; set; } = default!; - [UsedImplicitly] - public TD From { get; set; } = from; + [UsedImplicitly] + public Action Setter { get; set; } = default!; - [UsedImplicitly] - public TD To { get; set; } = to; + [UsedImplicitly] + public T From { get; set; } = default!; - [UsedImplicitly] - public float Duration { get; set; } = duration; + [UsedImplicitly] + public T To { get; set; } = default!; - [UsedImplicitly] - public TD CurrentValue { get; set; } = default!; - [UsedImplicitly] - public TDStep Step { get; set; } = default!; - [UsedImplicitly] - public bool UseFrom { get; set; } = useFrom; + internal bool UseCurrentValue { get; set; } - public void Launch() - { - CurrentValue = UseFrom ? From : Property.Compile()(Obj); - Step = CurrentValue switch - { - float fFrom when To is float fTo => (TDStep)(object)((fTo - fFrom) / Duration), - int iFrom when To is int iTo => (TDStep)(object)((iTo - iFrom) / Duration), - Color cFrom when To is Color cTo => (TDStep)(object)new ColorStep( - (short)((cTo.R - cFrom.R) / Duration), - (short)((cTo.G - cFrom.G) / Duration), - (short)((cTo.B - cFrom.B) / Duration), - (short)((cTo.A - cFrom.A) / Duration) - ), - _ => throw new NotSupportedException("Unsupported type for tweening.") - }; + public sealed override void Launch() + { + if (UseCurrentValue) + From = Getter(); + } + } - DebugManager.Log(LogLevel.Debug, $"Tween Launched: From = {From}, To = {To}, Duration = {Duration}, Step = {Step}"); - } + internal sealed class IntTweenData : TweenData + { + public override void Update(float t) + { + Setter(t >= 1f ? To : (int)(From + ((double)To - From) * t)); + } + } - public void Update(float deltaTime) - { - if (Duration <= 0) - { - ((PropertyInfo)((MemberExpression)Property.Body).Member).SetValue(Obj, To); - return; - } + internal sealed class FloatTweenData : TweenData + { + public override void Update(float t) + { + Setter(t >= 1f ? To : From + (To - From) * t); + } + } - CurrentValue = CurrentValue switch - { - float cVal when Step is float step => (TD)(object)(cVal + step * deltaTime), - int cVal when Step is int step => (TD)(object)(cVal + (int)(step * deltaTime)), - Color cVal when Step is ColorStep step => (TD)(object)new Color( - (byte)(cVal.R + step.RStep * deltaTime), - (byte)(cVal.G + step.GStep * deltaTime), - (byte)(cVal.B + step.BStep * deltaTime), - (byte)(cVal.A + step.AStep * deltaTime) - ), - _ => CurrentValue - }; - - ((PropertyInfo)((MemberExpression)Property.Body).Member).SetValue(Obj, CurrentValue); - Duration -= deltaTime; - } - } + internal sealed class ColorTweenData : TweenData + { + public override void Update(float t) + { + Setter( + new Color( + (int)(From.R + (To.R - From.R) * t), + (int)(From.G + (To.G - From.G) * t), + (int)(From.B + (To.B - From.B) * t), + (int)(From.A + (To.A - From.A) * t) + ) + ); + } + } } diff --git a/SharpEngine.Core/Utils/Tween/TweenStep.cs b/SharpEngine.Core/Utils/Tween/TweenStep.cs index 2d5d581..8836187 100644 --- a/SharpEngine.Core/Utils/Tween/TweenStep.cs +++ b/SharpEngine.Core/Utils/Tween/TweenStep.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq.Expressions; +using System.Reflection; using JetBrains.Annotations; namespace SharpEngine.Core.Utils.Tween; @@ -13,97 +14,316 @@ namespace SharpEngine.Core.Utils.Tween; /// animated properties. public class TweenStep { - /// - /// Duration of the step - /// - [UsedImplicitly] - public float CurrentDuration { get; set; } - - private readonly List> _floatTweens = []; - private readonly List> _intTweens = []; - private readonly List> _colorTweens = []; - - /// - /// Initializes a new instance of the TweenStep class with the specified duration. - /// - /// The duration, in seconds, for this tween step. Must be greater than or equal to zero. - public TweenStep(float duration) - { - CurrentDuration = duration; - } - - /// - /// Add a float tween to the step - /// - /// Entity - /// Property which be modified - /// Target Value - /// Duration of the tween - /// Source Value - /// Tween Step - public TweenStep Float(object entity, Expression> property, float to, float duration, float? from = null) - { - _floatTweens.Add(new TweenData(entity, property, from ?? 0, to, duration, from is not null)); - return this; - } - - /// - /// Add an int tween to the step - /// - /// Entity - /// Property which be modified - /// Target Value - /// Duration of the tween - /// Source Value - /// Tween Steps - [UsedImplicitly] - public TweenStep Int(object entity, Expression> property, int to, float duration, int? from = null) - { - _intTweens.Add(new TweenData(entity, property, from ?? 0, to, duration, from is not null)); - return this; - } - - /// - /// Animates a color property of the specified entity from a starting value to a target value over the given - /// duration. - /// - /// The entity whose color property will be animated. - /// An expression that selects the color property of the entity to animate. - /// The target color value to animate to. - /// The duration, in seconds, over which the animation occurs. Must be greater than zero. - /// The initial color value to animate from. If null, the current value of the property is used. - /// The current instance, allowing for method chaining. - [UsedImplicitly] - public TweenStep Color(object entity, Expression> property, Color to, float duration, Color? from = null) - { - _colorTweens.Add(new TweenData(entity, property, from ?? Utils.Color.White, to, duration, from is not null)); - return this; - } - - /// - /// Update the tween step - /// - /// Delta time - /// If a step ended - public bool Update(float deltaTime) - { - CurrentDuration -= deltaTime; - foreach (var floatTween in _floatTweens) - floatTween.Update(deltaTime); - foreach (var intTween in _intTweens) - intTween.Update(deltaTime); - foreach (var colorTween in _colorTweens) - colorTween.Update(deltaTime); - return CurrentDuration <= 0f; - } - - internal void Launch() - { - foreach (var floatTween in _floatTweens) - floatTween.Launch(); - foreach (var intTween in _intTweens) - intTween.Launch(); - foreach (var colorTween in _colorTweens) - colorTween.Launch(); - } + /// + /// Duration of the step + /// + [UsedImplicitly] + public float Duration { get; set; } + + /// + /// Elapsed time of the step + /// + [UsedImplicitly] + public float Elapsed { get; set; } + + private readonly List<(ITweenData Data, float Duration)> _tweens = []; + + /// + /// Initializes a new instance of the TweenStep class with the specified duration. + /// + /// The duration, in seconds, for this tween step. Must be greater than or equal to zero. + public TweenStep(float duration) + { + Duration = duration; + Elapsed = 0; + } + + /// + /// Add a float tween to the step + /// + /// Entity + /// Property which be modified + /// Target Value + /// Duration of the tween + /// Source Value + /// Tween Step + public TweenStep Float( + T entity, + Expression> property, + float to, + float duration, + float? from = null + ) + where T : class + { + return from.HasValue + ? Custom(entity, property, from.Value, to, duration) + : Custom(entity, property, to, duration); + } + + /// + /// Add an int tween to the step + /// + /// Entity + /// Property which be modified + /// Target Value + /// Duration of the tween + /// + /// Source Value + /// Tween Steps + [UsedImplicitly] + public TweenStep Int( + T entity, + Expression> property, + int to, + float duration, + int? from = null + ) + where T : class + { + return from.HasValue + ? Custom(entity, property, from.Value, to, duration) + : Custom(entity, property, to, duration); + } + + /// + /// Animates a color property of the specified entity from a starting value to a target value over the given + /// duration. + /// + /// The entity whose color property will be animated. + /// An expression that selects the color property of the entity to animate. + /// The target color value to animate to. + /// The duration, in seconds, over which the animation occurs. Must be greater than zero. + /// The initial color value to animate from. If null, the current value of the property is used. + /// The current instance, allowing for method chaining. + [UsedImplicitly] + public TweenStep Color( + T entity, + Expression> property, + Color to, + float duration, + Color? from = null + ) + where T : class + { + return from.HasValue + ? Custom(entity, property, from.Value, to, duration) + : Custom(entity, property, to, duration); + } + + public TweenStep Custom( + TEntity entity, + Expression> property, + T to, + float duration + ) + where TEntity : class + where TData : TweenData, new() + { + var compiledGetter = property.Compile(); + var compiledSetter = CompileSetter(property); + Func getter = () => compiledGetter(entity); + Action setter = value => compiledSetter(entity, value); + + return Custom(getter, setter, default!, to, duration, useCurrentValue: true); + } + + public TweenStep Custom( + TEntity entity, + Expression> property, + T from, + T to, + float duration + ) + where TEntity : class + where TData : TweenData, new() + { + var compiledGetter = property.Compile(); + var compiledSetter = CompileSetter(property); + Func getter = () => compiledGetter(entity); + Action setter = value => compiledSetter(entity, value); + + return Custom(getter, setter, from, to, duration); + } + + private TweenStep Custom( + Func getter, + Action setter, + T from, + T to, + float duration, + bool useCurrentValue = false + ) + where TData : TweenData, new() + { + _tweens.Add( + ( + new TData + { + Getter = getter, + Setter = setter, + From = from, + To = to, + UseCurrentValue = useCurrentValue, + }, + duration + ) + ); + return this; + } + + /// + /// Update the tween step + /// + /// Delta time + /// If a step ended + public bool Update(float deltaTime) + { + Elapsed += deltaTime; + foreach (var tween in _tweens) + { + float progress = + tween.Duration <= 0 ? 1f : System.Math.Clamp(Elapsed / tween.Duration, 0f, 1f); + tween.Data.Update(progress); + } + + return Elapsed >= Duration; + } + + internal void Launch() + { + Elapsed = 0; + foreach (var tween in _tweens) + tween.Data.Launch(); + } + + private static Action CompileSetter( + Expression> expression + ) + where T : class + { + if (expression.Body is not MemberExpression) + throw new ArgumentException( + "Expression must be a field or property access.", + nameof(expression) + ); + + // Extract: + // + // x => x.A.B.C.Value + // + // into: + // + // A, B, C, Value + + var members = new List(); + + Expression? current = expression.Body; + + while (current is MemberExpression memberExpression) + { + members.Add(memberExpression.Member); + current = memberExpression.Expression; + } + + if (current != expression.Parameters[0]) + throw new ArgumentException( + "Expression must originate from the lambda parameter.", + nameof(expression) + ); + + members.Reverse(); + + var objParameter = Expression.Parameter(typeof(T), "obj"); + + var valueParameter = Expression.Parameter(typeof(TValue), "value"); + + List variables = []; + List body = []; + + // Structs that will need to be copied back into their parents. + var writeBacks = + new List<(Expression Parent, MemberInfo Member, ParameterExpression Temp)>(); + + Expression owner = objParameter; + + // Walk everything except the final member. + for (int i = 0; i < members.Count - 1; i++) + { + var member = members[i]; + + var access = Expression.MakeMemberAccess(owner, member); + + if (access.Type.IsValueType) + { + /* + * Struct: + * + * obj.Position.X + * + * becomes roughly: + * + * var temp = obj.Position; + */ + + var temp = Expression.Variable(access.Type, $"temp{i}"); + + variables.Add(temp); + + body.Add(Expression.Assign(temp, access)); + + writeBacks.Add((owner, member, temp)); + + owner = temp; + } + else + { + // Classes don't need copying. + owner = access; + } + } + + // Final property/field. + var leafMember = members[^1]; + + EnsureWritable(leafMember); + + var leaf = Expression.MakeMemberAccess(owner, leafMember); + + body.Add(Expression.Assign(leaf, valueParameter)); + + for (int i = writeBacks.Count - 1; i >= 0; i--) + { + var writeBack = writeBacks[i]; + + EnsureWritable(writeBack.Member); + + var destination = Expression.MakeMemberAccess(writeBack.Parent, writeBack.Member); + + body.Add(Expression.Assign(destination, writeBack.Temp)); + } + + var block = Expression.Block(variables, body); + + return Expression.Lambda>(block, objParameter, valueParameter).Compile(); + } + + private static void EnsureWritable(MemberInfo member) + { + switch (member) + { + case PropertyInfo { SetMethod: null } property: + throw new ArgumentException($"Property '{property.Name}' is not writable."); + + case FieldInfo { IsInitOnly: true } field: + throw new ArgumentException($"Field '{field.Name}' is readonly."); + + case PropertyInfo: + case FieldInfo: + return; + + default: + throw new ArgumentException($"Member '{member.Name}' is not a field or property."); + } + } } diff --git a/Testing/MyScene.cs b/Testing/MyScene.cs index ff67ff7..ce1dd39 100644 --- a/Testing/MyScene.cs +++ b/Testing/MyScene.cs @@ -46,15 +46,16 @@ public override void OpenScene() { base.OpenScene(); + TransformComponent transform = Entities[0].GetComponentAs()!; Window?.TweenManager.Tweens.Add(new Tween([ new TweenStep(5) - .Float(Entities[0].GetComponentAs()!, x => ((TransformComponent)x).LocalRotation, 360, 5) - .Float(Entities[0].GetComponentAs()!.LocalPosition, x => ((Vec2)x).X, 100, 5) - .Float(Entities[0].GetComponentAs()!.LocalPosition, x => ((Vec2)x).Y, 100, 5), + .Float(transform, x => x.LocalRotation, 360, 5) + .Float(transform, x => x.LocalPosition.X, 100, 5) + .Float(transform, x => x.LocalPosition.Y, 100, 5), new TweenStep(10) - .Float(Entities[0].GetComponentAs()!, x => ((TransformComponent)x).LocalRotation, 720, 5) - .Float(Entities[0].GetComponentAs()!.LocalPosition, x => ((Vec2)x).X, 0, 5) - .Float(Entities[0].GetComponentAs()!.LocalPosition, x => ((Vec2)x).Y, 0, 5), + .Float(transform, x => x.LocalRotation, 720, 5) + .Float(transform, x => x.LocalPosition.X, 0, 5) + .Float(transform, x => x.LocalPosition.Y, 0, 5), ], () => DebugManager.Log(LogLevel.Info, "FIN DU TWEEN"))); } -} +} \ No newline at end of file