Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better animation system #834

Open
swoolcock opened this issue Oct 21, 2024 · 5 comments
Open

Better animation system #834

swoolcock opened this issue Oct 21, 2024 · 5 comments
Assignees
Labels
enhancement New feature or request

Comments

@swoolcock
Copy link
Contributor

I am planning on implementing this myself

Yes

Describe your request

The Tween class is nice and all, but it would be great to have a Unity-style AnimationClip containing multiple tweens with the option for rewinding, etc.

My rough idea is to have a builder that creates animation clips, but these could also be defined in XML for reuse and performance.

// loading a reusable animation from the animation bank
AnimationClip clip = GFX.AnimationBank["MyCoolAnimation"].Create();
// playing it
entity.Add(new Animator(clip));

Example of what a programmatic version could potentially look like:

AnimationClip clip = AnimationBuilder.Instance
    .PushUnion() // runs animations in parallel
        .Add(player.Sprite.AnimateScale(new Vector2(2, 2), duration: 4f, delay: 1f))
        .PushGroup() // runs animations in series
            .Add(player.AnimatePosition(new Vector2(100, 200), duration: 3f))
            .Add(player.AnimatePosition(new Vector2(100, 300), duration: 1f, delay: 1f))
        .Pop()
    .Pop()
    .Finish();

Creates the following animations:

var pos1 = player.AnimatePosition(new Vector2(100, 200));
var pos2 = player.AnimatePosition(new Vector2(100, 300));
var scale = player.Sprite.AnimateScale(new Vector2(2, 2));

And executes them over 5 seconds like so:

// 0-----1-----2-----3-----4-----5
//       |scale..................|
// |pos1.............|     |pos2.|

Additional context

No response

@swoolcock swoolcock added the enhancement New feature or request label Oct 21, 2024
@microlith57
Copy link
Member

overall good idea, fully support; just a few points:

  • i'd prefer PushParallel / PushSeries as it's not immediately clear what a group means
  • what kinds of things would get Animate methods? would there be eg. an Animate<T>(T start, T end, float duration, Action<T> setter) for lerpable T?
  • how would stuff like player.AnimatePosition translate to an xml format (if that's what you mean)?

@swoolcock
Copy link
Contributor Author

swoolcock commented Oct 21, 2024

Yes there would be some kind of generic version for doing arbitrary animations, as well as extension methods on common Celeste/Monocle types as shown in that example.

Also considering a reflection or DynamicData variant that will just animate any property of any object.

The XML could put object names on the animation targets, and then you assign those in the Animator before playing.

animator.SetTarget("player", player);

@swoolcock
Copy link
Contributor Author

As posted in Discord: https://discord.com/channels/403698615446536203/429775439423209472/1297904964932337738

// create an animation clip in code, this could also be GFX.AnimationBank["foo"].Create();
clip = AnimationBuilder.Instance
    .Add(new LerpAnimation<float>((v, _) => LineLength = v, 0f, 1f, 0.5f, Ease.CubeOut))
    .DelayAdd(0.5f, new LerpAnimation<float>((v, _) => LineLength = v, 1f, 0f, 1f, Ease.BounceOut))
    .BuildClip();

// triggered on key press, "true" means autoplay
Add(new Animator(clip, true));

// (and some rendering code based on LineLength)

https://cdn.discordapp.com/attachments/429775439423209472/1297904964680548393/Screen_Recording_2024-10-21_232003.mp4?ex=67179fb1&is=67164e31&hm=c60ab05196c76f70a18275bff8c1e5a1eb7b4d1ca6c7a983a3b3f33fd744e67d&

@swoolcock
Copy link
Contributor Author

This is a rough example of how I'd expect an Animations.xml to be defined/used.

<animations>
    <!--
        // Animate an arbitrary property using DynamicData
        var player = Scene.Tracker.GetEntity<Player>();
        var clip = AnimationBuilder.Instance
            .Add(new LerpPropertyAnimation<Vector2>(player, nameof(Player.Position), new Vector2(100, 100), new Vector2(200, 200), 2))
            .BuildClip();

        // Results in:
        // 0.....1.....2
        // |position...|
    -->
    <propertyExample>
        <!-- Define the targets the Animator should accept -->
        <target name="player" />
        <!-- Animations -->
        <property target="player" property="Position" from="100,100" to="200,200" duration="2" />
    </propertyExample>

    <!--
        // Animate using named helper extensions for supported targets
        var player = Scene.Tracker.GetEntity<Player>();
        var sprite = player.Sprite;
        var clip = AnimationBuilder.Instance
            .Add(player.AnimatePosition(new Vector2(100, 100), new Vector2(200, 200), 2))
            .DelayAdd(1f, sprite.AnimateScale(new Vector2(1, 1), new Vector2(2, 2), 1))
            .BuildClip();

        // Results in:
        // 0.....1.....2.....3.....4
        // |position...|     |scale|
    -->
    <extensionsExample>
        <!-- Define the targets the Animator should accept -->
        <target name="player" />
        <target name="sprite" />
        <!-- Animations default to series if a top-level group is not specified -->
        <position target="player" from="100,100" to="200,200" duration="2" />
        <scale target="sprite" from="1,1" to="2,2" duration="1" delay="1" />
    </extensionsExample>

    <!--
        // More complex example
        var player = Scene.Tracker.GetEntity<Player>();
        var sprite = player.Sprite;
        var clip = AnimationBuilder.Instance
            .PushParallel()
                .DelayAdd(1f, sprite.AnimateScale(new Vector2(1, 1), new Vector2(2, 2), 4f))
                .PushSeries()
                    .Add(player.AnimatePosition(new Vector2(100, 100), new Vector2(100, 200), 3f))
                    .DelayAdd(1f, player.AnimatePosition(new Vector2(100, 200), new Vector2(100, 300), 1f))
                .Pop()
            .Pop()
            .BuildClip();

        // Results in:
        // 0.....1.....2.....3.....4.....5
        //       |scale..................|
        // |pos1.............|     |pos2.|
    -->
    <complexExample>
        <!-- Define the targets the Animator should accept -->
        <target name="player" />
        <target name="sprite" />
        <!-- Animations -->
        <parallel>
            <scale target="sprite" from="1,1" to="2,2" duration="4" delay="1" />
            <series>
                <position target="player" from="100,100" to="100,200" duration="3" />
                <position target="player" from="100,200" to="100,300" duration="1" delay="1" />
            </series>
        </parallel>
    </complexExample>

    <!--
        // executing the above
        var clip = GFX.AnimationBank["complexExample"].Create();
        var animator = new Animator(clip);
        var player = Scene.Tracker.GetEntity<Player>();
        animator.Targets["player"] = player;
        animator.Targets["sprite"] = player.Sprite;
        Add(animator);
        animator.Rate = 0.5f; // run at half speed
        animator.Play();

        // example of waiting for animation to finish in a coroutine
        while (animator.Playing) yield return null;
    -->
</animations>

@swoolcock swoolcock self-assigned this Oct 22, 2024
@swoolcock
Copy link
Contributor Author

I've been looking through Unity's AnimationClips for some inspiration, and their clips are essentially a serial group per property of a single target object (or its children), all running in parallel. I'm trying to think of a way to cleanly introduce this in code, as well as having the ability to do arbitrary lambda tweens.

This is what I'm currently thinking:

XML:

  • Define clips that have curves for the target object and property name (this would have convenience tag names like position)
  • Curves define a start time and duration. The start/end values and easer are optional.
  • If start/end values are not supplied, a curve should contain child elements with keyframes (this is advanced stuff for pros)
<animations>
    <myclip duration="4">
        <!-- simple curves, most common ones that people will use -->
        <position target="player" offset="0" duration="2" from="0,0" to="100,100" easer="cubeout" />
        <scale target="sprite" offset="1" duration="3" from="1,1" to="2,2" easer="elasticout" />
        <!-- advanced curve, most people will never use but it's cool -->
        <property target="sprite" name="rotation" offset="0" duration="4">
            <keyframe time="0" value="0" easer="cubeout" />
            <keyframe time="0.25" value="180" easer="linear" />
            <keyframe time="0.5" value="0" easer="elasticout" />
            <keyframe time="1" value="360" />
        </property>
    </myclip>
</animations>

Runtime:

  • Create an animator that optionally accepts a clip from AnimationBank.
  • Optionally add extra curves in code, these could include arbitrary tween lambdas.
  • Populate the name/object dictionary in the animator for applying property values to entities, etc.
  • Set a playback rate, repeat type, etc.
  • Add the animator component wherever you want it.
Player player = Scene.Tracker.GetEntity<Player>();
AnimationClip clip = GFX.AnimationBank["myclip"].Create();
Animator animator = new Animator(clip);
animator.AddCurve(0, 4, Ease.BounceOut, t => { /* do something with t, which will be (0-1) */ });
animator.SetTarget("player", player);
animator.SetTarget("sprite", player.Sprite);
animator.Rate = 2f;
Add(animator);

This should cover most cases I think, and leave it open enough without the complexity of animation groups. A builder class would be unnecessary since the coder can just write a bunch of AddCurve calls like they would do with adding animations to Sprites in code. We could probably also make SetTarget accept a Func<object> so that it's not evaluated until the animator starts playing. This helps with situations where the target entity has not yet been added to the scene (or removed, if it's a dead player). We could even make this a default target so that people don't need to add it.

animator.SetTarget("player", () => Scene.Tracker.GetEntity<Player>());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants