I'm developing an RPG. I have 3D models with accompanying animations for all units/characters. Models and animations are made in Blender and exported as .glb.

I want to have an AnimationTree that can switch between these animations. These animations are standardized; every unit has a Run animation, Idle animation and so forth. I have a base class "Unit" that looks like this :

(UnitAnimation is an AnimationTree)
Since all Units have the same animations, I would like to create an AnimationTree blend tree graph that is generic and can be used by all Units, like this:

AnimationTrees require an AnimationPlayer to be able to add specific Animation-nodes to its node-graph ("Run", "Idle", etc). However, the AnimationPlayer for each 3D model with these animations exists in the glb-scene, and are thus added at a later stage when the Unit node is extended.

My solution (that is quite ugly):
In my UnitAnimation-node, I have a dummy AnimationPlayer with empty animations named "Run", "Idle", etc. I use this AnimationPlayer to create the blend tree graph.

When I add the glb, I make UnitAnimation target that node's AnimationPlayer instead. This works most of the time.

The big problem:
Some models don't have all animations; either because I haven't implemented them yet, or because that model wouldn't need it. That makes the AnimationTree stop working, since it cannot find all animations in its graph, even if those animations aren't used.

Ideally I'd like for the AnimationTree to play a default animation if it cannot find the specific one.

Questions:
1. Is there a better way to create a reusable AnimationTree, that also can account for missing animations?
2. Also, is it possible to create AnimationTree-graphs with code instead of visual programming?

  • trizZzle replied to this.
  • trizZzle Yeah the whole design is a bit confusing tome. Being able to save as .tres is nice, but as you say: if you need to assign the animations to each node anyway, then what is the point? Feels like the AnimationTree was designed for other purposes than this 😅

    But! I managed to create a tool script that satisfied my needs (for now at least). It loads in all animations from an AnimationPlayer and connects them to output with a Transition node. It is not perfect, but at least it works. Sure, godot prints errors when you try to run a nonexistant animation but it doesn't crash. Will probably have to add more logic for one-shots, different animations depending on weapon-type etc. A lot can probably be added in script outside of the BlendTree.

    Still feels quite hacky and dirty though :/

    But thanks for all the input! Appreciate it 🙂

    @tool
    extends AnimationTree
    class_name UnitAnimation
    
    enum Types {Bad = -1, Idle = 0, Run = 1, Attack = 2, Jump = 3, Combat = 4}
    var anim_strings = ["Idle", "Run", "Attack", "Jump", "Combat"]
    var current_anim:Types = Types.Bad
    var available_animations = []
    
    @export var add_anim_tree_nodes: bool:
        set(value):
            _add_anim_tree_nodes()
    
    func _ready() -> void:
        if not Engine.is_editor_hint():
            available_animations = get_node(anim_player).get_animation_list()
            if available_animations.size() != 0:
                play(Types.Idle)
    
    func play(type:Types):
        if current_anim != type:
            current_anim = type
            set("parameters/trans/transition_request", anim_strings[current_anim])
    
    func _add_anim_tree_nodes():
        var animation_player:AnimationPlayer = get_node(anim_player)
        if not animation_player:
            print("No anim_player found!")
            return
    
        tree_root =  AnimationNodeBlendTree.new()
        var blend_tree:AnimationNodeBlendTree = tree_root
        var trans = AnimationNodeTransition.new()
        trans.xfade_time = 0.2
        blend_tree.add_node("trans", trans)
        blend_tree.connect_node("output", 0, "trans")
    
        for anim_key in animation_player.get_animation_list():
            trans.add_input(anim_key)
            var anim = AnimationNodeAnimation.new()
            anim.animation = anim_key
            blend_tree.add_node(anim_key, anim)
            blend_tree.connect_node("trans", trans.input_count-1, anim_key)

    PoorlyDrawnCircle

    1. I haven't found anything about that.
    2. I tested it. It worked in editor, but at runtime it somehow didn't. I saw the locomotion parameters updated, but the animations didn't play. I will test again sometime. It's a bit annoying, well see for yourself:

    Result:

    Animation Library I have that I load at runtime:

    The result (code below):
    BlendTree:

    Blendspace1D:

    
    
     @tool
    extends AnimationTree
    
    @export var animation_player : AnimationPlayer
    
    const HUMAN_ANIMATION_LIB = preload("res://animations/HumanAnimationLib.res")
    
    
    func _ready() -> void:
    	init_animations()
    
    func init_animations():
    
    	animation_player.add_animation_library("Human", HUMAN_ANIMATION_LIB)
    	anim_player = animation_player.get_path()
    
    
    	var animation_blend_tree := AnimationNodeBlendTree.new()
    	tree_root = animation_blend_tree
    
    	# create locomotion blendspace1d and add 2 blend points
    	var locomotion := AnimationNodeBlendSpace1D.new()
    	tree_root.add_node("Locomotion", locomotion, Vector2(-150, -100))
    
    	var anim_idle := AnimationNodeAnimation.new()
    	anim_idle.animation = "Human/anim_idle"
    	locomotion.add_blend_point(anim_idle, 0)
    
    	var anim_walk := AnimationNodeAnimation.new()
    	anim_walk.animation = "Human/anim_walk"
    	locomotion.add_blend_point(anim_walk, 1)
    
    
    	# create jump animation
    	var anim_jump := AnimationNodeAnimation.new()
    	anim_jump.animation = "Human/anim_jump"
    	tree_root.add_node("JumpAnimation", anim_jump, Vector2(-150, 100))
    	print(anim_jump.animation)
    
    	# create jump one shot
    	var oneshot_jump := AnimationNodeOneShot.new()
    	tree_root.add_node("OneShotJump", oneshot_jump, Vector2(80, 0))
    
    	# connect nodes: to node , input index, from node
    	tree_root.connect_node("OneShotJump", 0, "Locomotion",)
    	tree_root.connect_node("OneShotJump", 1, "JumpAnimation")
    	tree_root.connect_node("output", 0, "OneShotJump")

    To change animations:

    	var jump := tree_root.get_node("JumpAnimation") as AnimationNodeAnimation
    	print("Old JumpAnimation: %s" % jump.animation)
    	jump.animation = "Human/anim_idle"
    	print("New JumpAnimation: %s" % jump.animation)

    Thanks for the reply! I have started to tinker a bit and it seems like we came up with similar designs.

    The following tool-script gets one of the animations from an AnimationPlayer and connects it to the output.

    var animation_player:AnimationPlayer = get_node(anim_player)
    available_animations = animation_player.get_animation_list()
    
    var anim = AnimationNodeAnimation.new()
    var anim_key = available_animations[0]
    anim.animation = anim_key
    blend_tree.add_node(anim_key, anim)
    
    blend_tree.call_deferred("connect_node", "output", 0, anim_key)

    It needs some more work, but I think I know how to do it 🙂 When I finish it I'll post it here for future reference

    I just discovered that when right clicking in the animation tree you can select "load" which can load AnimationNodes (which are Resources).

    So in the filesystem you can create -> new resource -> AnimationNodeAnimation and define the animation name.

    Same thing works for all the other animation nodes.
    Another way is to achieve this is to change the path to a location inside res.

    But I'm not sure if all this is useful in any way. You can load those into an animation tree, but having to rightclick -> load -> and browse to file location isn't faster than creating new ones and you can't drag them in from the file system.
    And most importantly: you can't assign animations to it. You still will need an animation player that has the animations with the names you set.

    I really don't know why this was designed this way. Am I overlooking something?
    In Unity you can simply drag and drop animations in and out, can create the entire blend tree without relying on an animator, can leave the animations empty etc.

    Well, right now my best guess to achieve reusable blend trees is to create one that has all required animation nodes inside it, change its path to res://... and depending on the character swap out the animation library on the animation player.

    I guess that's all we need? But I'm not happy. 🫨

    Ah yes, overriding the attack animation according the the carried weapon is still a thing.

      trizZzle Yeah the whole design is a bit confusing tome. Being able to save as .tres is nice, but as you say: if you need to assign the animations to each node anyway, then what is the point? Feels like the AnimationTree was designed for other purposes than this 😅

      But! I managed to create a tool script that satisfied my needs (for now at least). It loads in all animations from an AnimationPlayer and connects them to output with a Transition node. It is not perfect, but at least it works. Sure, godot prints errors when you try to run a nonexistant animation but it doesn't crash. Will probably have to add more logic for one-shots, different animations depending on weapon-type etc. A lot can probably be added in script outside of the BlendTree.

      Still feels quite hacky and dirty though :/

      But thanks for all the input! Appreciate it 🙂

      @tool
      extends AnimationTree
      class_name UnitAnimation
      
      enum Types {Bad = -1, Idle = 0, Run = 1, Attack = 2, Jump = 3, Combat = 4}
      var anim_strings = ["Idle", "Run", "Attack", "Jump", "Combat"]
      var current_anim:Types = Types.Bad
      var available_animations = []
      
      @export var add_anim_tree_nodes: bool:
          set(value):
              _add_anim_tree_nodes()
      
      func _ready() -> void:
          if not Engine.is_editor_hint():
              available_animations = get_node(anim_player).get_animation_list()
              if available_animations.size() != 0:
                  play(Types.Idle)
      
      func play(type:Types):
          if current_anim != type:
              current_anim = type
              set("parameters/trans/transition_request", anim_strings[current_anim])
      
      func _add_anim_tree_nodes():
          var animation_player:AnimationPlayer = get_node(anim_player)
          if not animation_player:
              print("No anim_player found!")
              return
      
          tree_root =  AnimationNodeBlendTree.new()
          var blend_tree:AnimationNodeBlendTree = tree_root
          var trans = AnimationNodeTransition.new()
          trans.xfade_time = 0.2
          blend_tree.add_node("trans", trans)
          blend_tree.connect_node("output", 0, "trans")
      
          for anim_key in animation_player.get_animation_list():
              trans.add_input(anim_key)
              var anim = AnimationNodeAnimation.new()
              anim.animation = anim_key
              blend_tree.add_node(anim_key, anim)
              blend_tree.connect_node("trans", trans.input_count-1, anim_key)