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

