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:
Pieter-Jan Briers
2019-09-17 22:57:12 +02:00
committed by GitHub
parent 65a8d33d0c
commit 5d5b897a9b
20 changed files with 561 additions and 176 deletions
-164
View File
@@ -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>
+7 -7
View File
@@ -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>
+12
View File
@@ -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)
{
+7
View File
@@ -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;
+7
View File
@@ -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
+7
View File
@@ -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();
+2
View File
@@ -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>