Thanks @Harald and @Mario, but unfortunately this wasn't a lot of help to me. I already understood how the Unity version worked. And the difference between 11-bone-node
in the example project and the Unity example is a SpineBoneNode
in Drive
mode completely overwrites the animation data in Godot. For a walk cycle, we still need the animation data. However, with some further digging into the Spine Godot source I was able to figure out a solution. In my opinion, this could be made simpler with an addition to SpineBoneNode
.
I'll walk through my solution in case anyone else stumbles onto this thread looking to do the same thing, then I'll highlight what changes I think would help.
The key to implementing this is knowing that SpineBoneNode
must have bone_mode
set to Follow
, not Drive
as is demonstrated in the example, and that the associated SpineBone
must have its transform updated to match.
Code
I've created 3 scripts/classes:
- bone_constraint.gd
- foot_constraint.gd
- ground_constraints.gd
bone_constraint.gd
This is a base class for SpineBoneNodes that act as constraints. All this class does is wrap a reference to the associated SpineBone, and provide a shortcut method for updating the bone's transform to match that of the node.
class_name BoneConstraint extends SpineBoneNode
@export var bone_name: String
var bone: SpineBone = null
func _ready():
var parent: Node = get_parent()
var spine_sprite: SpineSprite = null
while (parent):
spine_sprite = parent as SpineSprite
if spine_sprite:
bone = spine_sprite.get_skeleton().find_bone(bone_name)
break
parent = parent.get_parent()
func apply_transform():
bone.set_global_transform(global_transform)
foot_constraint.gd
This class extends BoneConstraint
and provides properties specific to a foot that may be grounded, similar to SkeletonUtilityGroundConstraint
in Unity. On _physics_process
it will perform a raycast to determine the ground position and angle for bone, and also supplies a method to set the position and rotation to the ground.
The raycast is performed separately in _physics_process
, as Godot documentation states that this is the only safe time to access PhysicsDirectSpaceState2D
.
class_name FootConstraint extends BoneConstraint
@export var ray_offset: Vector2
@export var ray_vector: Vector2
@export var position_offset: Vector2
@export var rotation_offset_degrees: float
var _ray_from: Vector2
var _ray_to: Vector2
var _is_colliding: bool
var _collision_position: Vector2
var _collision_normal: Vector2
func _physics_process(_delta: float):
_ray_from = global_position + ray_offset
_ray_to = _ray_from + ray_vector
var space_state: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state
var query: PhysicsRayQueryParameters2D = PhysicsRayQueryParameters2D.create(_ray_from, _ray_to)
var result: Dictionary = space_state.intersect_ray(query)
if result:
_is_colliding = true
_collision_position = result.position
_collision_normal = result.normal
else:
_is_colliding = false
func update_ground_transform():
if _is_colliding:
# global_position = _collision_position + position_offset # Causes feet to get stuck in position.
global_position = Vector2(global_position.x + position_offset.x, _collision_position.y + position_offset.y)
global_rotation = Vector2.UP.angle_to(_collision_normal) + deg_to_rad(rotation_offset_degrees)
ground_constraints.gd
This is the main class that drives the constraints. It's a separate Node that should be a child of the SpineSprite. It modifies the bone/foot constraints upon receiving the SpineSprite's world_transforms_changed
signal.
My example exposes a foot_constraints
array, that should be populated with the FootConstraints of the character, and an anchor_constraint
which represents the body bone that adjusts the character's hips/waist to be the appropriate height above the ground.
It's probably also worth noting, that this could easily be implemented inside a script that extends SpineSprite
. I've chosen to make it a separate Node, as in practice, many users will have a character controller script attached to their SpineSprite, and having a separate Node separates the functionality.
class_name GroundConstraints extends Node2D
@export var spine_sprite: SpineSprite
@export var anchor_constraint: BoneConstraint
@export var foot_constraints: Array[FootConstraint]
@export var anchor_offset: float
func _ready():
spine_sprite.connect("world_transforms_changed", _on_world_transforms_changed)
func _on_world_transforms_changed(_sprite: SpineSprite):
var ground_y: float = global_position.y
for foot_constraint in foot_constraints:
foot_constraint.update_ground_transform()
foot_constraint.apply_transform()
ground_y = max(foot_constraint.global_position.y, ground_y)
anchor_constraint.global_position.y = ground_y - anchor_offset
anchor_constraint.apply_transform()
Implementation
- Add a SpineSprite to a scene in Godot as is normal practice.
- Add 3 SpineBoneNodes as children of the SpineSprite: one for the Anchor bone, one for the Left Foot and one for the Right Foot.
- Attach a
bone_constraint
script to the Anchor SpineBoneNode.
- To each of the Foot nodes, attach a
foot_constraint
script.
- On each of the SpineBoneNodes under the SpineBoneNode properties, ensure Bone Mode is set to Follow and assign the appripriate Bone Name. By a fun quirk, this should also set the Bone Name of the BoneConstraint. If it doesn't, ensure the Bone Names match for each.
- Set the FootConstraint properties as is appropriate.
- Now add a new Node2D as a child of the SpineSprite.
- Attach a
ground_constraints
script to the new Node2D.
- Assign the GroundConstraints properties by drag and drop for the Node references, and set the Anchor Offset to an appropriate value.
After this, you should be able to run the scene and see the character's feet adjust to the slope of the terrain. Something that's missing from my solution that will be needed is a property for the ground mask to use with the ray casts.
API Improvements
The bone_constraint.gd
script wouldn't be need if SpineBoneNode
implemented its own version of the apply_transform
method. I'm not sure if it should be an exposed, parameterless overload of SpineBoneNode::update_transform
or if it should be called something like update_bone_transform
. Alternatively, SpineBoneNode could provide a bone
property, or expose the find_bone
method. I personally think both an apply/update transform method and a way to access the bone should be provided, as accessing the bone of a SpineBoneNode seems like something that would be useful.
The other note/question I have, is in foot_constraint.gd
I have a line commented out noting that setting global_position
causes the bone to get stuck in position (setting just y
gets around the problem). I was wondering why this is the case. I would expect the bone's position to be reflect the animation data on the next world_transforms_changed signal.
I hope others find this useful, and thanks for you continuous prompt responses!