Explanation
Why would I want to check specific frames? There's several situations where checking if an animation is in a specific frame comes in handy:- If you remember my previous post about Hitboxes and Hurtboxes, one of the qualities we needed our hitboxes to have was that It should be able to check if it’s overlapping a Hurtbox in arbitrary frames.
- Suppose you are making an action game and you want to have combos. You'll probably need a way to tell if an attack animation is in a frame that can be interrupted to chain the next move. Yo wouldn't want to chain the next punch if the first one didn't reached a certain point in the animation.
- Maybe you want to have a certain range of frames marked as parryable, probably a few frames before the attack connects.
- You could have a window of oportunity after a character misses an attack where getting hit makes more damage.
Examples are endless, but you can see where this is going: More control over our animations gives us more possibilities.
The underlying logic (and code) for all the situations I mentioned will be the same, but in this post I'll center in one of those cases: I want to be able to tell an attack how many frames of startup, hit and recovery it has. Or in other words, Move Stages.
Animation, Frames and Move Stages
So what do we see when we watch a fighting or action game? Characters fighting each other, is it not obvious? You may say, and you would be correct. But if we look deeper, what we are actually seeing are animation frames. Sprites being rendered one after the other at a fixed rate (Or polygons being moved and deformed at a fixed rate if you are in a 3D enviroment).
This "fixed rate" is usually 60 or 30 frames per second for 3D games. In 2D games it depends on what the developer or artist wants: More frames per second will give you a smoother and more detailed animation, but it means more sprites need to be created for every action. Note that we are talking about animation frames and not the speed the game runs at. For a fighting game to be responsive enough to be good it should at least detect the player input at a high framerate, indepentendly of the type of animation used.
SF3 runs at 60fps but it's animated at a much lower framerate (And it looks awesome).
As your probably know, an animation is composed of frames and has a duration that we can also measure in frames. Why would we want to measure an animation in frames? because as we stated previously we want to check if our animation is playing a specific frame.
When you trigger an attack in a game it doesn't automatically hit the enemy. The character starts the animation by preparing the attack, it hits for some amount of time and then recovers from the movement. Playing with the duration of those phases is how you get "heavier" or "lighter" characters, moves or weapons.Here's an example from Dark Souls, a quick Rapier that does small amounts of damage vs. a slower Great Club that does enourmos damage.
Pay attention to the really long startup of the club
Those stages of an attack animation are called Move Stages. Or Attack Phases, or any combination of those 4 words. As always, it depends on who you ask. I'll call them Move Stages just to be consistent.
Move Stages
As we mentioned, an attack has 3 stages:
- Startup: The frames before the attack hits, where the character is setting up the move.
- Active frames: The frames where the attack is hitting and hitboxes are checking collisions.
- Recovery: The frames after the attack hits or misses, where the character recovers.
Now that we know what we want, let's get to work! (This may seem obvious but never underestimate the power of actually knowing what problem you are trying to solve)
Quick solutions
As with every problem, there's the quick way and there's the more complicated way. Note that I didn't say one is right and one is wrong. Which solution you choose should respond to what problem you have, the scope of the problem and how much time you are willing to invest to solve it. Don't let yourself be distracted of your objective because some coding guru told you that the right way to iterate an array is an incredibly performant-functional-disruptive way that would take you 2 months to implement when you had it solved and working perfectly in 5 minutes with a for loop.
Okay, rant over.
There's 2 standard ways to change script values in specific frames of an animation:
- Animating the parameters: You can simply turn hitboxes on and off or change public script values while animating your character.
- Animation Events: You can add Animation Events to any frame you like of an animation and trigger a method from a class that is in the same GameObject. Check here if you don't know what I'm talking about.
These two methods could work perfectly depending on the needs of your game, and you can implement any of them in 5 minutes, so really evaluate what you need.
The problems
- No reusability: You will have to go animation by animation modifying the values in the frames you want.
- Mixing animation and events: If you are doing everything by yourself this may not be a problem, but if you work with an artist having animation events or setting values in them could lead to mistakes when changing animations.
- If using animation events you will need to have all the code in a monolitic class that the animations can access.
The long way
To solve the problems that we mentioned, we first need to understand how animations in Unity work.
Getting to know the components
In Unity, an animation is represented by an AnimationClip. It holds the keyframe data of an animation and you can reutilize it for different characters (if the animated objects respect the same hierarchy and have the same names).
Unless you are rolling your own animation system, to actually play a clip you need an Animator Controller. An animator controller provides you with a node-based interface to handle animation states, layers, transitions, playback speed and animation blending. Here's where things can get a little complicated, but don't worry too much right now.
Let's enumerate some definitions:
- An Animation controller plays one State per layer.
- A State has an AnimationClip
- Layers are identified by a number stating at 0
- You can query the controller for an AnimatorStateInfo for a specific layer.
- An AnimatorStateInfo gives us a number between 0 and 1 that represents the progress of the current state.
- AnimatorStateInfo identifies states by a hash.
- We need to know if the animator is playing a certain state in a certain layer, and in which frame is the animation in the state.
Working
Lets get the hash fo the state. We need to make a string composed of the name of the layer and the name of the state separated by a dot, and then ask the Animator class what's the hash:string name = animator.GetLayerName(layerNumber) + "." + animatorStateName;
string animationFullNameHash = Animator.StringToHash(name);
If you have Sub-state machines the "name of the state" is composed the same way: sub-state machine name and state separated by a dot.
Note that you get the layer name from an Animator instance, but the hash from the Animator class using a static method.
As you can see there's some level of indirection to get the progress of a state. In cases like this I like to use Extension Methods. Check the link if you don't know what extensions are. Be warned that some developers don't like extension methods because in some ways you are "hiding" your own methods in other classes. I'm not going to discuss that in this article but I recommend you do some googling and decide for yourself.
Lets make an extension to check if the animator is playing a certain animation and another to check the progress of the current animation.
public static class AnimatorExtension {
public static bool isPlayingOnLayer(this Animator animator, int fullPathHash, int layer) {
return animator.GetCurrentAnimatorStateInfo(layer).fullPathHash == fullPathHash;
}
public static double normalizedTime(this Animator animator, System.Int32 layer) {
double time = animator.GetCurrentAnimatorStateInfo(layer).normalizedTime;
return time > 1 ? 1 : time;
}
public static bool isPlayingOnLayer(this Animator animator, int fullPathHash, int layer) {
return animator.GetCurrentAnimatorStateInfo(layer).fullPathHash == fullPathHash;
}
public static double normalizedTime(this Animator animator, System.Int32 layer) {
double time = animator.GetCurrentAnimatorStateInfo(layer).normalizedTime;
return time > 1 ? 1 : time;
}
}
Some notes about normalizedTime:
Some notes about normalizedTime:
- We are not getting a frame number, we are getting a percentage.
- From the API: The integer part is the number of time a state has been looped. The fractional part is the % (0-1) of progress in the current loop.
- Beware of transitions, depending on how do you use them you may skip frames and never get the value you expected in normalizedTime.
- I'm clamping the value to 1 because, at least for now, I don't need to use these methods in looping animations. If you need it you'll need to check if the loop changed when checking for a frame.
- If you are not familiar with Extreme Programming, the link it's worth a read. While I don't like to adhere any methodology as a dogma, XP certainly has several great principles.
With the code above we have a way of querying an animator for the progress of a certain state, but if want to work with animation frames we need to convert that percentage to frames, or our frames to percentage.
An AnimationClip has a length and a frameRate. With those two values we can get the amount of frames of a clip:
int totalFrames = Mathf.RoundToInt(clip.length * clip.frameRate);
With the number of frames and the extensions we made we can now convert frames to percentage (I stored _totalFrames as a property):
double percentageOnFrame(int frameNumber) {
return (double)frameNumber / (double)_totalFrames;
}
Now we can get the progress of a state and also what that number should be in every frame. One problem you probably guessed is that normalized time is not precise, at least not in my experience. If you do something like this you'll get mixed results:
// WARNING, read above
public bool itsOnFrame(int frameNumber) {double percentage = animator.normalizedTime(layerNumber);
return (percentage >= percentageOnFrame(frameNumber) && (percentage < percentageOnFrame(frameNumber + 1)));
}
Take this with a grain of salt but from what I tried the precision will vary based on the framerate of your animations. For what is worth, I'm using 16 samples per animation.
In the Animation tab it's called Samples, 60 by default.
So how do we solve this? It will depend on the precision you need and your project.
- If you are always checking for several frames (for example if all your active frames are longer than 2 frames), you could probably get away with using the code above by checking the full range instead of every single frame.
- You could start debbuging and determine, based on your animations, a buffer value to use when checking frames. Maybe when normalizedTime is really close to the next frame value you return true.
- You could check for bigger or equal rather than just equal.
I went with the third option because it worked pretty well for my needs:
public bool biggerOrEqualThanFrame(int frameNumber) {
double percentage = animator.normalizedTime(layerNumber);
return (percentage >= percentageOnFrame(frameNumber));
}
double percentage = animator.normalizedTime(layerNumber);
return (percentage >= percentageOnFrame(frameNumber));
}
Obviously this method will return true in the frame we want and in all subsequent ones, so we need a flag to avoid repeating functionality we just wanted to run on that specific frame.
While we are at it we could add a special case for the last frame, to avoid the aforementioned problem with transitions:
public bool itsOnLastFrame() {
double percentage = animator.normalizedTime(layerNumber);
return (percentage > percentageOnFrame(_totalFrames - 1));
}
double percentage = animator.normalizedTime(layerNumber);
return (percentage > percentageOnFrame(_totalFrames - 1));
}
Now that we have the methods we need, lets put them in a class that makes sense. We need a class that represents an Animation from our point of view, that you can ask in what frame it is, if it's active in an animator and how many frames does it have.
I chose to call this class AnimationClipExtended. It's an awful name and you are right in hating me right now. Feel free to suggest a better name! I'll be happy to change it.
[System.Serializable]
public class AnimationClipExtended {
public Animator animator;
public AnimationClip clip;
public string animatorStateName;
public int layerNumber;
private int _totalFrames = 0;
private int _animationFullNameHash;
public void initialize() {
_totalFrames = Mathf.RoundToInt(clip.length * clip.frameRate);
if (animator.isActiveAndEnabled) {
string name = animator.GetLayerName(layerNumber) + "." + animatorStateName;
public class AnimationClipExtended {
public Animator animator;
public AnimationClip clip;
public string animatorStateName;
public int layerNumber;
private int _totalFrames = 0;
private int _animationFullNameHash;
public void initialize() {
_totalFrames = Mathf.RoundToInt(clip.length * clip.frameRate);
if (animator.isActiveAndEnabled) {
string name = animator.GetLayerName(layerNumber) + "." + animatorStateName;
_animationFullNameHash = Animator.StringToHash(name);
}
}
}
public bool isActive() {
return animator.isPlayingOnLayer(_animationFullNameHash, 0);
}
// ... And add the methods we worked on before.
}
In my defense, I intended to change the name later...
- Give it a range to check
- Handle the flags we talked about internally
- Have some sort of callback when important events happen
- Be reusable in any class we want
For the callbacks lets take the same approach we did with Hitboxes and make an interface. If you want to know the rationale behind it check the post.
public interface IFrameCheckHandler {
void onHitFrameStart();
void onHitFrameEnd();
void onLastFrameStart();
void onLastFrameEnd();
}
void onHitFrameStart();
void onHitFrameEnd();
void onLastFrameStart();
void onLastFrameEnd();
}
Now we have everything we need to make our class:
[System.Serializable]
public class FrameChecker {
public int hitFrameStart;
public int hitFrameEnd;
public int totalFrames;
private IFrameCheckHandler _frameCheckHandler;
private AnimationClipExtended _extendedClip;
private bool _checkedHitFrameStart;
private bool _checkedHitFrameEnd;
private bool _lastFrame;
public void initialize(IFrameCheckHandler frameCheckHandler, AnimationClipExtended extendedClip) {
frameCheckHandler = frameCheckHandler;
public class FrameChecker {
public int hitFrameStart;
public int hitFrameEnd;
public int totalFrames;
private IFrameCheckHandler _frameCheckHandler;
private AnimationClipExtended _extendedClip;
private bool _checkedHitFrameStart;
private bool _checkedHitFrameEnd;
private bool _lastFrame;
public void initialize(IFrameCheckHandler frameCheckHandler, AnimationClipExtended extendedClip) {
frameCheckHandler = frameCheckHandler;
_extendedClip = extendedClip;
totalFrames = extendedClip.totalFrames();
initCheck();
}
public void initCheck() {
_checkedHitFrameStart = false;
_checkedHitFrameEnd = false;
_lastFrame = false;
}
public void checkFrames() {
if (_lastFrame) {
public void checkFrames() {
if (_lastFrame) {
_lastFrame = false;
_frameCheckHandler.onLastFrameEnd();
}
if (!_extendedClip.isActive()) { return; }
if (!_checkedHitFrameStart && _extendedClip.biggerOrEqualThanFrame(hitFrameStart)) {
_frameCheckHandler.onHitFrameStart();
_checkedHitFrameStart = true;
} else if (!_checkedHitFrameEnd && _extendedClip.biggerOrEqualThanFrame(hitFrameEnd)) {
_frameCheckHandler.onHitFrameEnd();
_checkedHitFrameEnd = true;
}
if (!_lastFrame && _extendedClip.itsOnLastFrame()) {
_frameCheckHandler.onLastFrameStart();
_lastFrame = true; // This is here so we don't skip the last frame
}
}
}
We could use an enum to handle state changes instead of booleans, but then the last frame and the last active frame can't be equal. As always, it depends of your use case.
How do we use this class?
- Have an instance of it in the class we want to react to frame changes.
- Declare that class as implementing IFrameCheckHandler.
- Do whatever you need in the callback methods.
- Call initCheck() when the attack starts, or when the animation ends.
- Call checkFrames() in an update method (Remember that we don't want to depend too much on Unity's API, so it could be your own update method)
And that's it! Here's a look at all of this in action in the game I'm working on:
So now what?
In my next post I'll probably delve a little in how I handle attacks and combos as the one you saw in the gif, with animation interruption and such.
Also, with a really simple Editor script you can make the FrameChecker look like this:
Any feedback or questions? Let me know in the comments!
If you enjoyed the post, remember to check the Patreon!
You can also follow Strangewire on Twitter.
Also, with a really simple Editor script you can make the FrameChecker look like this:
Any feedback or questions? Let me know in the comments!
If you enjoyed the post, remember to check the Patreon!
You can also follow Strangewire on Twitter.
i struggle to understand how to put all of this together in a class and use it, could u help me?
ReplyDeleteHey MyLastFear, sorry for the delay, for some reason my email notifications were off.
DeleteCheck the comment noogaibb made, he explained it correctly. If you are still having trouble contact me via twitter! (it's @strangewire)
Good article, but lack of instruction of how to use it, that's why other people use AnimationEvent instead.
ReplyDeleteSome simple explanation like when to call initialize will be useful.
Well, I finally figure it out.
DeleteI'll post my explanation later, but my solution might be the worst.
The best way is waiting for author's explanation, but it seems that it won't happen in a few month.
First, you need to follow the author's instruction and create the classes first.
DeleteAnd, to let the code works like a charm, you need to set things.
Something like this.
public FrameChecker checker; //Make it public so you can adjust it in the editor.
public AnimationClipExtended extendedClip; //Put that animationclip and animator you want to do the frame checking in the editor.
public string animatorStateName; //The name of the clip's state.
public int layerNumber; //The animation layer you want to check.
Then, you need to let it initialize.
void Start()
{
extendClip.initialize();
checker.initialize(this, extendedClip);
}
And then just follow the author's instruction.
It will become something like this.
public class FrameCheckAnimation : MonoBehaviour, IFrameCheckHandler //Don't forget to declare the class.
{
public FrameChecker checker; //Make it public so you can adjust it in the editor.
public AnimationClipExtended extendedClip; //Put that animationclip and animator you want to do the frame checking in the editor.
public string animatorStateName; //The name of the clip's state name.
public int layerNumber; //The animation layer you want to check.
void Start()
{
extendClip.initialize();
checker.initialize(this, extendedClip);
//And some other thing...
}
void Update() //Or your own update method.
{
checkFrames();
//And your other method....
}
public void onHitFrameStart()
{
//Do something when the hit frame starts.
}
public void onHitFrameEnd()
{
//Do something when the hit frame ends.
}
public void onLastFrameStart()
{
//Do something when last frame starts.
}
public void onLastFrameEnd()
{
//Do something when last frame ends.
}
}
OK, now it should work.
Again, my solution might be the worst, but at least it would work.
If you have any suggestion, feel free to reply or leave comments.
Hey noogaibb! Sorry, for some reason I had the email notifications off.
DeleteYour implementation is correct! The only thing I would check is that maybe you don't need to call checkFrames() every frame, only when the attack is active
There's more pros, and maybe some cons if someone comes up with more I could talk about, but these are the biggest ones. www.vakantiehuis-dezilverenmaan.be
ReplyDeleteCertain personalities are also more in need of vacations than others because they are more at risk for stress related health problems. "Type A personalities" are people that are always working and are very intractable in what they do. Vakantiehuis Limburg
ReplyDelete