Search Unity

Looking for best practices on creating an action-adventurer character (Mecanim)

Discussion in 'Animation' started by Reverend-Speed, Jul 24, 2019.

  1. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    Hey folks. I'm building a game a little like Hob or Hyper Light Drifter or Akane or Beacon, etc - a hack-and-slash action adventure.

    store.steampowered.com/app/404680/Hob/
    store.steampowered.com/app/257850/Hyper_Light_Drifter/
    store.steampowered.com/app/884260/Akane/
    https://steamcommunity.com/app/856610

    I'm a reasonably experienced Unity dev and at the moment I'm the team's sole character modeller and animator (not a very good one). I have a little experience with Mecanim and I'm slowly exploring the options open to me, but as a coder I'm loath to solve a problem which is already well-understood. =D

    I'm wondering if anybody can point me towards best practices or tutorials on Mecanim state machines handling animation blending, animation events, root motion, combos, cancelling attacks, different movement states. I'd even take a recommendation of a good example on the Unity Asset Store - I'd really like to find something that maybe approaches the quality of Uncharted.

    Thank you in advance.
     
    TheKingOfTheRoad likes this.
  2. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    657
    You might be interested in my Animancer plugin which has lots of examples and solves many of the problems with Mecanim from a coding perspective.
     
  3. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    That's an amazing-looking plugin, @Kybernetik - I'll spend some time studying that. Thank you.
     
  4. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,392
    If you end up using the Animator, here's some considerations:

    If you use triggers, note that they're implemented such that if it's set, it stays set until it gets consumed. So if you do this:

    Code (csharp):
    1. // one place in the code:
    2. if (AttackPressed())
    3.     animator.SetTrigger("Attack");
    4.  
    5. // some other place in the code:
    6. public void TakeDamage() {
    7.     animator.SetTrigger("Damaged");
    8. }
    You now have a bug, where if the player presses attack about at the same time as it's getting damaged, you'll get two animations in a row in a very weird way. To solve this, avoid triggers when you can, and be very proactive about Reseting them. You will miss this, it will lead to bugs.

    By default, transitions can't be interrupted. This is bad, since you want every single transition to be interuptable unless you want your input to feel like garbage. You'll have to turn transitions on manually every time you make one. You will forget this, it will lead to bugs.

    Transitions by default have "exit time" checked. That looks bad in all cases unless you're automatically transitioning from one state to another. You will lose a lot of time on going back to your animator to uncheck that toggle.

    All the messages you get on a State Machine Behaviour has some kind of absurd implementation. OnStateEnter and OnStateExit is called at really strange times, OnStateMachineEnter and OnStateMachineExit is only called when you use the enter/exit nodes of the state machine. It will be very tempting to use these for game logic, but their design makes that an incredibly bad idea. Only use these for modifying the animation on the state they're on, anything else is bogus.
    Let me repeat that: you cannot use the enter/exit messages on these states to tell anything else about what state the animator is in, that is broken, by design.

    Getting info about what state you're in is error prone. The API is very cumbersome, and the behaviour of "what state you are in" when transitioning is ill-defined and not documented.


    In short... it's got a lot of pitfalls!
    I'd second @Kybernetik; don't use the Animator for this. It does some things well, but especially combos and cancelling stuff is very, very error prone.

    You can either use his Animancer framework, or do the same thing as that does by writing your own animation control script on top of Playables. That's a bit daunting, but I've done it myself, and the results are pretty good. I've got an earlier version of our framework on GitHub. I should probably get around to updating it to match the version in our code base at some point! It's not ready for use, but if you end up building your own, you can look at it for how to use Playables.
     
    Kybernetik and craigjwhitmore like this.
  5. craigjwhitmore

    craigjwhitmore

    Joined:
    Apr 15, 2018
    Posts:
    28
    I'm convinced that I should not use the animator now and use Animancer, thank you.
     
  6. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    @Baste Holy S***! World to the West! I should have mentioned you guys earlier, but I was picking names out of the air. As it happens, I should have namechecked you earlier - except for Beacon, none of the games I've previously mentioned were made in Unity, and similarly we're going for a more authored game than anything procedural.

    World to the West is awesome (the variety and beautiful art and the mechanical depth...!), and Sir Clonington is a great example of the kind of combo behaviour we'd like to get going in our game.

    My fellow coder and I read your notes on Github, especially your Motivation document. Yikes. I'm usually a big Unity booster (I run the local Unity User Group) and I had no idea Unity was so weak in this area. It's really horrifying.

    At the moment we're starting to take a look at Playables. We may even try to contribute something to your github project, depending on how things go.

    It's infuriating that this is required, though. Obviously when something is broken, you have to fix it yourself, but I've seen really fluid, impressive combo gameplay in Unity before - Aztez is a real gold standard. It's obviously possible, somehow, but...!

    Until now I haven't noticed how few Axonometric Action Adventures are built with Unity... or really, anything with interruptable transitions.

    Thanks for your contributions, guys. I'll see if I can get some more eyes on this, and if I find out anything I'll report back here.
     
  7. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,392
    Hey, thanks! I wrote pretty much all of that code. It way... a bit of a mess, and I'm not 100% happy with how it turned out, but it was a lot of fun! I'm very happy that we got to put in a dedicated flex button.

    Easter egg: one of my coworkers added a feature where if you do the running jump slam thing, and just hold the button down so he keeps lying there, the camera zooms in to give you an extra glorious view after like 10 seconds.

    To be fair, Playables is pretty good. It's really hard to get into, due to being built in a strange way. The docs also suffer from the documentation software not really handling that strange implementation very well.
    It's also very, very clearly not meant to be something you use to play your animations directly, but something you use to build up those components from.

    Still, it's fast, and so far most things have worked the way I expected them to. I don't have any of the kinds of bugs I had with the Animator in the AnimationPlayer. To be fair, our current game has much less complex animation transitions going on, so it might be that I'm missing stuff, but the "just do a default linear transition if no transition is defined" gives us so much fewer issues than the old way.

    About AnimationPlayer; Let me know if you want to seriously give it a go, and I'll take the time to push an up to date version of the code. I've been avoiding doing it because there's a version of state queuing in there that I'm not very happy with. There's also a big question of when I pull the plug on rewriting the UI (it's bad) - I probably want to rewrite it in UI elements, our current project's probably staying on 2018.4 for the foreseeable future.
     
  8. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    Man as perfection - sexual magnetism personified! Love the easter egg... =D

    I also love all the little details the game is filled with - the gorgeous particle effects, the footprints, the flowers recoiling from impacts, etc...

    It works really well. I might be able to see one or two tiny bits where you'd want to tune it (and I know you're aware of them), but the whole interface really sells Clonington as an idea and experience. Great work...! =D

    Interesting! We're also targeting 2018.4 at the moment. Right now I'm going to focus on building infrastructure (menus, faders, etc) for the game, then I'll dive into animation proper (my co-coder, who will be mostly fighting with this, is on holiday atm). Thank you for the offer of updating Animation Player, but we should probably not immediately bother you on this until we've done all our due diligence...!

    It is a bit of a shame, though. I - like most Unity programmers - was fairly resistant to Mecanim when it turned up first, but I slowly came around to the idea of it being something that freed up animators/designers to focus on animation and gameplay harmony. And now I find out that the goal Mecanim was meant to serve, it precisely thwarts.

    Hmm. Sadface.
     
  9. dibdab

    dibdab

    Joined:
    Jul 5, 2011
    Posts:
    856
    as Baste said, avoid triggers

    - use CrossFade (keep triggers for selected cases)
    - keep track of your animator states yourself
    - interrupt and transition to states by CrossFade

    recently discovered this animator events which look good (haven't tried yet)
    https://github.com/forestrf/UnityAnimatorEvents

    - keep in mind that transitions are grey zone,
    but UnityAnimatorEvents says it can track transition starts/ends
     
  10. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    @dibdab - Yeah, triggers seem like the kiss of death. Full disclosure, I've used them for tons of stuff in the past and it's mostly been fine. I've encountered the fail-to-reset issue, but this use case really brings out the worst in them. Will take your other notes on board.

    Hey, that does look promising. Will investigate in a while...!

    Thanks for these notes.
     
  11. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    So, just continuing some research, I figured I'd take a look at Unity's best examples for this kind of input. At the moment, I'm poking around with their 3D Game Kit, a project intended to provide a foundation for a 3rd Person action adventure.

    I also built it as a WebGL player in case anybody wants to take a look. KB&M is WASD + Left Mouse, while controllers are recognised but the camera yaw is reversed. =/

    Aside from them using a Character Controller component (I thought everybody was using Rigidbody!) and some very nifty mecanim setups, it doesn't take long before you encounter a bunch of interrupt issues - such as if you take damage and immediately jump, the character just levitates upwards without going into the 'jump' animation.

    They've also got a bunch of 'triggers' in the animator, which seem like they're causing trouble ('inputDetected' is one of them, which you'd think you'd handle as a bool).

    I appreciate the amount of prefabs they've built for common interactions, but the main character feels a bit clunky. It's disappointing.

    I'm going to try out some more Unity examples shortly, including the recently released RPG Creator Kit - but if anybody has a better Unity example for me to try, I'm all ears.

    Had no time to experiment with Playables yet, looking forward to it.
     
    Last edited: Jul 29, 2019
  12. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,392
    I updated the github repo!

    I've been wanting to learn how to distribute my github repos through the package manager for a while, so I went and did that. It's surprisingly simple.

    Now, to use the AnimationPlayer, you can add it to your manifest.json file:

    Code (csharp):
    1. "com.baste.animationplayer": "https://github.com/Baste-RainGames/AnimationPlayer.git"
    Or, if you want to make changes, you can just clone the entire repo into the Packages folder. If your project is already a git project, you can include it as a submodule:

    Code (csharp):
    1. cd Packages/
    2. git submodule add https://github.com/Baste-RainGames/AnimationPlayer
    I haven't used submodules much, so we'll see how well it works.
     
  13. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    657
    "A bit clunky" is a massive understatement. I actually started looking at the 3D Game Kit today in order to use it as an example of how to rework an Animator Controller based setup to use Animancer instead and I'm absolutely astounded at how bad it is for something that's supposed to be a showcase of what Unity can do and how to implement things properly.

    Take the basic movement for example. The animations move your character forward as they turn, meaning that it moves you perpendicular to the direction of your input. Here's a gif of me pressing A and D to move left and right, yet the character keeps moving towards the camera with each turn (especially at the end):

    Recording.gif

    That alone makes it absolutely horrible to control with any precision, but there are several other basic issues too like how you can't jump again during the landing animation, which is absurd in a platformer.

    Then you get into the actual implementation of the code and Animator Controller which is of course a total mess:
    • There's an IdleSM sub state machine with a default idle and several others for when you stand idle too long. It has a RandomStateSMB script which will pick one of the others after a random amount of time. Sounds good. Min 0, max 5 (x normalized time of the basic animation which is about 4 seconds long). That doesn't sound right, because the character could get bored immediately any time you return to idle. Not to worry though, because the LocomotionSM actually has an Idle state of its own with one of its exit transitions being a TimeOutToIdle trigger that gets set somewhere in the 680 line PlayerController class using a manually updated float timer to count towards a value set in the Inspector (5 seconds).
    • So to implement the behaviour of "if the player stands idle for between 5 and 25 seconds, play a different animation", they used a convoluted combination of a state machine behaviour, a regular script, the animation length, and an extra Idle state inside the Locomotion sub state machine.
    • And since Animator Controllers have no concept of data structures, adding another animation to the random choices would require manually adding another state, setting up another 4 transitions (and making sure they're the same as the others), and also remembering to tell the RandomStateSMB that there are now 4 options. All that instead of just having an array that you could just add another element to and everything treating it as an array would continue working properly.
    • Then there's the PlayerInput class which has a singleton instance that it assigns in its own Awake method so some things access it through that property, but others use FindObjectOfType to find it for some reason and then the PlayerController uses GetComponent to find it.
    • It also has an attack wait time which is a constant 0.03 seconds, which really isn't the sort of thing that should be hard coded. But what's it used for? Of course it's to set a bool back to false after that time, so instead of "hey, the user clicked, if they can attack then do so" it's actually "hey, the user clicked, I have no idea if they can actually attack right now so just keep trying for about 2 frames and then give up".
    • Even their naming convention is shoddy. The PlayerInput class has an m_Movement field backing a MoveInput property ... well OK, there's also CameraInput and JumpInput and ... Attack and Pause ...
    That's all I've actually looked at so far and every single bit of it has been utterly terrible.

    Edit: BTW, sorry for the rant.
     
  14. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    Holy S***, @Baste, that's fantastic! I've yet to have time to sit down and really evaluate everything, but I really appreciate you uploading that! When my co-coder returns from his holidays, we'll devour that... =D

    @Kybernetik , no need to apologise for the rant. I started the thread seeking best practices for action adventure characters with interruptable animations; I think pulling apart what's been previously offered as 'the example' totally fits with that rubric... it's analysis.

    Regards the character moving forward with the animations - I'm the team's current animator and I'm not a good one, but isn't this kind of thing usually part of incorporating root motion, which helps avoid skating, etc? Isn't this what's used for games like Uncharted, et al? I guess the solution here is that there should be more animations with varying degrees of forward movement, handled by a blend tree.

    I don't have a PS3/4, so I can't really test how this works in Uncharted for myself atm. =)

    0_o

    Would you have any suggestions on better naming conventions? I see your point, I'm just curious. =D

    ...


    It'd still be nice to find a way to make things just work with Animator. To give credit to my co-coder, he also found the following two videos on the topic... I might try to get in touch with these folks:



    And speaking of alternative frameworks, here's a... very particular, somewhat limited one...

    Hope to come back with some more tests and information shortly.

    Thank you so much for taking an interest in this.
     
  15. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,392
    No problem! It's actually really valuable for me to get that project properly into it's own package living in a git submodule in our game, so I don't have to go copy over files from our game to the github repo every time I want to use the code in a different project.

    So the problem with root motion is that while you get better-looking result, it takes more time to make.

    If you're animating without root motion, you're just telling the blend tree where you're moving, and you hope that you can match that as well as possible based on some parameters. Let's call this "Best-fit" animation.

    Best-fit animation looks worse. You get foot-sliding and other mismatches between how the animator animated the model to move, and how it actually moves.

    Root motion looks much better. The character moves like the animator made it to move. There's also some things you can get away with not coding - like you don't have to add acceleration or deacceleration to the gameplay code, it just happens because when you set the speed blend to max or 0, the animation system is set up to do just that.

    On the other hand, it limits you to exactly the kinds of movements that's in the animation set. If you really need the character to turn real fast like, but you don't have a real fast like turn animation, you just can't do that, and have to go ask the animator to do that.
    There's also things that are harder to program. If you need your character to stop exactly at a position, that becomes hard as you're not setting the position of the character, you're steering it by setting values in a blend tree. If you have ever seen an npc in a AAA game that just keeps walking in tight circles around some target that it's trying to get to, I'll bet that the turn radius is tighter than the tightest turn radius it has animations for.

    So root motion vs. best-fit really comes down to resources. Do you have enough animator time to implement all the motions you need for your game? Do you have the programming time to work around the parts of root motion that are difficult? Do you need that level of fidelity?

    For World to the West, we only had one guy doing all the character models and all of the animations. We also didn't share any rigs between any of the characters, and none of them were Humanoid-compatible, so we literally had to hand-make every single animation for everyone.
    Oh and that guy didn't work full-time. So yeah we didn't use root motion.
    Also like don't do that. Also don't animate in 50 fps. Also don't put blend files directly in the project. We did everything wrong.

    Anyways, looking at the 3D game kit gif from above, it seems like the character has an instant turn animation, but for some reason it's only happening when turning from right to left, not left-to-right. Or, probably more precisely, it's happening every second time the character does a sharp turn. I'm guessing that the animator controller's set up with too few transitions that are interrupt-able, so the blending visits states that push the character downwards with root motion.
     
  16. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    Tangent -

    I have a bit about learning. Folks sometimes talk about learning, wisdom, etc - as the tree of knowledge. You climb the tree, you win the apple of understanding, etc.

    Bullshit.

    I think you start at the top of the tree of knowledge and fall down through it, hitting every branch on the way down.

    And maybe, if you get through the tree and survive the landing, you'll have learnt something.

    Which is to say I appreciate the work you guys did on World to the West, and I doubly appreciate you (and @Kybernetik !) for sharing that hard-won wisdom. My team are just balancing on the edge of the diving board above this next tree of knowledge (so what if it's a weird metaphor, it's my metaphor, if you don't like it go F*** yourself) and anything you can share to save us from getting hit by the same branches is very gratefully received.

    We'll undoubtedly hit hundreds of different branches, but we'll try to pass the fruits of those bruisings on in the same spirit.

    TLDR: Facts. Cheers, @Baste.

    As I said, at the moment, I'm our animator (we're a team of 3 atm and unwilling to expand until we have the basics right). I'm not a great one, in fairness, but I'm willing to do a bunch of different movement animations for our character as we explore systems. I kinda wanted to try doing things the 'right' way and avoid skating, etc, but given your notes and the angle we're playing at (very similar to World to the West), we'll probably stick with 'Best-fit'. We probably don't need super-high fidelity.

    ...That said...!

    Here's some Uncharted data from 2010 (relevant bit starts at 10.40 (idle and turning on the spot root motion), though the whole vid is interesting). Related info as .pdf. Obviously the dev is talking about a team that can afford 1 animator and 1 programmer per character and I can't afford to replace this piece of S*** Microsoft keyboard I'm using, but I think there's still some interesting stuff there about blending multiple animations together to save memory (and, presumably, time spent animating?). I don't think I'd mind about enemy characters not having this much attention on them if we made the main character look groovy...

    Just looking around again at games made from this point of view (Axonometric Top Down 60Degree Cabinet Oblique Zelda-Like With Perspective) - with the exception of the esteemed World to the West and Lucah, nearly everything is being made with UE... Seven, Redeemer, etc. Annoying.

    Well, time to start hitting those branches...

    Thanks again folks.
     
  17. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    657
    Yeah, @Baste described the problem accurately here. Though one thing I plan on trying when I get to it is using root motion but cancelling out any motion perpendicular to the player's input so in theory you get a middle ground between avoiding sliding and responsive controls.

    The specific convention you use is less important than sticking to it, because names like JumpInput and Attack suggest that those properties are fundamentally different somehow when they actually aren't. I would just use Jump/Attack/etc. because there's no point in suffixing everything with "Input" when you're inside a PlayerInput class.
     
  18. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    @Kybernetik - that sounds like a nice compromise between root motion and player intention. Please let us know how that goes, it's vital work. =D

    Regards naming conventions, yeah, that's basically what I was thinking. Verbose, consistent, aware of context. Great advice.
     
  19. dibdab

    dibdab

    Joined:
    Jul 5, 2011
    Posts:
    856
    about Uncharted-type of ways and methods:

    in Unity all layers consume processing time. so it's not wise to have too many layers.

    you can mix (playables might be better for this) in anim like the idlesinglepose+breathin in the video but needs to have separate animator controller and probably best to keep it separate from mecanim of the original anim (the idle) the pose should not be one-frame anim (because it might not play well with mecanim transitions)

    then adding in lateupdate (or anywhere later added on top of it) still have to write them on skeleton ie. read the bone curves out

    okay like this it doesn't look like many
    animlayers.jpg

    it probably has some ways to inject animation data on bodyparts separately (arms, legs, etc)
    yeah propably need to make the animations on T-pose, which makes up the additive anim (so cannot use animations already made, or at least you need to convert (zero out bone rotations etc)

    if you build up your mecanim by bodypart layers, then the system that animates them will be quite special... every "animation" will need a list of layers, on which it is played
     
    Last edited: Jul 30, 2019
  20. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    Well, ultimately there's a balance to be struck here between something that's practical for a small indie team and doing things 'properly', especially given your notes on multiple layers consuming processing time.

    Again, kind of a novice in this particular area, but don't we have avatar masks for just the purpose of putting animation data on bodyparts separately?

    At the moment I'm leaning towards the layer setup mentioned in the first video I linked to above, where you have one layer for locomotion, one override layer for combat actions and one additive layer for pain reactions.

    I'd quite like to have information about the character conveyed by posture as well, so I need to look into whether we should modify the normal animations or just swap out walkcycles and the like. Might be able to minimise that further, though.

    Thanks @dibdab , really useful information!
     
  21. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    657
    Here's another piece of awesomeness from the 3D Game Kit's character controller:
    Recording.gif
    It's exactly what it looks like. The bottom of the capsule is just touching the corner so it counts as grounded, but that ground is too steep for it to be allowed to move in that direction ...
     
  22. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    Hey @Kybernetik - I see you giving the asset a good kicking on their store page too. =D They just updated the project, but I can't seem to see a changelog, so it's impossible to see if they're paying attention to notes and suggestions without downloading the entire huge project...!

    Regards the capsule issue, a certain amount of that is kinda expected edge-case jankiness, no? Granted, I'm a novice in this area, so please feel free to put me right here.

    My default character size is usually 1.76y (average European male height), .64x, .32z, which gives me a capsule radius of .64. (I tend to run everything off a rigidbody - which also confused me about the 3DGameKit - should I be using Character Controllers?)
    upload_2019-7-31_10-48-54.png upload_2019-7-31_10-51-40.png
    Obviously a thinner capsule will help solve some of the levitation issues you've identified, but aside from maybe doing some raycast/overlapsphere tests to check for slopes and groundedness (which could also be somewhat expensive - multiple physics calls, even if spread out over frames etc), what's the good alternative to using capsule colliders?

    Would love to learn if there's a better way to achieve this on an indie budget!

    It also strikes me that we should maybe make a reference to this thread in the official 3D Game Kit thread, or post some of our notes and suggestions in there.

    Lastly, I note that one of your Unity Assets is named 'Inspector Gadgets'. I think that deserves a polite, but appreciative, round of golf claps.

    PS: Btw, your comment on the asset store page, "You can't jump again during the landing animation, which feels extremely clunky for a platformer" is absolutely on-point, that's a real weakness there.
     
  23. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    657
    Of course there will always be edge cases, but the handling of slopes and corners is one of the main things a character controller needs to focus on.

    I've always built my characters with Rigidbodies too so I can control things like this instead of relying on Unity's black box which has plenty of known issues.

    For this particular problem, I've often inherently solved it by simply having the slope detection (to determine if you can't move in a direction because it's too steep) being part of the regular ground detection code. So if your only contact normal is too steep, you aren't grounded so stop applying so much friction and the capsule will naturally slide off the corner.

    In one of my prototypes I even added code for when that happens to also calculate how much of the acceleration due to gravity is being countered each frame by the platform preventing the character from falling straight down so that I can re-apply that acceleration in the downhill direction. I never ended up releasing that game, but from what I can remember the idea worked quite well.

    If I had to do it again I'd start by looking at the top controllers on the Asset Store and github. I remember seeing the Kinematic Character Controller when it was released and its videos went through lots of common situations which it seemed so handle quite well. Chances are I'd still end up doing it myself, but I'd use them for inspiration.
     
  24. dibdab

    dibdab

    Joined:
    Jul 5, 2011
    Posts:
    856
    avatar masks and additive layers

    on the video starting at 17:10 he talks about that "stiffness"

    made a test using delta rotations

    this is how it looks
    deltaR1.gif

    Code (CSharp):
    1. public class addDeltaR : MonoBehaviour {
    2.  
    3.     public Transform[] def;//T-pose
    4.     public Transform[] mod;
    5.     public Transform[] addto;
    6.  
    7.     public Vector3[] eulDif;
    8.     int i;
    9.  
    10.     void LateUpdate () {
    11.  
    12.         for (i = 0; i < def.Length; i++) {
    13.            
    14.             Quaternion a = def[i].localRotation;
    15.             Quaternion b = mod[i].localRotation;
    16.             Quaternion relative = Quaternion.Inverse (a) * b;
    17.  
    18.             eulDif[i] = relative.eulerAngles;
    19.             addto[i].localRotation = relative;
    20.         }
    21.     }
    22. }
     
  25. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,392
    The built-in character controller isn't that bad, it's just made to be generic, and is only appropriate up to the point where you need the character movement to be more specific to your game.

    Since you get some things "for free" with it, I'd start with that (or the Kinematic Character Controller), and stick with it until it's not good enough anymore.

    You're going to want to be shooting out a lot of Raycasts. I think the characters in WttW does something like 20 each frame. They're surprisingly cheap, by the way.

    For ledges, as you notice when you play the game, we do a bunch of stuff. Mostly you just jump off when you move directly towards them, that's done with raycasts. If you move towards them at an angle, we steer you sideways so you run perpendicular to the edge - it's a variation on this (which we also do). You can wiggle your way into a situation similar to what's happening in @Kybernetik's example from the game kit, in which case we make you just slide down once you're far enough over.

    That took a ton of time to do. Not all at once, but gradually over the project. It took a day here, a couple of hours there, and sometimes several days at once, and gradually we improved the movement.

    This boils down to two pieces of advice:
    - Iterate a bunch on the character controller. You'll need something usable to start out with, but as your design changes, you'll want to change the controller.
    - Don't rely on physics simulations unless you're making a physics simulation game. For snappy controls, you definitely want to set the velocity of the character directly for the most part. Unless you're using root motion, I guess.
     
  26. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    - Just a quote from a friend with a fairly successful Unity game on Steam (which is played from the axonometric angle). Echos a lot of what Baste's said...!

    @Baste:

    I've been using the Rigidbody since Unity released examples of characters using Rigidbody. I figured, oh, we're moving in this direction, I should check it out... and really, it's proven to be a very reliable workhorse for me since then.

    Interesting regards Raycasts! Didn't think they were that cheap, I've always ended up staggering them over frames. But then I've been using fairly weak hardware until now, and we're currently targeting modern consoles for our game. Thanks for the advice.

    The info on World to the West is really useful. Our intention is to have no dedicated 'jump' - we'll employ dashing ala Hyper Light Drifter, but the Zelda-like hop on edges would also be very useful. My compliments to your implementation on WttW, it's very reliable, as I remember.

    To clarify, are you saying we should iterate on top of the built-in controller, or that we should iterate on our own character controller throughout the project? If it's the latter, yeah, I think we'll probably do that. Good advice.

    Regards your second point of advice, well, I think we're probably going to use the Rigidbody, but move it with Rigidbody.movePosition , which has always served me well in the past as a responsive solution (with the occasional Rigidbody.angularVelocity = 0.0f, to halt the craziness). Per your advice, I think we'll ditch root motion and go with best fit (goodbye 'right' way of doing things, hello snappy controls).


    @dibdab:

    0_o

    quaternions that's above my paygrade sir

    ulp

    I'll take a look, though. Thank you.


    @Kybernetik:

    I'm kinda hoping I can encounter these as I develop the game, iterating similar to Baste's advice above. And... you can get away with a lot, I've found, with an appropriately sized Rigidbody, some movePositions and moveRotations and a smattering of physics materials...

    Do you mind if I ask - do you continually detect the ground? In the past I've always just fired a raycast at the ground on jump input, and with a little care it seems to have worked passably well. Mind you, that's just in tests - this is the first 'production level' game of this type that I'll have made. I can see the constant 'isGrounded' test also being useful for slope detection (and other things).

    Actually, another thought - a colleague of mine recently remarked that he used overlapSphere instead of raycasts for ground detection, and on doing some research it seems overlapSphere is indeed as effective and a little more efficient. I'm reminded that Brendan Chung used sphere checks for mantling etc in Quadrilateral Cowboy, with Valve using them for zombie navigation in L4D... so I might start investing in spheres in the future...

    Hmm. So I presume the 'identify countered gravity and then re-apply' is to prevent people from, uh, Elder Scrollsing their way up things like mountains?

    I definitely plan to take a look at the top controllers on Asset Store and github... or at least the ones I can afford and understand. KCC is WAY beyond my paygrade. I think for a lot of this we'll follow Baste's advice and get stuck in and when we hit a problem we'll just have to reinvent calculus.

    Still pissed off about the mecanim situation, though. I'll let you know how I get on.

    Thank you all for your wonderful advice.
     
  27. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    657
    If you don't want the Rigidbody rotating, set its constraints to freeze rotation. I usually have the character model as a child of the Rigidbody. Rotate the model, move the Rigidbody.

    There's almost always a need to constantly know the grounded state to determine whether to play falling animations, how much acceleration the player has, etc.

    I often use physics collision events to determine groundedness (based on the angle of the collision normal).

    Yeah, any time you can't jump because the slope is too steep, my controller would slide you down as if in free fall where the Elder Scrolls controller lets you stand on and walk along super steep slopes with only slow sliding when you stand still.
     
  28. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    Sorry, yes, the last couple of times I've used Rigidbody it was useful to rotate it as well, but you're right, of course:
    Play the ball, not the man. =D

    So, when you say physics collision events, are you talking about something like OnCollision? So, OnCollision, iterate through array of colliders, dot product the collider normals and if we find one similar to the vertical orientation of the player character / vector3.up (depending on gameplay), then we're grounded? Hmm. I'd really got it into my head that in most cases a few raycasts would suffice. Makes sense, though.

    Thanks again, man.
     
  29. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    657
    Yeah, that's pretty much it. If you don't need to account for changing gravity, just find the normal with the largest y value and that's your most groundy contact. So either that's the ground, or you use it for the free fall calculation.
     
  30. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,392
    You might be able to use collision events for grounded checks. You might not.

    If you have a mesh that looks like this:

    upload_2019-8-1_16-29-58.png

    And your capsule-based rigidbody hits it like this:

    upload_2019-8-1_16-32-10.png

    You can get into all kinds of hairy situations. Right there you're not grounded, so you should start falling right down. But, if you're still in contact with the wall when you hit the ground, I'm pretty sure that you're not getting a new OnCollisionEnter message, if the ground is the same collider as the wall.

    That being said, I haven't tried this too much, so I might be missing a solution, or the problem might not be as big as I imagine.
     
  31. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    657
    You should get OnCollisionStay messages every frame unless your Rigidbody goes to sleep and they should contain multiple contact points. I don't remember specifically testing something like that, but I know that you can get multiple collision messages in one frame and each of them can have multiple contact points so I would expect that to mean it should work properly (but I might not have tested that either since like Unity 3).
     
  32. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,392
    I generally avoid OnCollision/TriggerStay as those have a quite high perf overhead. I've had those methods just existing wreck framerate. That being said, that's on objects that there are many of, it should be 100% fine on a character controller.
     
  33. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    @Baste - Your point on OnCollision performance is why I've mainly avoided using it in the past - seemed to make more sense to only get that information if I really needed it (on jump). That said, if it's reasonable on one player (or maybe a couple), then I'll use this going forward. I'll also look into OnCollisionStay - thank you, @Kybernetik.
     
  34. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    657
    I just released Animancer v3.1 which includes the new example that remakes the player from the 3D Game Kit to use Animancer instead. In particular, this section goes over the locomotion problem I mentioned.

    It turns out that I was wrong about being unable to jump during the landing animation. The actual issue was an even simpler mistake on their part: the PlayerController only checks the jump input in FixedUpdate so it can randomly miss the input if there happen to be multiple Updates between two FixedUpdates.

    Also, I was never able to get the character to levitate by jumping and getting hit at the same time like you described, but again I think that was actually a simpler issue ... it just does it randomly for no apparent reason as you can see in the first gif here.
     
  35. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    Wow, @Kybernetik, that's a really thorough examination of the 3D Game Kit and its failings...! The FixedInput thing is... ugh. I wonder how that happened, that's a fairly rookie error (said the rookie).

    You make Animancer look extremely attractive. =D I'm currently hit with some heavy work, so I can't really dig into this at the moment...

    ...but I do have some more ideas for investigations. 3D Game Kit isn't the only 'best practice' project Unity's released recently...! =D

    Thanks again, man, really appreciate your insight.
     
  36. Grhyll

    Grhyll

    Joined:
    Oct 15, 2012
    Posts:
    91
    Hi!

    After that short email exchange, I figured I could just share our PawnStateController class (and got the green light from my boss to do so).

    Some insight:
    - It allows you to change state whenever you want, to always know in which state you are, and to effectively trigger the state you want in the animator if it's really what you need.
    - It doesn't use trigger at all because I needed it to be very responsive, only Crossfade as it was mentionned earlier in this thread I believe (sorry I've read it pretty quickly). Triggers may still be used elsewhere in the code, for specific cases where it makes sense, but as said before in this thread, they have to be handled with caution and reset if needed.
    - By no mean this code is perfect, a lot of it comes from previous projects and may be vastly improved, and it's designed to suit the needs of our current project, so possibly it will lack important elements for other projects. For example the need to have a BasePawnState implement "HasAutomaticAnimatorEndTransition" is probably unecessary and leads to mistakes where you forget to implement it.
    - A tedious aspect of this is that you have to create new classes for each important PawnState, get their hashes, register them, etc... It could probably be improved as well. The states have to be added manually in the InitStates functions. If you forget any of these steps, the state controller will be unhappy. A lot of this, however, can be automatized, depending on your workflow. Automatizing is highly recommended, it will save a lot of time and reduce dramatically the amount of mistakes. In Furi 99% of the states were just one generic state, with a set of data defined in a custom editor attached (the custom editor also generated the animator and saved the data in txt files that the game then loaded to create and initialize all the states).
    - The cool thing is that the currentState is highly reliable, you can do a lot in your PawnStates and all the other classes can pretty much ignore the animator. You'll have a virtual function CanStartWalking in your BasePawnState, and then your pawn controller can just check it on the current state to know if it's allowed to start walking, rather than enumerating all the various conditions.
    - One thing that this doesn't handle at all because our current project doesn't need it, but that was highly needed on Furi, is events. Basically, each time an event was fired, we checked if we really wanted it based on the current state (events can be fired after a transition has started and you don't want it anymore), and also when leaving a state (if the animation was supposed to have played until the end) we checked if it had any remaining events that we then fired by hand. Of course all of this is harder done than said and require a lot of caching and data everywhere.

    I've probably forgotten a lot, but here's the code, hope it helps.

    Code (CSharp):
    1. using UnityEngine;
    2. using System;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5.  
    6. public abstract class BasePawnStateController : MonoBehaviour
    7. {
    8.     public BasePawn pawn { get; set; }
    9.     public Animator animator { get { return pawn.animator; } }
    10.  
    11.     Dictionary<int, BasePawnState> pawnStateDict;
    12.  
    13.     public BasePawnState currentState { get; protected set; }
    14.     public BasePawnState previousState { get; protected set; }
    15.  
    16.     public bool forceNextCrossFadeInstant { get; set; }
    17.  
    18.     public float lastManualStateChangeTime { get; protected set; }
    19.     public int lastStateManualChangeFrame { get; protected set; }
    20.  
    21.     public bool IsWaitingToApplyNewState { get { return needApplyState; } }
    22.  
    23.     AnimatorStateInfo currentAnimatorStateInfo;
    24.     AnimatorStateInfo previousAnimatorStateInfo;
    25.  
    26.     bool needApplyState = false;
    27.     bool waitingToEnterInNewAnimatorState;
    28.     bool resetting;
    29.  
    30.     Coroutine updateCoroutine;
    31.  
    32.     const int layerIndex = 0;
    33.  
    34.     protected virtual bool debugLogStateChanges { get { return false; } }
    35.     protected virtual bool debugLogDetail { get { return false; } }
    36.  
    37.     public Action<BasePawnState, BasePawnState, bool> OnPawnStateChanged;
    38.  
    39.     public void Init(BasePawn p)
    40.     {
    41.         pawn = p;
    42.  
    43.         pawnStateDict = new Dictionary<int, BasePawnState>();
    44.  
    45.         InitPawnStates();
    46.  
    47.         currentState = pawnStateDict[GetDefaultState()];
    48.  
    49.         if(gameObject.activeInHierarchy && updateCoroutine == null)
    50.         {
    51.             updateCoroutine = StartCoroutine(ProcessUpdate());
    52.         }
    53.     }
    54.  
    55.     void OnEnable()
    56.     {
    57.         if(updateCoroutine == null)
    58.         {
    59.             updateCoroutine = StartCoroutine(ProcessUpdate());
    60.         }
    61.     }
    62.  
    63.     void OnDisable()
    64.     {
    65.         if(updateCoroutine != null)
    66.         {
    67.             StopCoroutine(updateCoroutine);
    68.             updateCoroutine = null;
    69.         }
    70.     }
    71.  
    72.     protected virtual void OnDestroy()
    73.     {
    74.         if(currentState != null)
    75.         {
    76.             currentState.OnCurrentStateDestroy();
    77.         }
    78.     }
    79.  
    80.     //TODO: prevent another state to be triggered during reset?
    81.     public virtual void HardReset()
    82.     {
    83.         if(debugLogDetail)
    84.         {
    85.             Debug.Log("HardReset for " + gameObject.name + " - " + Time.frameCount);
    86.         }
    87.  
    88.         resetting = true;
    89.  
    90.         lastManualStateChangeTime = Time.time;
    91.         lastStateManualChangeFrame = Time.frameCount;
    92.  
    93.         currentState.OnExit(pawnStateDict[GetResetState()], false);
    94.         animator.Play(GetResetState(), 0, UnityEngine.Random.Range(0f, 1f));
    95.         currentState = pawnStateDict[GetResetState()];
    96.  
    97.         previousState = null;
    98.         needApplyState = false;
    99.         waitingToEnterInNewAnimatorState = false;
    100.         forceNextCrossFadeInstant = false;
    101.     }
    102.  
    103.     protected virtual int GetDefaultState()
    104.     {
    105.         return HashManager.Instance.SDefault;
    106.     }
    107.     protected virtual int GetResetState()
    108.     {
    109.         return HashManager.Instance.SDefault;
    110.     }
    111.  
    112.     public virtual void InitPawnStates()
    113.     {
    114.         AddState(GetDefaultState());
    115.     }
    116.  
    117.     public void AddState(int stateHash)
    118.     {
    119.         pawnStateDict[stateHash] = CreateState(stateHash);
    120.         pawnStateDict[stateHash].Init();
    121.     }
    122.  
    123.     protected virtual BasePawnState CreateState(int stateHash)
    124.     {
    125.         return PawnStateFactory.CreateState(stateHash, pawn);
    126.     }
    127.  
    128.     public void AddState(BasePawnState state)
    129.     {
    130.         pawnStateDict[state.stateHash] = state;
    131.         state.Init();
    132.     }
    133.  
    134.     public BasePawnState GetState(int stateHash)
    135.     {
    136.         BasePawnState resultState = null;
    137.         if(!pawnStateDict.TryGetValue(stateHash, out resultState))
    138.         {
    139.             Debug.LogError(this + " Pawn.GetState can't find state: " + GetNameForHash(stateHash) + "; did you add it in InitPawnStates?");
    140.  
    141.             // Get the default state
    142.             if(!pawnStateDict.TryGetValue(GetDefaultState(), out resultState))
    143.             {
    144.                 Debug.LogError(this + " can't find default state either.");
    145.             }
    146.         }
    147.         return resultState;
    148.     }
    149.  
    150.     public void GoToState(int nextStateHash)
    151.     {
    152.         if(!enabled || !gameObject.activeSelf)
    153.         {
    154.             Debug.LogWarning("GoToState shouldn't be called on a disabled PawnStateController! This call will be ignored.", gameObject);
    155.             return;
    156.         }
    157.         if(debugLogDetail || debugLogStateChanges)
    158.         {
    159.             Debug.Log("Manual GoToState " + GetNameForHash(nextStateHash) + " for " + gameObject.name + " - " + Time.frameCount);
    160.         }
    161.         GoToState(nextStateHash, false);
    162.     }
    163.  
    164.     private void GoToState(int nextStateHash, bool animatorAutomaticTransition)
    165.     {
    166.         if(resetting)
    167.             return;
    168.  
    169.         if(debugLogDetail || debugLogStateChanges)
    170.         {
    171.             Debug.Log("GoToState " + GetNameForHash(nextStateHash) + " (automatic animator transition: " + animatorAutomaticTransition + ") " + Time.frameCount);
    172.         }
    173.  
    174.         if(needApplyState && nextStateHash == currentState.stateHash)
    175.         {
    176.             return;
    177.         }
    178.  
    179.         previousState = currentState;
    180.  
    181.         previousAnimatorStateInfo = currentAnimatorStateInfo;
    182.  
    183.         currentState = GetState(nextStateHash);
    184.  
    185.         if(previousState != null)
    186.         {
    187.             previousState.OnExit(currentState, animatorAutomaticTransition);
    188.         }
    189.         currentState.OnEnter(previousState);
    190.  
    191.         lastManualStateChangeTime = Time.time;
    192.         lastStateManualChangeFrame = Time.frameCount;
    193.  
    194.         if(!animatorAutomaticTransition)
    195.         {
    196.             needApplyState = true;
    197.         }
    198.  
    199.         OnPawnStateChanged(previousState, currentState, animatorAutomaticTransition);
    200.     }
    201.  
    202.     public float GetCurrentStateNormalizedTime()
    203.     {
    204.         if(needApplyState || lastStateManualChangeFrame == Time.frameCount)
    205.             return 0f;
    206.  
    207.         return currentAnimatorStateInfo.normalizedTime;
    208.     }
    209.     // Warning: depending on where you're using this, you may need to add Time.deltaTime / CurrentStateLength in order to apply it immediately
    210.     public void SetCurrentStateNormalizedTime(float normalizedTime)
    211.     {
    212.         animator.Play(0, 0, normalizedTime);
    213.     }
    214.  
    215.     //Warning: it's not bound to be exact if called from LateUpdate
    216.     public float GetCurrentStateRealRemainingTime()
    217.     {
    218.         if(needApplyState)
    219.         {
    220.             //We're not in current state, so there's no way to know its duration
    221.             return 999f;
    222.         }
    223.         return currentAnimatorStateInfo.length * (1f - GetCurrentStateNormalizedTime());
    224.     }
    225.  
    226.     public float GetCurrentStateLength()
    227.     {
    228.         return currentAnimatorStateInfo.length;
    229.     }
    230.  
    231.     public bool IsInTransition()
    232.     {
    233.         return animator.IsInTransition(layerIndex);
    234.     }
    235.     public float GetTransitionProgress()
    236.     {
    237.         return animator.GetAnimatorTransitionInfo(0).normalizedTime;
    238.     }
    239.  
    240.     void Update()
    241.     {
    242.         if(currentState.ShouldTriggerStateEnd())
    243.         {
    244.             currentState.TriggerStateEnd();
    245.         }
    246.     }
    247.  
    248.     IEnumerator ProcessUpdate()
    249.     {
    250.         yield return null;
    251.         while(true)
    252.         {
    253.             if(needApplyState)
    254.             {
    255.                 needApplyState = false;
    256.                 ApplyState();
    257.             }
    258.             if(currentState != null)
    259.             {
    260.                 currentState.Update();
    261.             }
    262.             yield return null;
    263.         }
    264.     }
    265.  
    266.     void LateUpdate()
    267.     {
    268.         if(animator.runtimeAnimatorController == null)
    269.             return;
    270.  
    271.         if(resetting)
    272.         {
    273.             resetting = false;
    274.             currentAnimatorStateInfo = animator.GetCurrentAnimatorStateInfo(layerIndex);
    275.             previousAnimatorStateInfo = currentAnimatorStateInfo;
    276.         }
    277.  
    278.         if(animator && animator.enabled && animator.isInitialized)
    279.         {
    280.             currentAnimatorStateInfo = animator.IsInTransition(layerIndex) ? animator.GetNextAnimatorStateInfo(layerIndex)
    281.                 : animator.GetCurrentAnimatorStateInfo(layerIndex);
    282.  
    283.             // It can happen that the animator is playing a transition from a state to itself
    284.             if(currentAnimatorStateInfo.fullPathHash != previousAnimatorStateInfo.fullPathHash ||
    285.                 (previousAnimatorStateInfo.fullPathHash == currentState.stateHash && waitingToEnterInNewAnimatorState))
    286.             {
    287.                 if(animator.IsInTransition(layerIndex) &&
    288.                     animator.GetCurrentAnimatorStateInfo(layerIndex).fullPathHash != previousAnimatorStateInfo.fullPathHash
    289.                     && previousAnimatorStateInfo.fullPathHash != 0)
    290.                 {
    291.                     Debug.LogError("Animator is in transition from state " + GetNameForHash(animator.GetCurrentAnimatorStateInfo(layerIndex).fullPathHash)
    292.                         + " whereas " + GetNameForHash(previousAnimatorStateInfo.fullPathHash) + " was expected;"
    293.                         + " (going to " + GetNameForHash(animator.GetNextAnimatorStateInfo(layerIndex).fullPathHash) + ") - " + Time.frameCount);
    294.                     Debug.Break();
    295.                 }
    296.                 else if(waitingToEnterInNewAnimatorState)
    297.                 {
    298.                     if(currentAnimatorStateInfo.fullPathHash == currentState.stateHash)
    299.                     {
    300.                         waitingToEnterInNewAnimatorState = false;
    301.                         previousAnimatorStateInfo = currentAnimatorStateInfo;
    302.                         if(debugLogDetail)
    303.                         {
    304.                             Debug.Log("Did enter in awaited animator state with remaining time "
    305.                                 + (currentAnimatorStateInfo.loop ? " (looping) " : ((1f - currentAnimatorStateInfo.normalizedTime) * currentAnimatorStateInfo.length).ToString())
    306.                                 + " - " + Time.frameCount + " - " + Time.time);
    307.                             Debug.Log("Set previousAnimatorStateInfo to " + GetNameForHash(previousAnimatorStateInfo.fullPathHash));
    308.                         }
    309.                     }
    310.                     else
    311.                     {
    312.                         Debug.LogError("Animator for " + pawn.gameObject.name + " changed state but didn't reach expected one: "
    313.                             + "Current animator state : " + currentAnimatorStateInfo.fullPathHash + " / " + GetNameForHash(currentAnimatorStateInfo.fullPathHash)
    314.                             + " And expected animator state: " + currentState.stateHash + " / " + GetNameForHash(currentState.stateHash) + " - " + Time.frameCount);
    315.                         if(debugLogDetail)
    316.                             Debug.Break();
    317.                     }
    318.                 }
    319.                 else if((currentState.HasAutomaticAnimatorEndTransition() || currentState.IsDefault()) &&
    320.                     (animator.IsInTransition(layerIndex) || lastStateManualChangeFrame != Time.frameCount))
    321.                 {
    322.                     if(debugLogDetail)
    323.                     {
    324.                         Debug.Log("Transition started to " + GetNameForHash(currentAnimatorStateInfo.fullPathHash)
    325.                             + (animator.IsInTransition(layerIndex) ? (" from " + GetNameForHash(animator.GetCurrentAnimatorStateInfo(layerIndex).fullPathHash)) : "(not in transition anymore)")
    326.                             + "; going to new pawn state controller state. " + Time.frameCount + " - " + Time.time);
    327.                     }
    328.                     GoToState(currentAnimatorStateInfo.fullPathHash, true);
    329.                 }
    330.                 else if(lastStateManualChangeFrame == Time.frameCount && previousState.HasAutomaticAnimatorEndTransition())
    331.                 {
    332.                     previousAnimatorStateInfo = currentAnimatorStateInfo;
    333.                 }
    334.                 else
    335.                 {
    336.                     Debug.LogError("Error: animator for " + pawn.gameObject.name + " unexpectedely changed state; CurrentState " + currentState.ToString() + " / In transition: " + animator.IsInTransition(layerIndex)
    337.                         + " with current " + GetNameForHash(currentAnimatorStateInfo.fullPathHash) + " and previous " + GetNameForHash(previousAnimatorStateInfo.fullPathHash) + " - " + Time.frameCount);
    338.                     if(debugLogDetail)
    339.                         Debug.Break();
    340.                 }
    341.             }
    342.             else if(waitingToEnterInNewAnimatorState)
    343.             {
    344.                 Debug.LogWarning("Warning: waiting to enter in new animator state " + GetNameForHash(currentState.stateHash) + " but still in previous one " + GetNameForHash(currentAnimatorStateInfo.fullPathHash) + ". " + Time.frameCount);
    345.                 if(animator.IsInTransition(layerIndex))
    346.                 {
    347.                     Debug.LogWarning(">Animator is in transition from " + GetNameForHash(animator.GetCurrentAnimatorStateInfo(layerIndex).fullPathHash)
    348.                         + " to " + GetNameForHash(currentAnimatorStateInfo.fullPathHash));
    349.                 }
    350.                 else
    351.                 {
    352.                     Debug.LogWarning(">Animator is playing " + GetNameForHash(currentAnimatorStateInfo.fullPathHash), pawn);
    353.                 }
    354.                 if(debugLogDetail)
    355.                     Debug.Break();
    356.             }
    357.         }
    358.  
    359.         if(currentState != null)
    360.         {
    361.             currentState.LateUpdate();
    362.         }
    363.     }
    364.  
    365.     protected virtual void ApplyState()
    366.     {
    367.         if(pawn.animator.IsInTransition(layerIndex))
    368.         {
    369.             if(debugLogDetail)
    370.             {
    371.                 Debug.Log("Update previous AnimatorStateInfo from " + GetNameForHash(previousAnimatorStateInfo.fullPathHash)
    372.                     + " to " + GetNameForHash(pawn.animator.GetCurrentAnimatorStateInfo(layerIndex).fullPathHash)
    373.                     + " because applying state with animator still in transition - " + Time.frameCount);
    374.             }
    375.             previousAnimatorStateInfo = pawn.animator.GetCurrentAnimatorStateInfo(layerIndex);
    376.         }
    377.  
    378.         AnimatorStateInfo asi = pawn.animator.GetCurrentAnimatorStateInfo(layerIndex);
    379.         float transitionDuration = currentState.GetCrossfadeDuration(previousState);
    380.         float normalizedTransitionDuration = transitionDuration / asi.length;
    381.         if(forceNextCrossFadeInstant)
    382.         {
    383.             transitionDuration = 0f;
    384.             normalizedTransitionDuration = 0f;
    385.             forceNextCrossFadeInstant = false;
    386.         }
    387.         float normalizedTime = currentState.GetCrossfadeNormalizedTime(previousState, currentAnimatorStateInfo.normalizedTime);
    388.         if(debugLogDetail)
    389.         {
    390.             Debug.Log("> Start crossfade to " + GetNameForHash(currentState.stateHash) + " with duration " + transitionDuration + " and normalizedTime " + normalizedTime
    391.                 + " with animator in current state " + GetNameForHash(asi.fullPathHash)
    392.                 + (asi.loop ? " (looping) " : (" with remaining time " + ((1f - asi.normalizedTime) * asi.length)))
    393.                 + (pawn.animator.IsInTransition(layerIndex) ? (" and in transition to " + GetNameForHash(pawn.animator.GetNextAnimatorStateInfo(layerIndex).fullPathHash))
    394.                     : " and not in transition ")
    395.                 + " / PSC currentAnimatorStateInfo normalized time: " + currentAnimatorStateInfo.normalizedTime
    396.                 + " - " + Time.deltaTime + " - " + Time.time);
    397.         }
    398.         animator.CrossFade(currentState.stateHash, normalizedTransitionDuration, layerIndex, normalizedTime);
    399.         waitingToEnterInNewAnimatorState = true;
    400.     }
    401.  
    402.     public virtual string GetNameForHash(int hash)
    403.     {
    404.         return HashManager.Instance.GetNameForHash(hash);
    405.     }
    406. }
    407.  
     
    Reverend-Speed likes this.
  37. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    @Grhyll HOLY MOTHERFUCKING S***BALLS

    uh THANKS?!?!!!!!! Thank you! (F***!)

    I can't absorb this now, but rest assured that a bunch of us will be devouring this over the next few days--!

    Again, thank you!

    --Rev
     
    TheKingOfTheRoad likes this.
  38. stefa-no

    stefa-no

    Joined:
    Mar 20, 2018
    Posts:
    1
    Hi!

    I'm @Reverend-Speed 's "fellow coder". I wanted first of all to thank all of you, everybody's help is being invaluable to progress with this project!

    Not only your suggestions and explanations have been extremely helpful to clarify the pros, cons and limitis of mechanim and animation controller, but all the alternative plugins that you have shared have been very useful!

    I have been testing the waters with some of those plugins, in particular I tried to replicate the same controller with 3 different plugins: AnimatitorStateMachineUtil, UnityAnimatorEvents, and @Baste 's AnimationPlayer.

    The first one I tried was AnimatorStateMachineUtil, attracted by its simple yet effective functionality: "Allows methods within your MonoBehaviours to be notified by the current state of an Animator. This turns the Unity Animator component into a powerful state machine." Basically this tool allowed for state machine behavior since before they were introduced, and it an arguably much more organized way. Since official behavior have been introduced, the tool seems to be "also" based on native state machine behavior, so I'm not sure how much more "reliable" it is. Also, it does not really resolve any of the Animator Controller problems, although it helps soothe some of them through a more centralized and understandable interface for state machine behaviors.

    UnityAnimatorEvents is a good plugin, allowing to easily set up events to control the animator controller at specific moments (start/end of entering a state, start/end of exiting a state, and at specific point in the animation). In this way, it basically obtains in a different way the same outcome of AimatorStateMachineUtil, but with the additional value of a GUI for things like animation previews. I admit I did not test this as much as the other two, but I found it a bit less reliable than AnimatorStateMachineUtil.

    Finally, what I'm currently having a blast with is AnimationPlayer! @Baste I can't thank you enough for sharing this with us! I love the simplicity of API and how fast it is to answer to inputs! I'm currently using it with good results, and am seriously thinking of keeping it as our main plugin! One side-effect of using it is that you have to completely ditch the Animator Controller, which as a """"""visual programming"""""" tool is fairly useful, and allows to handle things like animation previews of transitions, blendtrees etc quite nicely.
    Since I already had an animator controller layer with all the animations and transitions I needed (used for the two previous plugins) I created a small add-one to your plugin, a loader to add all the states and transitions I had to AnimationPlayer. Its quite raw and extremely limited atm, as I preferred to not dedicate to much time to it for now, but I wonder if it might evolve into an actual Animator Controller -> AnimationPlayer "interpreter", so that a user can create a state machine with Animator Controller, and then use the loader to load all the animations, transitions and events to AnimationPlayer, and generate a script to "run" it. I'm not sure if this makes sense atm, but I though it might be something interesting to consider for the future.
    In any case thanks again for sharing the project, it's being extremely valuable so far! I might have some questions to ask you regarding certain details of how AnimationPlayer work (if I can't figure it out by myself in the near future), but that is probably more suited for another moment/thread.


    Finally, @Grhyll , WOW. That's really amazing. I'm having a bit of a hard time following some bits of the script not knowing how a PawnState class sort of look like/work, but then again it would be hard to fully understand it anyway without using it. Still, it indubitably an extremely useful resource, I can get a lot of inspiration from that on things like how to actually build a framework out of the scripting mess that currently compose our character controller, and so much more! Thanks.



    Interestingly, most of the solutions suggested here, either through videos, plugins or snippets, end up avoiding away a lot of what Animator Controller is designed to do. In particular, most of the solutions that do not involve completely substituting Animator Controller with a different plugin and up on doing things like making 90% of the transitions from/to Any State (if I understood @Grhyll correctly, even Furi and their current project rely on this tactic), which kinda defeats the point of having a visual state machine in the first place! It's honestly a real shame to say, but for what I've seen so far, it seems like Animator Controller is fundamentally badly designed, especially for reactive kinds of gameplay.
     
  39. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,392
    Hey, thanks for the nice words!

    I don't have time to make the AnimationPlayer good enough to ship at the moment - while I tinker away at it and fix bugs and such, I haven't gotten around to making a proper test suite or fixing the UI. The UI is especially a sore point - I want to fix it, but I want to fix it in UI Elements, and our project is locked on 2018.4.

    Once I get time to do that (probably some time next year), I'll probably create a proper thread somewhere and whatnot.

    So, thanks for the patience, and beware the bugs! If you have questions or feedback, you can create a thread and ping me, or PM me, or just raise an issue on GitHub. The last one is probably the best.


    Down the line, I do want a graph-view of the AnimationPlayer. I think it's really useful to see the states laid out, and to visualize the defined transitions with clickable arrows. My main problem with the AnimatorController is that it can only follow those lines if you're using it's transitions.
     
  40. Reverend-Speed

    Reverend-Speed

    Joined:
    Mar 28, 2011
    Posts:
    185
    Holy cow, @Baste , this sounds like an even more major endevour. =D Hopefully we can be a little useful...!

    In passing, I was speaking to Matthew Wegner of Aztez and he mentioned that he had a video on the production of that game, with some relevant information for developers of this genre... so I'm gonna drop this here!

    Thanks so much for your attention, folks. I think it's down to @stefa-no and myself to start seriously committing grievous errors in code now...!
     
    Last edited: Oct 3, 2019