mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-06 17:58:05 +02:00
Property animations track (#867)
* WiP property animations system * Use better lerp for Angle property animations. * Fix handling of offset in sprite component. * Allow animating some sprite layer properties. * Allow animating some Transform properties. Obviously not advisable for server entities, but great for client side entities! * Improve animation property interpolation handling. Added a "previous" mode. Made values that cannot be sanely interpolated fall back to this mode. * Improve some animation docs.
This commit is contained in:
@@ -1,12 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.GameObjects.Components.Animations;
|
||||
using Robust.Client.GameObjects.EntitySystems;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Animations
|
||||
{
|
||||
@@ -23,162 +17,4 @@ namespace Robust.Client.Animations
|
||||
|
||||
public TimeSpan Length { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single track of an <see cref="Animation"/>.
|
||||
/// </summary>
|
||||
public abstract class AnimationTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// Return the values necessary to initialize a playback.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A tuple containing the new key frame the animation track is on and the new time left in said key frame.
|
||||
/// </returns>
|
||||
public abstract (int KeyFrameIndex, float FramePlayingTime) InitPlayback();
|
||||
|
||||
/// <summary>
|
||||
/// Advance this animation track's playback.
|
||||
/// </summary>
|
||||
/// <param name="context">The object this animation track is being played on, e.g. an entity.</param>
|
||||
/// <param name="prevKeyFrameIndex">The key frame this animation track is on.</param>
|
||||
/// <param name="prevPlayingTime">The amount of time this keyframe has been running.</param>
|
||||
/// <param name="frameTime">The amount of time to increase.</param>
|
||||
/// <returns>
|
||||
/// A tuple containing the new key frame the animation track is on and the current time on said key frame.
|
||||
/// </returns>
|
||||
public abstract (int KeyFrameIndex, float FramePlayingTime)
|
||||
AdvancePlayback(object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An animation track that plays RSI state animations manually, so they can be precisely controlled etc.
|
||||
/// </summary>
|
||||
public sealed class AnimationTrackSpriteFlick : AnimationTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of key frames for when to fire flicks.
|
||||
/// </summary>
|
||||
public readonly List<KeyFrame> KeyFrames = new List<KeyFrame>();
|
||||
|
||||
// TODO: Should this layer key be per keyframe maybe?
|
||||
/// <summary>
|
||||
/// The layer key of the layer to flick on.
|
||||
/// </summary>
|
||||
public object LayerKey { get; set; }
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime) InitPlayback()
|
||||
{
|
||||
return (-1, 0);
|
||||
}
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime)
|
||||
AdvancePlayback(object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime)
|
||||
{
|
||||
var entity = (IEntity) context;
|
||||
var sprite = entity.GetComponent<ISpriteComponent>();
|
||||
|
||||
var playingTime = prevPlayingTime + frameTime;
|
||||
var keyFrameIndex = prevKeyFrameIndex;
|
||||
// Advance to the correct key frame.
|
||||
while (keyFrameIndex != KeyFrames.Count - 1 && KeyFrames[keyFrameIndex + 1].KeyTime < playingTime)
|
||||
{
|
||||
playingTime -= KeyFrames[keyFrameIndex + 1].KeyTime;
|
||||
keyFrameIndex += 1;
|
||||
}
|
||||
|
||||
if (keyFrameIndex >= 0)
|
||||
{
|
||||
var keyFrame = KeyFrames[keyFrameIndex];
|
||||
// Advance animation on current key frame.
|
||||
var rsi = sprite.LayerGetActualRSI(LayerKey);
|
||||
var state = rsi[keyFrame.State];
|
||||
DebugTools.Assert(state.AnimationLength != null, "state.AnimationLength != null");
|
||||
var animationTime = Math.Min(state.AnimationLength.Value - 0.01f, playingTime);
|
||||
sprite.LayerSetAutoAnimated(LayerKey, false);
|
||||
// TODO: Doesn't setting the state explicitly reset the animation
|
||||
// so it's slightly more inefficient?
|
||||
sprite.LayerSetState(LayerKey, keyFrame.State);
|
||||
sprite.LayerSetAnimationTime(LayerKey, animationTime);
|
||||
}
|
||||
|
||||
return (keyFrameIndex, playingTime);
|
||||
}
|
||||
|
||||
public struct KeyFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// The RSI state to play when this keyframe gets triggered.
|
||||
/// </summary>
|
||||
public readonly RSI.StateId State;
|
||||
|
||||
/// <summary>
|
||||
/// The time between this keyframe and the last.
|
||||
/// </summary>
|
||||
public readonly float KeyTime;
|
||||
|
||||
public KeyFrame(RSI.StateId state, float keyTime)
|
||||
{
|
||||
State = state;
|
||||
KeyTime = keyTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An animation track that plays RSI state animations manually, so they can be precisely controlled etc.
|
||||
/// </summary>
|
||||
public sealed class AnimationTrackPlaySound : AnimationTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of key frames for when to fire flicks.
|
||||
/// </summary>
|
||||
public readonly List<KeyFrame> KeyFrames = new List<KeyFrame>();
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime) InitPlayback()
|
||||
{
|
||||
return (-1, 0);
|
||||
}
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime)
|
||||
AdvancePlayback(object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime)
|
||||
{
|
||||
var entity = (IEntity) context;
|
||||
|
||||
var playingTime = prevPlayingTime + frameTime;
|
||||
var keyFrameIndex = prevKeyFrameIndex;
|
||||
// Advance to the correct key frame.
|
||||
while (keyFrameIndex != KeyFrames.Count - 1 && KeyFrames[keyFrameIndex + 1].KeyTime < playingTime)
|
||||
{
|
||||
playingTime -= KeyFrames[keyFrameIndex + 1].KeyTime;
|
||||
keyFrameIndex += 1;
|
||||
|
||||
var keyFrame = KeyFrames[keyFrameIndex];
|
||||
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>()
|
||||
.Play(keyFrame.Resource, entity);
|
||||
}
|
||||
|
||||
return (keyFrameIndex, playingTime);
|
||||
}
|
||||
|
||||
public struct KeyFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// The RSI state to play when this keyframe gets triggered.
|
||||
/// </summary>
|
||||
public readonly string Resource;
|
||||
|
||||
/// <summary>
|
||||
/// The time between this keyframe and the last.
|
||||
/// </summary>
|
||||
public readonly float KeyTime;
|
||||
|
||||
public KeyFrame(string resource, float keyTime)
|
||||
{
|
||||
Resource = resource;
|
||||
KeyTime = keyTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Robust.Client.Animations
|
||||
{
|
||||
/// <summary>
|
||||
/// A single track of an <see cref="Animation"/>.
|
||||
/// </summary>
|
||||
public abstract class AnimationTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// Return the values necessary to initialize a playback.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A tuple containing the new key frame the animation track is on and the new time left in said key frame.
|
||||
/// </returns>
|
||||
public abstract (int KeyFrameIndex, float FramePlayingTime) InitPlayback();
|
||||
|
||||
/// <summary>
|
||||
/// Advance this animation track's playback.
|
||||
/// </summary>
|
||||
/// <param name="context">The object this animation track is being played on, e.g. an entity.</param>
|
||||
/// <param name="prevKeyFrameIndex">The key frame this animation track is on.</param>
|
||||
/// <param name="prevPlayingTime">The amount of time this keyframe has been running.</param>
|
||||
/// <param name="frameTime">The amount of time to increase.</param>
|
||||
/// <returns>
|
||||
/// A tuple containing the new key frame the animation track is on and the current time on said key frame.
|
||||
/// </returns>
|
||||
public abstract (int KeyFrameIndex, float FramePlayingTime)
|
||||
AdvancePlayback(object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Robust.Client.Animations
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class AnimationTrackComponentProperty : AnimationTrackProperty
|
||||
{
|
||||
public Type ComponentType { get; set; }
|
||||
public string Property { get; set; }
|
||||
|
||||
protected override void ApplyProperty(object context, object value)
|
||||
{
|
||||
var entity = (IEntity) context;
|
||||
var component = entity.GetComponent(ComponentType);
|
||||
|
||||
if (component is IAnimationProperties properties)
|
||||
{
|
||||
properties.SetAnimatableProperty(Property, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimationHelper.SetAnimatableProperty(component, Property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Animations
|
||||
{
|
||||
/// <summary>
|
||||
/// An animation track that plays a sound as keyframes.
|
||||
/// </summary>
|
||||
public sealed class AnimationTrackPlaySound : AnimationTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of key frames for when to fire flicks.
|
||||
/// </summary>
|
||||
public readonly List<KeyFrame> KeyFrames = new List<KeyFrame>();
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime) InitPlayback()
|
||||
{
|
||||
return (-1, 0);
|
||||
}
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime)
|
||||
AdvancePlayback(object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime)
|
||||
{
|
||||
var entity = (IEntity) context;
|
||||
|
||||
var playingTime = prevPlayingTime + frameTime;
|
||||
var keyFrameIndex = prevKeyFrameIndex;
|
||||
// Advance to the correct key frame.
|
||||
while (keyFrameIndex != KeyFrames.Count - 1 && KeyFrames[keyFrameIndex + 1].KeyTime < playingTime)
|
||||
{
|
||||
playingTime -= KeyFrames[keyFrameIndex + 1].KeyTime;
|
||||
keyFrameIndex += 1;
|
||||
|
||||
var keyFrame = KeyFrames[keyFrameIndex];
|
||||
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>()
|
||||
.Play(keyFrame.Resource, entity);
|
||||
}
|
||||
|
||||
return (keyFrameIndex, playingTime);
|
||||
}
|
||||
|
||||
public struct KeyFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// The RSI state to play when this keyframe gets triggered.
|
||||
/// </summary>
|
||||
public readonly string Resource;
|
||||
|
||||
/// <summary>
|
||||
/// The time between this keyframe and the last.
|
||||
/// </summary>
|
||||
public readonly float KeyTime;
|
||||
|
||||
public KeyFrame(string resource, float keyTime)
|
||||
{
|
||||
Resource = resource;
|
||||
KeyTime = keyTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Animations
|
||||
{
|
||||
/// <summary>
|
||||
/// Animation that changes the value of a property based on keyframes.
|
||||
/// </summary>
|
||||
public abstract class AnimationTrackProperty : AnimationTrack
|
||||
{
|
||||
public readonly List<KeyFrame> KeyFrames = new List<KeyFrame>();
|
||||
|
||||
/// <summary>
|
||||
/// How to interpolate values when between two keyframes.
|
||||
/// </summary>
|
||||
public AnimationInterpolationMode InterpolationMode { get; set; } = AnimationInterpolationMode.Linear;
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime) InitPlayback()
|
||||
{
|
||||
return (-1, 0);
|
||||
}
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback(object context,
|
||||
int prevKeyFrameIndex,
|
||||
float prevPlayingTime, float frameTime)
|
||||
{
|
||||
var playingTime = prevPlayingTime + frameTime;
|
||||
var keyFrameIndex = prevKeyFrameIndex;
|
||||
|
||||
// Advance to the correct key frame.
|
||||
while (keyFrameIndex != KeyFrames.Count - 1 && KeyFrames[keyFrameIndex + 1].KeyTime < playingTime)
|
||||
{
|
||||
playingTime -= KeyFrames[keyFrameIndex + 1].KeyTime;
|
||||
keyFrameIndex += 1;
|
||||
}
|
||||
|
||||
// Find the value we've interpolated to.
|
||||
object value;
|
||||
|
||||
var nextKeyFrame = keyFrameIndex + 1;
|
||||
if (nextKeyFrame == 0)
|
||||
{
|
||||
// Still before the first keyframe, do nothing.
|
||||
return (keyFrameIndex, playingTime);
|
||||
}
|
||||
|
||||
if (nextKeyFrame == KeyFrames.Count || InterpolationMode == AnimationInterpolationMode.Previous)
|
||||
{
|
||||
// After the last keyframe, or doing previous interpolation.
|
||||
value = KeyFrames[keyFrameIndex].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get us a scale 0 -> 1 here.
|
||||
var t = playingTime / KeyFrames[nextKeyFrame].KeyTime;
|
||||
|
||||
switch (InterpolationMode)
|
||||
{
|
||||
case AnimationInterpolationMode.Linear:
|
||||
value = InterpolateLinear(KeyFrames[keyFrameIndex].Value, KeyFrames[nextKeyFrame].Value, t);
|
||||
break;
|
||||
case AnimationInterpolationMode.Cubic:
|
||||
var pre = keyFrameIndex > 0 ? keyFrameIndex - 1 : keyFrameIndex;
|
||||
var post = nextKeyFrame < KeyFrames.Count - 1 ? nextKeyFrame + 1 : nextKeyFrame;
|
||||
|
||||
value = InterpolateCubic(KeyFrames[pre].Value, KeyFrames[keyFrameIndex].Value,
|
||||
KeyFrames[nextKeyFrame].Value, KeyFrames[post].Value, t);
|
||||
|
||||
break;
|
||||
case AnimationInterpolationMode.Nearest:
|
||||
value = t < 0.5f ? KeyFrames[keyFrameIndex].Value : KeyFrames[nextKeyFrame].Value;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
// Set the value.
|
||||
ApplyProperty(context, value);
|
||||
|
||||
return (keyFrameIndex, playingTime);
|
||||
}
|
||||
|
||||
protected abstract void ApplyProperty(object context, object value);
|
||||
|
||||
private static object InterpolateLinear(object a, object b, float t)
|
||||
{
|
||||
switch (a)
|
||||
{
|
||||
case Vector2 vector2:
|
||||
return Vector2.Lerp(vector2, (Vector2) b, t);
|
||||
case Vector3 vector3:
|
||||
return Vector3.Lerp(vector3, (Vector3) b, t);
|
||||
case Vector4 vector4:
|
||||
return Vector4.Lerp(vector4, (Vector4) b, t);
|
||||
case float f:
|
||||
return FloatMath.Lerp(f, (float) b, t);
|
||||
case double d:
|
||||
return FloatMath.Lerp(d, (double) b, t);
|
||||
case Angle angle:
|
||||
return (Angle) FloatMath.Lerp(angle, (Angle) b, t);
|
||||
case Color color:
|
||||
return Color.InterpolateBetween(color, (Color) b, t);
|
||||
case int i:
|
||||
return (int) FloatMath.Lerp((double) i, (int) b, t);
|
||||
default:
|
||||
// Fall back to "previous" interpolation, treating this as a discrete value.
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
private static object InterpolateCubic(object preA, object a, object b, object postB, float t)
|
||||
{
|
||||
switch (a)
|
||||
{
|
||||
case Vector2 vector2:
|
||||
return Vector2.InterpolateCubic((Vector2) preA, vector2, (Vector2) b, (Vector2) postB, t);
|
||||
case Vector3 vector3:
|
||||
return Vector3.InterpolateCubic((Vector3) preA, vector3, (Vector3) b, (Vector3) postB, t);
|
||||
case Vector4 vector4:
|
||||
return Vector4.InterpolateCubic((Vector4) preA, vector4, (Vector4) b, (Vector4) postB, t);
|
||||
case float f:
|
||||
return FloatMath.InterpolateCubic((float) preA, f, (float) b, (float) postB, t);
|
||||
case double d:
|
||||
return FloatMath.InterpolateCubic((double) preA, d, (double) b, (double) postB, t);
|
||||
case int i:
|
||||
return (int) FloatMath.InterpolateCubic((int) preA, (double) i, (int) b, (int) postB, t);
|
||||
default:
|
||||
// Fall back to "previous" interpolation, treating this as a discrete value.
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
public struct KeyFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// The value of the property at this keyframe.
|
||||
/// </summary>
|
||||
public readonly object Value;
|
||||
|
||||
/// <summary>
|
||||
/// The time between this keyframe and the previous.
|
||||
/// </summary>
|
||||
public readonly float KeyTime;
|
||||
|
||||
public KeyFrame(object value, float keyTime)
|
||||
{
|
||||
Value = value;
|
||||
KeyTime = keyTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Animations
|
||||
{
|
||||
/// <summary>
|
||||
/// An animation track that plays RSI state animations manually, so they can be precisely controlled etc.
|
||||
/// </summary>
|
||||
public sealed class AnimationTrackSpriteFlick : AnimationTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of key frames for when to fire flicks.
|
||||
/// </summary>
|
||||
public readonly List<KeyFrame> KeyFrames = new List<KeyFrame>();
|
||||
|
||||
// TODO: Should this layer key be per keyframe maybe?
|
||||
/// <summary>
|
||||
/// The layer key of the layer to flick on.
|
||||
/// </summary>
|
||||
public object LayerKey { get; set; }
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime) InitPlayback()
|
||||
{
|
||||
return (-1, 0);
|
||||
}
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime)
|
||||
AdvancePlayback(object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime)
|
||||
{
|
||||
var entity = (IEntity) context;
|
||||
var sprite = entity.GetComponent<ISpriteComponent>();
|
||||
|
||||
var playingTime = prevPlayingTime + frameTime;
|
||||
var keyFrameIndex = prevKeyFrameIndex;
|
||||
// Advance to the correct key frame.
|
||||
while (keyFrameIndex != KeyFrames.Count - 1 && KeyFrames[keyFrameIndex + 1].KeyTime < playingTime)
|
||||
{
|
||||
playingTime -= KeyFrames[keyFrameIndex + 1].KeyTime;
|
||||
keyFrameIndex += 1;
|
||||
}
|
||||
|
||||
if (keyFrameIndex >= 0)
|
||||
{
|
||||
var keyFrame = KeyFrames[keyFrameIndex];
|
||||
// Advance animation on current key frame.
|
||||
var rsi = sprite.LayerGetActualRSI(LayerKey);
|
||||
var state = rsi[keyFrame.State];
|
||||
DebugTools.Assert(state.AnimationLength != null, "state.AnimationLength != null");
|
||||
var animationTime = Math.Min(state.AnimationLength.Value - 0.01f, playingTime);
|
||||
sprite.LayerSetAutoAnimated(LayerKey, false);
|
||||
// TODO: Doesn't setting the state explicitly reset the animation
|
||||
// so it's slightly more inefficient?
|
||||
sprite.LayerSetState(LayerKey, keyFrame.State);
|
||||
sprite.LayerSetAnimationTime(LayerKey, animationTime);
|
||||
}
|
||||
|
||||
return (keyFrameIndex, playingTime);
|
||||
}
|
||||
|
||||
public struct KeyFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// The RSI state to play when this keyframe gets triggered.
|
||||
/// </summary>
|
||||
public readonly RSI.StateId State;
|
||||
|
||||
/// <summary>
|
||||
/// The time between this keyframe and the last.
|
||||
/// </summary>
|
||||
public readonly float KeyTime;
|
||||
|
||||
public KeyFrame(RSI.StateId state, float keyTime)
|
||||
{
|
||||
State = state;
|
||||
KeyTime = keyTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,9 @@ using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Interfaces.Reflection;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -41,7 +43,7 @@ namespace Robust.Client.GameObjects
|
||||
/// <summary>
|
||||
/// Z-index for drawing.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public DrawDepth DrawDepth
|
||||
{
|
||||
get => drawDepth;
|
||||
@@ -53,7 +55,8 @@ namespace Robust.Client.GameObjects
|
||||
/// <summary>
|
||||
/// A scale applied to all layers.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[Animatable]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Vector2 Scale
|
||||
{
|
||||
get => scale;
|
||||
@@ -62,6 +65,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private Angle rotation;
|
||||
|
||||
[Animatable]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Angle Rotation
|
||||
{
|
||||
@@ -74,7 +78,8 @@ namespace Robust.Client.GameObjects
|
||||
/// <summary>
|
||||
/// Offset applied to all layers.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[Animatable]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Vector2 Offset
|
||||
{
|
||||
get => offset;
|
||||
@@ -83,6 +88,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private Color color = Color.White;
|
||||
|
||||
[Animatable]
|
||||
[ViewVariables]
|
||||
public Color Color
|
||||
{
|
||||
@@ -1044,7 +1050,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
var mOffset = Matrix3.CreateTranslation(Offset);
|
||||
var mRotation = Matrix3.CreateRotation(angle);
|
||||
Matrix3.Multiply(ref mOffset, ref mRotation, out transform);
|
||||
Matrix3.Multiply(ref mRotation, ref mOffset, out transform);
|
||||
|
||||
var worldTransform = Owner.Transform.WorldMatrix;
|
||||
transform.Multiply(ref worldTransform);
|
||||
@@ -1578,5 +1584,34 @@ namespace Robust.Client.GameObjects
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void IAnimationProperties.SetAnimatableProperty(string name, object value)
|
||||
{
|
||||
if (!name.StartsWith("layer/"))
|
||||
{
|
||||
AnimationHelper.SetAnimatableProperty(this, name, value);
|
||||
return;
|
||||
}
|
||||
|
||||
var delimiter = name.IndexOf("/", 6, StringComparison.Ordinal);
|
||||
var indexString = name.Substring(6, delimiter - 6);
|
||||
var index = int.Parse(indexString, CultureInfo.InvariantCulture);
|
||||
var layerProp = name.Substring(delimiter+1);
|
||||
|
||||
switch (layerProp)
|
||||
{
|
||||
case "texture":
|
||||
LayerSetTexture(index, (string) value);
|
||||
return;
|
||||
case "state":
|
||||
LayerSetState(index, (string) value);
|
||||
return;
|
||||
case "color":
|
||||
LayerSetColor(index, (Color) value);
|
||||
return;
|
||||
default:
|
||||
throw new ArgumentException($"Unknown layer property '{layerProp}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Shaders;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -9,7 +10,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Interfaces.GameObjects.Components
|
||||
{
|
||||
public interface ISpriteComponent : IComponent
|
||||
public interface ISpriteComponent : IComponent, IAnimationProperties
|
||||
{
|
||||
void FrameUpdate(float delta);
|
||||
|
||||
@@ -23,21 +24,25 @@ namespace Robust.Client.Interfaces.GameObjects.Components
|
||||
/// <summary>
|
||||
/// A scale applied to all layers.
|
||||
/// </summary>
|
||||
[Animatable]
|
||||
Vector2 Scale { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A rotation applied to all layers.
|
||||
/// </summary>
|
||||
[Animatable]
|
||||
Angle Rotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Offset applied to all layers.
|
||||
/// </summary>
|
||||
[Animatable]
|
||||
Vector2 Offset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Color to multiply all layers with.
|
||||
/// </summary>
|
||||
[Animatable]
|
||||
Color Color { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -16,18 +16,18 @@
|
||||
<PackageReference Include="CommandLineParser" Version="2.6.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.0.121" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
|
||||
<PackageReference Include="NVorbis" Version="0.8.6" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.2.0" />
|
||||
<PackageReference Include="NVorbis" Version="0.8.6" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="NJsonSchema" Version="10.0.23" />
|
||||
<PackageReference Include="NJsonSchema" Version="10.0.23" />
|
||||
<PackageReference Include="SixLabors.Core" Version="1.0.0-beta0007" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0006" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0006" />
|
||||
<PackageReference Include="System.Memory" Version="4.5.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0006" />
|
||||
<PackageReference Include="System.Memory" Version="4.5.3" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="6.1.2" />
|
||||
<PackageReference Include="OpenTK" Version="3.1.0" />
|
||||
<PackageReference Include="SharpFont" Version="4.0.1" />
|
||||
<PackageReference Include="OpenTK" Version="3.1.0" />
|
||||
<PackageReference Include="SharpFont" Version="4.0.1" />
|
||||
<!-- Including SharpFont.Dependencies to silence a warning -->
|
||||
<PackageReference Include="SharpFont.Dependencies" Version="2.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -72,6 +72,18 @@ namespace Robust.Shared.Maths
|
||||
return a + (b - a) * blend;
|
||||
}
|
||||
|
||||
public static float InterpolateCubic(float preA, float a, float b, float postB, float t)
|
||||
{
|
||||
return a + 0.5f * t *
|
||||
(b - preA + t * (2.0f * preA - 5.0f * a + 4.0f * b - postB + t * (3.0f * (a - b) + postB - preA)));
|
||||
}
|
||||
|
||||
public static double InterpolateCubic(double preA, double a, double b, double postB, double t)
|
||||
{
|
||||
return a + 0.5 * t *
|
||||
(b - preA + t * (2.0 * preA - 5.0 * a + 4.0 * b - postB + t * (3.0 * (a - b) + postB - preA)));
|
||||
}
|
||||
|
||||
// Clamps value between 0 and 1 and returns value
|
||||
public static float Clamp01(float value)
|
||||
{
|
||||
|
||||
@@ -250,6 +250,13 @@ namespace Robust.Shared.Maths
|
||||
return Lerp(a, b, factor);
|
||||
}
|
||||
|
||||
public static Vector2 InterpolateCubic(Vector2 preA, Vector2 a, Vector2 b, Vector2 postB, float t)
|
||||
{
|
||||
return a +
|
||||
(b - preA + (preA * 2.0f - a * 5.0f + b * 4.0f - postB + ((a - b) * 3.0f + postB - preA) * t) * t) *
|
||||
t * 0.5f;
|
||||
}
|
||||
|
||||
public void Deconstruct(out float x, out float y)
|
||||
{
|
||||
x = X;
|
||||
|
||||
@@ -609,6 +609,13 @@ namespace Robust.Shared.Maths
|
||||
result.Z = blend * (b.Z - a.Z) + a.Z;
|
||||
}
|
||||
|
||||
public static Vector3 InterpolateCubic(Vector3 preA, Vector3 a, Vector3 b, Vector3 postB, float t)
|
||||
{
|
||||
return a +
|
||||
(b - preA + (preA * 2.0f - a * 5.0f + b * 4.0f - postB + ((a - b) * 3.0f + postB - preA) * t) * t) *
|
||||
t * 0.5f;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Barycentric
|
||||
|
||||
@@ -586,6 +586,13 @@ namespace Robust.Shared.Maths
|
||||
result.W = blend * (b.W - a.W) + a.W;
|
||||
}
|
||||
|
||||
public static Vector4 InterpolateCubic(Vector4 preA, Vector4 a, Vector4 b, Vector4 postB, float t)
|
||||
{
|
||||
return a +
|
||||
(b - preA + (preA * 2.0f - a * 5.0f + b * 4.0f - postB + ((a - b) * 3.0f + postB - preA) * t) * t) *
|
||||
t * 0.5f;
|
||||
}
|
||||
|
||||
#endregion Lerp
|
||||
|
||||
#region Barycentric
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Shared.Animations
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies that a property can be animated, or that a method can be called by animations.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)]
|
||||
public sealed class AnimatableAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Shared.Animations
|
||||
{
|
||||
public static class AnimationHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets properties marked with <see cref="AnimatableAttribute"/> on an object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does not use <see cref="IAnimationProperties"/>.
|
||||
/// </remarks>
|
||||
/// <param name="target">The object to set the property on.</param>
|
||||
/// <param name="name">The name of the property to set.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown if the property does not exist or does not have <see cref="AnimatableAttribute"/>.
|
||||
/// </exception>
|
||||
public static void SetAnimatableProperty(object target, string name, object value)
|
||||
{
|
||||
var property = target.GetType().GetProperty(name);
|
||||
if (property == null)
|
||||
{
|
||||
throw new ArgumentException($"Animatable property with name '{name}' does not exist.");
|
||||
}
|
||||
|
||||
if (!Attribute.IsDefined(property, typeof(AnimatableAttribute)))
|
||||
{
|
||||
throw new ArgumentException($"Animatable property with name '{name}' does not exist.");
|
||||
}
|
||||
|
||||
property.SetValue(target, value);
|
||||
}
|
||||
|
||||
public static void CallAnimatableMethod(object target, string name, object[] arguments)
|
||||
{
|
||||
var method = target.GetType().GetMethod(name);
|
||||
if (method == null)
|
||||
{
|
||||
throw new ArgumentException($"Animatable method with name '{name}' does not exist.");
|
||||
}
|
||||
|
||||
if (!Attribute.IsDefined(method, typeof(AnimatableAttribute)))
|
||||
{
|
||||
throw new ArgumentException($"Animatable method with name '{name}' does not exist.");
|
||||
}
|
||||
|
||||
method.Invoke(target, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Robust.Shared.Animations
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies how animated properties are interpolated between two keyframes.
|
||||
/// </summary>
|
||||
public enum AnimationInterpolationMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Use a linear interpolation for supported values.
|
||||
/// For unsupported values, this falls back to <see cref="Previous"/>.
|
||||
/// </summary>
|
||||
Linear,
|
||||
|
||||
/// <summary>
|
||||
/// Use a cubic interpolation for supported values.
|
||||
/// For unsupported values, this falls back to <see cref="Previous"/>.
|
||||
/// </summary>
|
||||
Cubic,
|
||||
|
||||
/// <summary>
|
||||
/// Use nearest neighbor as interpolation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Nearest neighbor discretely flips between the previous and next keyframe 50% in between the two.
|
||||
/// </remarks>
|
||||
Nearest,
|
||||
|
||||
/// <summary>
|
||||
/// Use the previous keyframe as value.
|
||||
/// </summary>
|
||||
Previous
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Robust.Shared.Animations
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies that this object has special animation properties
|
||||
/// that are not able to be represented with <see cref="AnimatableAttribute"/>.
|
||||
/// </summary>
|
||||
public interface IAnimationProperties
|
||||
{
|
||||
void SetAnimatableProperty(string name, object value);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects.EntitySystemMessages;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
@@ -72,6 +73,7 @@
|
||||
|
||||
/// <inheritdoc />
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Animatable]
|
||||
public Angle LocalRotation
|
||||
{
|
||||
get => GetLocalRotation();
|
||||
@@ -272,6 +274,7 @@
|
||||
public MapCoordinates MapPosition => new MapCoordinates(WorldPosition, MapID);
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Animatable]
|
||||
public Vector2 LocalPosition
|
||||
{
|
||||
get => GetLocalPosition();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Network;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.Reflection;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -17,6 +18,7 @@ namespace Robust.Shared.Interfaces.GameObjects.Components
|
||||
/// Local offset of this entity relative to its parent
|
||||
/// (<see cref="Parent"/> if it's not null, to <see cref="GridID"/> otherwise).
|
||||
/// </summary>
|
||||
[Animatable]
|
||||
Vector2 LocalPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -38,6 +40,7 @@ namespace Robust.Shared.Interfaces.GameObjects.Components
|
||||
/// <summary>
|
||||
/// Current rotation offset of the entity.
|
||||
/// </summary>
|
||||
[Animatable]
|
||||
Angle LocalRotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user