Search Unity

Rhythm Game Button Prompts

Discussion in 'Timeline' started by jeshjesh, Nov 18, 2019.

  1. jeshjesh

    jeshjesh

    Joined:
    Jul 7, 2019
    Posts:
    14
    Hi all,

    I'm creating a rhythm game that syncs button prompts up to animations. I've been investigating and testing the timeline system, as I'd like to be able to place notes into a track at the same time as scrubbing through animations such that I can place them accurately in-time with animations without needing to touch any code.

    So as to not constrain the design in the early stages to a perfectly even attack and fall off, I became interested in Timeline's ability to animate values over time with curves. That brings me to my current implementation of the system. The below image represents a single note; the straight lines are the different breakpoints for the different categories of hit (miss, good, perfect). The green line represents the score over time; if the user inputs a button press, the score is simply compared to the ranges to be categorized at the frame in the graph where they registered an input.



    I was really happy with this system until it became time to figure out how to read this data and assemble the minigame at runtime. I naiively assumed getting the keyframes for this graph at runtime would be a trivial matter of nesting through to the animation clip asset, and calling a getter of some sort; teaches me to not check the API before I leap! In my research, I found this is only possible in-editor using the AnimationUtility class.

    All I really need is that center keyframe, the frame at which the score reaches its peak value. That's the value I'm going to use to arrange the score that will visually display the notes to the player in the UI.

    I am not excited about pursuing the avenue of writing an editor script to cache this keyframe at runtime, as the association between each note instance is currently separated from the generic note class where the editor script would be housed; It looks like it would take a huge .asset file with all the keys in a lookup table indexed unique IDs, or some other implementation like that. It might be possible to create an editor script to assign a value to a variable in the behavior for each note in OnGUI, but this feels more hackish than anything else, and I am not experienced enough with editor scripts to be confident in how I would implement this.

    A lazy implementation would simply to have a variable in the behavior that I manually set to the frame where the center keyframe is. But the programmer in me hates this option deeply, even if it will not really be that big of a QoL feature for the tool.

    A final option is to simply poll the animationclip at each frame as it's being loaded and write a simple max algorithm to store the frame where the score value reaches its peak, but I'd like to avoid anything that eats up O(n) runtime when it feels like caching and retrieving this value should be so easy.

    I am also very willing to refactor the system entirely if I've went down an unhelpful rabbit hole. The important thing for me is to preserve this workflow, where I can assign the various note ranges visually without touching code in the same view as scrubbing through the animation frame by frame.



    Usually I consider myself pretty able to figure out solutions myself, but this feels like a lose/lose situation to me, so I wanted to come here for expert advice and guidance. If you've read to this point, thank you very very much for lending your time, effort and expertise to help me in this issue.

    Here are the relevant classes:

    BattleTrack.cs
    Code (CSharp):
    1. [TrackClipType(typeof(Note))]
    2. [TrackBindingType(typeof(BattleManager))]
    3. public class BattleTrack : AnimationTrack {}
    BattleBehavior.cs
    Code (CSharp):
    1. public class BattleBehavior : PlayableBehaviour
    2. {
    3.     public float score = 1f;
    4.     public float missRange = 3f;
    5.     public float goodRange = 6f;
    6.     public float perfectRange = 9f;
    7.     public float noteCenter = 0f;
    8. }
    Note.cs
    Code (CSharp):
    1. public class Note : PlayableAsset
    2. {
    3.     //variables that will show up in inspector through the timeline
    4.     public BattleBehavior noteProperties;
    5.     public override double duration {get;} = .5;
    6.  
    7.     public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
    8.     {
    9.         return ScriptPlayable<BattleBehavior>.Create(graph, noteProperties);
    10.     }
    11. }
     
  2. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    The API you are looking for is the TrackAsset.curves/TimelineClip.curves property, which is the animation clip that stores the animated track data.

    One suggestion is to query the AnimationCurve from the AnimationClips in a TrackAsset.CreateTrackMixer override and assign it to the the playable asset/behaviour when you create them. That will only run when the playable graph is built, and prior to Note.CreatePlayable being called. If that is still expensive, then you could cache the values using a serialized field in the NotePlayableAsset, and only run the queries on the animation clip when not in playmode, or if it's unset.

    Also, I'd suggest overriding TrackAsset, not AnimationTrack, if you are just using scripting playables.
     
  3. jeshjesh

    jeshjesh

    Joined:
    Jul 7, 2019
    Posts:
    14
    Hey there. I am finally getting back to this after a big move, and have tried to implement your suggestion. Here's my first pass.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Timeline;
    5. using UnityEngine.Playables;
    6. using UnityEditor;
    7.  
    8. [TrackClipType(typeof(Note))]
    9. [TrackBindingType(typeof(BattleManager))]
    10. public class BattleTrack : TrackAsset {
    11.  
    12.     public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount) {
    13.         #if UNITY_EDITOR
    14.         AnimationClip clip = this.curves;
    15.         Debug.Log("Creating track mixer for: " + this.name);
    16.         Debug.Log("HasCurves?:" + this.hasCurves);
    17.         Debug.Log("Curves: " + this.curves);
    18.  
    19.         if(clip != null) {
    20.             foreach(var binding in AnimationUtility.GetCurveBindings(clip)) {
    21.                 if(binding.propertyName == "score") {
    22.                     AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding);
    23.                     foreach(Keyframe key in curve.keys) {
    24.                         Debug.Log("Keyframe at: " + key.time);
    25.                     }
    26.                 }
    27.             }
    28.         }
    29.         #endif
    30.      
    31.         return base.CreateTrackMixer(graph, go, inputCount);
    32.     }
    33. }
    My issue now is hasCurves is always false, and curves is always null, despite the timeline having these curves right there inside of the BattleTrack. Any suggestions? None of the other code has changed. Here's a screenshot of the current graph.

     
  4. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    Either a track or a clip can have curves. The code sample provided is checking for curves on the track, but the image has them on the clip.

    So, you can either make a playable behaviour on the track (similar to how the note playable asset) which will give you something similar to the recorded animation tracks, or check for the curves on each clip. e.g.

    foreach (var clip in GetClips())
    if (clip.curves != null) .....
     
  5. jeshjesh

    jeshjesh

    Joined:
    Jul 7, 2019
    Posts:
    14
    Ah, gosh, I feel really dumb. That makes so much sense. Thank you very much for pointing that out! I'll refactor accordingly and try again.