Search Unity

[Released] Mecanim Callbacker

Discussion in 'Assets and Asset Store' started by NeonTanto, Sep 17, 2018.

  1. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    forum_post-png[1].png
    Hi all!

    We are a small independent team called “Holy Shovel Soft” and today we want to present to you an asset called “Mecanim Callbacker”. This asset was developed for our not yet announced game, but it’s growing up to be an independent product.
    So, what is it? It’s complex and flexible solution for writing code which must work with Mecanim animator. This asset provides seamless, intuitive and useful way of writing code with tons of events and callbacks. Now your code will no longer bloat due to the if-else hell. All you have to do is to subscribe to the particular event of the particular state.
    Asset is currently in beta stage.

    First of all "Why this asset?"
    We as a team have confronted a problem while developing our game. The bigger our FSMs were getting and more complex out logic of working with animation code was becoming; the more fixes and corrections we had to make to old code and harder it was getting to control what was actually going on. The amount of branching was getting out of control. Every iteration to refine logic was becoming a war with Mecanim. Thus, we decided to write a framework that would simplify and systematize our approach to writing code that worked with Mecanim. We also didn't want to double up the FSM built into Mecanim with our own code, but to widen it's functionality to a full FSM framework.

    To better understand what we are talking about, lets look at a few examples and how they can be solved with our asset.

    Lets talk about base entities and concepts of MC.

    1. CallbackerController
    Core component of MC is Controller. This component is the enter point for all API. All entities can be set up from two points: presets (assets which are created in editor) and from code.

    2. Categories.
    This entity consists of states grouped into one entity to work as one whole in all callbacks.
    This entity can be used for:
    • Complex update and transition logic for specific states (even from different mecanim layers) unified with one idea (idles, run states, crouch states).
    • Show/Hide particle effects/game objects/sound effects in OnStart and OnEnd events of specific states.
    For example we create Category with these states: “Base Layer.Idle 1”, “Base Layer.Idle 2”, “Base Layer.Idle 3”, and call it “Idles”. In presets editor it will look like this:
    upload_2018-9-18_2-26-17.png
    Lets look closer what we have in category:
    • Name - it’s very simple. Just name of category.
    • Excluding - if set to true, category processes all states not included in list of states.
    • Layer weight tolerance - weight of layer after which states start being processed.
    • States - list of included states.
    And in code we can work with categories in this manner (there are other possible scenarios besides examples below).

    Code (CSharp):
    1.  
    2. //Get created in editor category by name
    3. //(for example its consist from three states: Idle 1, Idle 2, Idle 3)
    4. controller.Categories["Idles"].BaseEvents.OnTick += sourceController =>
    5. {
    6.     //This code will be called every frame when one of our states active
    7.     //But its called once per frame, all category work as one entity even if we
    8.     Debug.Log("I'm in 'Idles' category.");
    9. };
    10.  
    11. controller.Categories.Excluding["Idles"].BaseEvents.OnEnd += sourceController =>
    12. {
    13.     //This code will be called every time when we exit from any category except 'Idles'
    14.     Debug.Log("I'm exit from some category.");
    15. };
    16.  
    17. //We can create Category dynamicly (inversed state list supported)
    18. //...and we can cashe it =)
    19. var walksCategory = controller.Categories.FormStates["BaseLayer.Walk 1", "BaseLayer.Walk2"];
    20. walksCategory.BaseEvents.OnTick += sourceController =>
    21. {
    22.     Debug.Log("So, I'm walking now");
    23. };
    24.  
    25. //And actualy we can work with group of categories as with one category!
    26. //...and yes, we can use dynamicly created category too, and inversed lists supported
    27. controller.Categories["Idles", walksCategory.Name].BaseEvents.OnTick += sourceController =>
    28. {
    29.     Debug.Log("So, I'm walking now... but maybe in Idle... or in both");
    30. };
    31.  
    32. //Cool feature its that categories provide transitions events too
    33. walksCategory.TransitionEvents.OnTick += sourceController =>
    34. {
    35.     Debug.Log("I'm in transition from (or to) any state to (or from) state inside walk category.");
    36. };
    37.  
    38. //It's support direction of transition
    39. walksCategory.TransitionEvents.From.OnStart += sourceController =>
    40. {
    41.     Debug.Log("I'm in start of transition from any state to state inside walk category.");
    42. };
    43.  
    44. //And also can be used specific states
    45. walksCategory.TransitionEvents.To["BaseLayer.Crouch 1", "BaseLayer.Crouch 2"].OnEnd += sourceController =>
    46. {
    47.     Debug.Log("I'm in end of transition from state inside walk category to 'Crouch 1' or 'Crouch 2' state.");
    48. };
    Sample usecase of categories:

    Let's imagine a situation, when we have a character, that can only attack while standing, for example, an archer. To solve this problem in the standard approach we need to get current state from the animator, compare it to the list of states that would consider the character to be running, and only then decide whether to shoot or not to shoot.
    Code (CSharp):
    1. private Animator animator;
    2. private string[] runStates;
    3.  
    4. private void Attack()
    5. {
    6.     //...
    7. }
    8.  
    9. private void Update()
    10. {
    11.     var isRun = false;
    12.     for (var i = 0; i <= animator.layerCount - 1; i++)
    13.     {
    14.         var stateInfo = animator.GetCurrentAnimatorStateInfo(i);
    15.         for (var j = 0; j <= runStates.Length - 1; j++)
    16.         {
    17.             if (stateInfo.fullPathHash == Animator.StringToHash(runStates[j]))
    18.             {
    19.                 isRun = true;
    20.                 break;
    21.             }
    22.         }
    23.  
    24.         if (isRun)
    25.         {
    26.             break;
    27.         }
    28.     }
    29.  
    30.     if (Input.GetKeyUp(KeyCode.A))
    31.     {
    32.         if (!isRun)
    33.         {
    34.             Attack();
    35.         }  
    36.     }
    37. }
    At the same time we can get the same result much easier, by creating a category from states when the character is moving, and to subscribe to its Update; that would be much easier and more transparent.
    Code (CSharp):
    1. private string[] runStates;
    2.  
    3. private void Attack()
    4. {
    5.     //...
    6. }
    7.  
    8. protected override void OnAttached(CallbackerController controller)
    9. {
    10.     controller.Categories.FormStates.Excluding[runStates].BaseEvents.OnTick += sourceController =>
    11.     {
    12.         if (Input.GetKeyUp(KeyCode.A))
    13.         {
    14.             Attack();
    15.         }
    16.     };
    17. }
    At the same time if category is done inside a preset, we can get a separation of logic and content "from the box"

    3. Groups.
    It’s very similar to Categories. But instead of working as one its call callbacks for every grouped up state. API and Editor UI very similar to Category. But usual this entity used rarely.
    Its can be used for:
    • Process several states in one manner but in several calls (one call per state).
    • Randomize similar animations like idles/attacks in OnLoop callback
    • Add/Remove some effects based on played state or even states
    Sample usecase of groups:

    To give an example: to let our archer an ability to change the type of arrow after every cycle of animation of shooting. Without our asset it would look like this:
    Code (CSharp):
    1. private Animator animator;
    2. private string attackState;
    3. private int lastLoop;
    4.          
    5. private void SwapArrow()
    6. {
    7.     //...
    8. }
    9.  
    10. private void Update()
    11. {
    12.     for (var i = 0; i <= animator.layerCount - 1; i++)
    13.     {
    14.         var stateInfo = animator.GetCurrentAnimatorStateInfo(i);
    15.         if (stateInfo.fullPathHash == Animator.StringToHash(attackState))
    16.         {
    17.             var currentLoop = Mathf.FloorToInt(stateInfo.normalizedTime);
    18.             if (currentLoop != lastLoop)
    19.             {
    20.                 SwapArrow();
    21.             }  
    22.         }
    23.     }
    24. }
    While with our asset this problem would be solved much easier:
    Code (CSharp):
    1. private string attackState;
    2.  
    3. protected override void OnAttached(CallbackerController controller)
    4. {
    5.     controller.Groups.FromStates[attackState].BaseEvents.OnLoop += sourceController =>
    6.     {
    7.         SwapArrow();
    8.     };
    9. }

    4. Curves
    Very useful tool for creating curves and modifiers for it based on states, layer weights and Mecanim parameters. Curves can be used to directly access values and for mapping it to animator parameter (it’s useful but not necessary unlike standard unity animation curves).
    Its can be used for:
    • Sync particle parameters/sound volume/material properties with mecanim states.
    • Provide additional data for game logic (feet height, waving power, charge time) for simple states and blend trees
    How does it work? For example, we have curve called “Test Curve”. In editor it looks like this:
    upload_2018-9-18_2-28-41.png
    Some explanation of all of the above stuff:
    • Name - just the name of curve.
    • Default value - constant value which will be returned if no subcurves will be evaluated.
    • Layer blending modes - how value of subcurves must process layer weights.
    • Subcurves - list of evaluating curves.
      • Value curve - main curve.
      • State - in this state our subcurve will be evaluated.
      • Modifiers - list of modifiers
        • Value Curve - modifiers’ main value curve.
        • Interpolation - how modifier must change the resulting value depending to selected source.
        • Modifier mode - type of modifying: override, multiply, etc.
        • Modifier type - type of source: parameter or transition normalized time.
    Code (CSharp):
    1.  
    2. //Get curve by name
    3. var testCurve = controller.Curves["Test Curve"];
    4. //Get base (not modified) value of curve
    5. var testCurveBaseValue = testCurve.BaseValue;
    6. //Get fully modified value of curve
    7. var testCurveValue = testCurve.Value;
    8. //We also can create curves in runtime
    9. //but its more complex than groups and categories
    10. var newCurvePreset = new CurvePreset
    11. {
    12.     //Set mapping to variable
    13.     mapTo = controller.Variables.Floats["Some float var 1"],
    14.     //Set animator layer blending modes
    15.     layerBlendingModes = new []
    16.     {
    17.         LayerBlendingMode.Additive,
    18.         LayerBlendingMode.Override,
    19.         LayerBlendingMode.Override
    20.     },
    21.     //Set layer weight mapping curves
    22.     layerWeightMapping = new []
    23.     {
    24.         //You can use any animation curve
    25.         new AnimationCurve(),
    26.         new AnimationCurve(),
    27.         new AnimationCurve(),
    28.     },
    29.     //Fill subcurves
    30.     subcurves = new []
    31.     {
    32.         new SubcurvePreset
    33.         {
    34.             statePath = "Base Layer.Idle 1",
    35.             valueCurve = new AnimationCurve()
    36.         },
    37.         new SubcurvePreset
    38.         {
    39.             statePath = "Base Layer.Idle 2",
    40.             valueCurve = new AnimationCurve(),
    41.             //Also you can fill modifiers for subcurve
    42.             modifiers = new []
    43.             {
    44.                 new CurveModifierPreset
    45.                 {
    46.                     type = CurveModifierType.Parameter,
    47.                     mode = CurveModifierMode.Multiply,
    48.                     parameter = controller.Variables.Floats["Some float var 2"],
    49.                     interpolationCurve = new AnimationCurve(),
    50.                     valueCurve = new AnimationCurve()
    51.                 },
    52.             }
    53.         },
    54.     }
    55. };
    56.  
    57. //And pass preset to create method
    58. controller.Curves.FromPreset(newCurvePreset);
    Sample usecase of curves:

    Lets make the string of our archers bow light up depending on how far the bow is drawn. By defining a curve within the state of shooting the bow, we can request the curve value by code this way:
    Code (CSharp):
    1. private string attackState;
    2. private string stringPowerCurve;
    3.  
    4. private void SetBowStringPower(float val)
    5. {
    6.     //...
    7. }          
    8.          
    9. protected override void OnAttached(CallbackerController controller)
    10. {
    11. var curve = controller.Curves[stringPowerCurve];
    12.     controller.Groups.FromStates[attackState].BaseEvents.OnTick += sourceController =>
    13.     {
    14.         SetBowStringPower(curve.Value);
    15.     };
    16. }
    5. Events.
    Self-explanatory. It’s just events depended to state time.
    Its can be used for:
    • Detect On/Off stuff inside state (like create stone prefab in hand when dropping animation played at 0.3f time and drop this stone at 0.7f)
    • Detect one shot events like shoots, feet grounding, weapon goes to inventory
    upload_2018-9-18_2-32-24.png
    Basic examples below.
    • Name - again… just the name =)
    • Type - type of event: “One Shot” or “Between”. “One Shot” fires only when we have reached target normalized time of state. “Between” fires between two points and produces start and end callbacks as well.
    • Layer weight tolerance - the same as in categories.
    • Is guaranteed - how the event must handle situation when we don’t fire callback in this loop (e.g. due to lag). If set to false we just ignore this situation; if set to true we get all callbacks (needed count of callbacks).
    • Value - point or range for normalized time where the event must fire.
    • State - state where the event must be processed.
    And some code.

    Code (CSharp):
    1.  
    2. //For one shot events only OnFire event is called
    3. controller.Events["Some One Shot Event"].OnFire += sourceController => { Debug.Log("Some One Shot Event is fired!");};
    4. var evt1 = controller.Events["Some Between Event"];
    5.    
    6. //For Between events we can use OnStart and OnEnd too
    7. //and OnFire will be called every frame while we inside range
    8. evt1.OnStart += sourceController => { Debug.Log("Some Between Event start fired!");};
    9.  
    10. //and we can create event from code
    11. var preset = new EventPreset
    12. {
    13.     isGuaranteed = false,
    14.     type = CallbackerEventType.OneShot,
    15.     statePath = "Base Layer.Crouch 1",
    16.     timePoint = 0.5f,
    17. };
    18.  
    19. var evt2 = controller.Events.CreateFromPreset(preset);
    20. evt2.OnFire += sourceController => { Debug.Log("Base Layer.Crouch 1 in half time!");};
    Sample usecase of events:

    In this case the example is straighforward: lets define the moment the arrow is supposed to launch from the bow. My creating an event in our preset or runtime code we simply subscribe to it:
    Code (CSharp):
    1. private string shootEvent;
    2.          
    3. private void ShootArrow()
    4. {
    5.     //...
    6. }
    7.          
    8. protected override void OnAttached(CallbackerController controller)
    9. {
    10.     controller.Events[shootEvent].OnFire += sourceController =>
    11.     {
    12.         ShootArrow();
    13.     };
    14. }
    Also of note is the universality of this approach, the code is not dependent to any states and just waits for the particular event to happen. When creating the event in the preset, we can at any point in time change the targeted state, as well as the timing of the shot without changing the rest of the system.

    6. Procedures This is a cunning thing that are very useful in certain situations. Usually they are supposed to be used in the situations, when the code is supposed to be executed as if inside a category, group, or a lengthy event, but at the same time it's call doesn't occur inside the main Update/LateUpdate, but rather in PreRender/OnAnimatorIK/OnCollisionEnter, etc. This thing can’t be setted from editor, only in runtime from code.
    Code (CSharp):
    1.  
    2. //First. We must create procedure. Our have one int argument.
    3. //0-5 arguments supported.
    4. var awesomeProcedure = controller.Procedures.GetProcedure<int>("MyAwesomeProcedure");
    5. //Now we must attach action to some entity which can be "inside"
    6. //like categories, groups, between events
    7. var target = controller.Categories["Idles"];
    8. target.BindProcedure(awesomeProcedure).OnCall += (sourceController, arg1) =>
    9. {
    10.     Debug.LogFormat("MyAwesomeProcedure called in 'Idles' with {0}", arg1);
    11. };
    12. //Its can be binded by name too
    13. controller.Groups["Crouches"].BindProcedure<int>("MyAwesomeProcedure").OnCall += (sourceController, arg1) =>
    14. {
    15.     Debug.LogFormat("MyAwesomeProcedure called in 'Crouches' with {0}", arg1);
    16. };
    17. //And also its can be binded from procedure self
    18. awesomeProcedure.Binder(controller.Events["Some between event"]).OnCall += (sourceController, arg1) =>
    19. {
    20.     Debug.LogFormat("MyAwesomeProcedure called in 'Some between event' with {0}", arg1);
    21. };
    22.  
    23. //And now we can call our procedure in any place.
    24. awesomeProcedure.Call(10);
    Sample usecase of procedures:

    For example lets look at the situation, when our archer should fall when pushed, but only if he's currently in a particular state. For that, let's do the following:

    Code (CSharp):
    1. private string[] canFallStates;
    2. private IProcedure FallProcedure;
    3.          
    4. private void FallArcher()
    5. {
    6.     //...  
    7. }
    8.          
    9. protected override void OnAttached(CallbackerController controller)
    10. {
    11.     FallProcedure = controller.Procedures.GetProcedure("FallArcher");
    12.     controller.Categories.FormStates[canFallStates].BindProcedure(FallProcedure).OnCall += sourceController =>
    13.     {
    14.         FallArcher();
    15.     };
    16. }
    17.  
    18. private void OnCollisionEnter(Collision other)
    19. {
    20.     FallProcedure.Call();  
    21. }
    As you can see, everything is still pretty simple and at the same time, functional and fluid.

    7. Presets
    Lets talk a bit about presets themselves. Everything that we've talked about above can be created in runtime code, but it's not as comfortable, if we are talking not about prototyping, but about development of big and complex projects. In this case it's easier to use presets - they are assets that store setting of entites (except procedures, they can only be created in runtime). Presets can also be more complex and support the functionality of redefining entities. For example, if we have OverrideAnimatorController that changes the archers shooting animation, we can also create WrapperPreset, that will wrap our original preset and change, for example, the timing of the shot event. At the same time, let us reiterate - the code that's processing the shot itself is no way changed or affected. All entities that are supported by presets can be redefined. Besides redefining, you can also expand / create new entities. You can stack wrapping - you can wrap a preset that is already wrapping another preset.



    That’s not the complete breakdown, just the main points. If you have any questions, suggestions, requests - speak freely. We are open to new ideas and are glad to answer any of your questions.

    And ofcourse links:
    1. Asset store page
    2. Asset web site
    3. Support email
     
    Last edited: Sep 18, 2018
    ZhavShaw, Demigiant and nuverian like this.
  2. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Looks really great and flexible indeed with a lot of thought put into it. Might as well just be the answer to my problems with Mechanim/Code syncing!

    Well done and good job on the release! :)
     
    NeonTanto and mf_andreich like this.
  3. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    I'm glad to hear it! If there are any questions, we will be happy to help!
     
  4. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    Main post updated with simple usecases.
     
  5. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    Hi all! =) Nice info!

    We are still interested in final beta-testing of our asset, in order to finalize its version as soon as possible. Thus, we are offering three free copies of the asset to those who are willing to test its in their projects, experiments, etc. If you are interested in this fun, please write to us at team@holyshovelsoft.com, and put "MecanimCallbacker beta test" in your subject line.
     
  6. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Oh :) Well, I just bought it, but hey I don't regret it at all.
    Meanwhile, I have a small error going on.
    It's not a big deal, since this only happens on Unity 2018.3 beta though.
    This exception is thrown once and at the moment I close the Preset Editor.
    Code (CSharp):
    1. NullReferenceException: Object reference not set to an instance of an object
    2. MecanimCallbacker.Reflection.ReflectedClass.Call (System.String name, System.Type[] types, System.Object[] parameters) (at Assets/HolyShovelSoft/MecanimCallbacker/Editor/Reflection/ReflectedClass.cs:235)
    3. HolyShoveSoft.MecanimCalbacker.Editor.Utils.AvatarPreviewer.OnDestroy () (at Assets/HolyShovelSoft/MecanimCallbacker/Editor/Utils/AvatarPreviewer.cs:77)
    4. MecanimCallbacker.Utils.AvatarPreviewHelper.Destroy () (at Assets/HolyShovelSoft/MecanimCallbacker/Editor/Utils/AvatarPreviewHelper.cs:668)
    5. MecanimCallbacker.GuiHolders.PresetGuiHolder.Dispose () (at Assets/HolyShovelSoft/MecanimCallbacker/Editor/GuiHolders/PresetGuiHolder.cs:1032)
    6. MecanimCallbacker.PresetEditorWindow.OnDestroy () (at Assets/HolyShovelSoft/MecanimCallbacker/Editor/PresetEditorWindow.cs:621)
    7. UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr)
     
  7. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    Hi!
    Thanks for bug report.
    Yes we know about this issues with 2018.2+ Unity, and already fix it. This fix will be sended into Asset Store into 1-2 days, because right now we have some issues with dictionary serialization and try resolve it.
     
  8. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    Version 0.9.2b sended to review on Asset Store.
     
  9. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    We start working on some integrations. Right now its FlowCanvas, but we plan integrate Mecanim Callbacker with Bolt too!
     

    Attached Files:

  10. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    @HolyShovel First of all, thanks for fixing the problem already, but also thanks as well for working on an integration with FlowCanvas! That looks great and I can already see the potential of an integration between the two tools :)
    Looking forward to see what you come up with!
     
  11. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    Glad to hear that issues gone in current version!

    About FC integration - we have good news! All dirty job done! Right now we work on docs/guids/node description. Thats how sample scene reworked with FC looks:

    Full version (WARNING BIG PICTURE).

    Release of this package planed in near weak with realease of new version of our asset on store.
     
    nuverian likes this.
  12. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    Good news!

    Version 0.9.3b alive! Now with FlowCanvas integration. Integration package can be downloaded here.​

    Twitter FC.png
     
    Last edited: Oct 30, 2018
    nuverian and codestage like this.
  13. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    I missed your previous post on FC, but this is awesome! Thank you so much for making this integration! :)
     
    NeonTanto likes this.
  14. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    Work on integration with Bolt started!

     
  15. spinaLcord

    spinaLcord

    Joined:
    May 25, 2015
    Posts:
    29
    Hey bought this yesterday, it's quite interesting asset. I don't know this is a unity version (2018
    .3) related issue, but i noticed that your asset have really high impact on unity editor performance, even in a vanilla project without any component or window open(related to this asset).
     
    NeonTanto likes this.
  16. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    Hi!
    Can you give more info about this issue? You are second user who tald about this... But I cant "catch" this bug on my two workstations. Which kind of performance drop you see?

    UPD. I think I found problem... Or another problem with in editor performance. I send fix to asset store today, but If you want, I can send new version to you for early access and for test this point on your side.
     
    Last edited: Feb 5, 2019
    spinaLcord likes this.
  17. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    Information about Bolt integration.
    Right now I will wait of release Bolt 2.0, because it can have drasticaly different API and visual style. And after this release I finish work on integration package.
     
  18. spinaLcord

    spinaLcord

    Joined:
    May 25, 2015
    Posts:
    29
    i would come back to your offer, to get a updated version. Thanks. I send you a email along with my ordernumber.
     
    NeonTanto likes this.
  19. NeonTanto

    NeonTanto

    Joined:
    Jun 19, 2013
    Posts:
    156
    Yep, I will send new version to you. Hope that your issues resolved in this build.
     
    spinaLcord likes this.
  20. spinaLcord

    spinaLcord

    Joined:
    May 25, 2015
    Posts:
    29
    Yip, working and running well now. Thanks. :)
     
    NeonTanto likes this.