Godot Version
Godot v4.3.stable
Question
I was recently inspired by this series of videos, which takes assets from Super Mario World in an attempt to recreate its particular gamefeel. The assets shown are extracted from my own copy of Devil May Cry 5. The character is Nero and the mechanic I am trying to replicate is his ability to swap arms (my testing arm of choice is called Quicksilver). Each arm has its own set of bones and animations controlling them, but Nero’s various animations (Idle, Run, Jump, etc) also drive the animation of his currently equipped arm. How could I achieve this at scale in Godot?
Setup
I have Nero imported from an FBX, with a handful of animations (such as Idle) attached to the animation player. As you can see, his right hand is missing, but the bones are still there and are used in his animations.
I have Quicksilver imported from an FBX, with its own unique set of animations. The skeleton only goes up to the elbow joint, and also includes many additional bones.
I have a scene called Testing in which the FBX files are instantiated like so, and below is the entirety of my testing script. The relevant functions are do_merge
and merge
.
extends Node3D
@onready var nero: Node3D = $"nero-rq-qs-walk"
@onready var quicksilver: Node3D = $quicksilver
# name of bone in primary skeleton which corresponds to the root bone of secondary skeleton
@export var virtual_root: String
func _ready() -> void:
do_merge()
do_animations()
func do_merge():
var nero_skeleton := nero.get_child(0) as Skeleton3D
var quicksilver_skeleton := quicksilver.get_child(0) as Skeleton3D
var primary_root: int = nero_skeleton.find_bone(virtual_root)
var secondary_root: int = 0
merge(nero_skeleton, quicksilver_skeleton, primary_root, secondary_root)
var quicksilver_meshes := quicksilver_skeleton.get_children()
for mesh in quicksilver_meshes:
mesh.reparent(nero_skeleton)
func do_animations():
$"nero-rq-qs-walk/AnimationPlayer".play("quicksilver-animations/wp00_011_GT_QS_alpha (94 frames) ID_ 0")
# recursively merges two skeletons together by name, adding new branches from {secondary} to {primary} along the way
func merge(primary: Skeleton3D, secondary: Skeleton3D, primary_root: int, secondary_root: int):
# if either skeleton runs out of bones, stop merging
if primary_root >= primary.get_bone_count() || secondary_root >= secondary.get_bone_count():
return
var primary_bones := get_bone_children_names(primary, primary_root)
var secondary_bones := get_bone_children_names(secondary, secondary_root)
var shared_bones := intersection(primary_bones, secondary_bones)
var new_bones := difference(secondary_bones, primary_bones)
for bone in new_bones:
add_branch(secondary, primary, secondary.find_bone(bone), primary_root)
for bone in shared_bones:
merge(primary, secondary, primary.find_bone(bone), secondary.find_bone(bone))
# recursively adds bones from {source} starting at {source_root} to {dest} starting at {dest_root}
func add_branch(source: Skeleton3D, dest: Skeleton3D, source_root: int, dest_root: int):
if dest.find_bone(source.get_bone_name(source_root)) != -1:
return
var added := dest.add_bone(source.get_bone_name(source_root))
dest.set_bone_parent(added, dest_root)
dest.set_bone_pose(added, source.get_bone_pose(source_root))
dest.set_bone_rest(added, source.get_bone_rest(source_root))
for child in source.get_bone_children(source_root):
add_branch(source, dest, child, added)
func get_bone_children_names(source: Skeleton3D, bone_idx: int) -> Array[String]:
var temp: Array[String] = []
for idx in source.get_bone_children(bone_idx):
temp.append(source.get_bone_name(idx))
return temp
func intersection(a: Array[String], b: Array[String]) -> Array[String]:
var temp: Array[String] = []
for string in a:
if b.has(string):
temp.append(string)
return temp
func difference(a: Array[String], b: Array[String]) -> Array[String]:
var temp: Array[String] = []
for string in a:
if not b.has(string):
temp.append(string)
return temp
The script takes the two skeletons and merges them, preserving the original structure of Nero and adding bones which are found in Quicksilver. It then plays an animation from Quicksilver’s animation library (Animation > Animation > Manage Animations > Save as).
This script works! If I play Quicksilver’s animation, it plays properly, and likewise if I play Nero’s Idle animation. If I added an extra AnimationPlayer, I am confident they would animate together without issue.
Therefore my main question is this: is there a built-in solution or plugin which covers this use-case?
I am aware of BoneAttachment3D, it’s the only thing referenced when searching things like “godot weapon/armor swapping”, but as far as I can tell it cannot satisfy this use-case. It works perfectly for swappable objects which logically are only skinned to one bone (sword, shield), but not if the relationship is any more complicated than that (i.e.: hand stuff, non-rigid clothing).