Skip to content

Feat: Add skeleton animation plugin spine#233

Open
JiepengTan wants to merge 2 commits into
goplus:spx4.4.1from
JiepengTan:pr_support_spine
Open

Feat: Add skeleton animation plugin spine#233
JiepengTan wants to merge 2 commits into
goplus:spx4.4.1from
JiepengTan:pr_support_spine

Conversation

@JiepengTan

@JiepengTan JiepengTan commented Jan 4, 2026

Copy link
Copy Markdown

test demo: 09_Spine.zip

test script:

onStart => {
	wait 5
	scale := 0.1
	for {
		scale = 0.1
		for {
		    setSize scale
		    waitNextFrame
		    scale += 0.1 * deltaTime
		    if( scale > 0.6){
			    break;
		    }
		}
	}
}

onStart => {
	for {
		animateAndWait "hoverboard"
		animateAndWait "aim"
		animateAndWait "shoot"
		animateAndWait "portal"
		animateAndWait "walk"
		animateAndWait "run"
		animateAndWait "dying"
		wait 0.1
	}
}
SpineAnimation4.mp4

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @JiepengTan, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a comprehensive Spine animation plugin for the Godot engine. It integrates the Spine C++ runtime, provides Godot-specific wrappers for Spine's core components (skeletons, animations, skins, etc.), and extends the Godot editor with custom resource types and inspector functionalities. This allows Godot developers to import, manage, and display Spine-created 2D skeletal animations directly within their projects, offering robust control over animation playback, mixing, and interaction with other Godot nodes.

Highlights

  • Core Spine Runtime Integration: This pull request integrates the Spine C++ runtime into the Godot engine, providing the foundational logic for handling Spine animation data and rendering.
  • Godot-Specific Spine API Exposure: Numerous new classes (e.g., SpineAnimation, SpineSkeleton, SpineAnimationState) are introduced to wrap Spine's core functionalities, making them accessible and usable within Godot's scripting environment.
  • Custom Resource Management: New resource types like SpineAtlasResource and SpineSkeletonFileResource are added, along with custom loaders and savers, to seamlessly integrate Spine's proprietary file formats into Godot's asset pipeline.
  • Editor Integration and Workflow Enhancements: The module includes editor-specific components such as import plugins for Spine files and inspector plugins for Spine resources, aiming to improve the developer experience directly within the Godot editor.
  • Node-Based Interaction: Dedicated SpineBoneNode and SpineSlotNode classes allow for direct attachment and control of Godot nodes to specific bones or slots within a Spine skeleton, enabling complex character interactions and effects.
  • Comprehensive Debugging Tools: The SpineSprite node now includes extensive debug drawing options for various Spine elements like bones, regions, meshes, bounding boxes, paths, and clipping, aiding in visual debugging during development.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is a massive pull request that adds a full-featured Spine animation module. The overall architecture with custom resources, wrappers for the Spine API, and editor integration is well-designed. The code is generally clean and follows Godot's conventions.
I've found a few potential bugs, mostly related to inverted logic in getters and inconsistencies in handling Y-axis scaling. There are also some minor maintainability issues, like confusing variable names and non-English comments in a build script.
Given this is a work-in-progress, these issues are understandable. Great work so far on this complex integration!

float rotation = spine::MathUtil::Rad_Deg * transform.get_rotation();
Vector2 scale = transform.get_scale();
Vector2 local_position = position;
float local_rotation = bone->worldToLocalRotation(rotation) - 180;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The calculation for local_rotation subtracts 180 degrees after converting from world to local rotation. This is suspicious and might be a bug related to coordinate system conversions. worldToLocalRotation should ideally return the correct local rotation. This subtraction might be causing incorrect bone orientations when setting global transforms.

Ref<SpineAttachment> SpineSkin::get_attachment(int slot_index, const String &name) {
SPINE_CHECK(get_spine_object(), nullptr)
auto attachment = get_spine_object()->getAttachment(slot_index, SPINE_STRING(name));
if (attachment) return nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic here seems inverted. It returns nullptr if an attachment is found. It should return nullptr if the attachment is not found.

if (!attachment) return nullptr;

Comment on lines +395 to +403
float SpineSkeleton::get_scale_y() {
SPINE_CHECK(skeleton, 1)
return -skeleton->getScaleY();
}

void SpineSkeleton::set_scale_y(float v) {
SPINE_CHECK(skeleton, )
skeleton->setScaleY(v);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

get_scale_y() returns the negated scale, but set_scale_y() sets the value directly. This inconsistency can lead to bugs. For example, set_scale_y(get_scale_y()) would flip the skeleton's scale every time it's called.

To ensure consistency, the setter should also negate the value, like skeleton->setScaleY(-v);.

void SpineSkeleton::set_scale_y(float v) {
	SPINE_CHECK(skeleton, )
	skeleton->setScaleY(-v);
}

Comment on lines +705 to +706
if (skin)
return nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic here seems inverted. It returns nullptr if a default skin is found, and tries to create a wrapper for a null pointer if one is not found. It should be if (!skin) return nullptr;.

if (!skin)
		return nullptr;

Comment on lines +47 to +51
void *GodotSpineExtension::_calloc(size_t size, const char *file, int line) {
auto p = memalloc(size);
memset(p, 0, size);
return p;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _calloc function is implemented by calling memalloc and then memset. Godot provides a memcalloc function that does both, which would be more idiomatic and potentially more efficient.

void *GodotSpineExtension::_calloc(size_t size, const char *file, int line) {
	return memcalloc(size);
}

Comment thread modules/spine/SCsub Outdated
Comment on lines +9 to +22
# Spine C++ 运行时的头文件路径
env_spine_godot.Append(CPPPATH=["spine-cpp/include"])

# 如果是生成 VS 项目,也需要添加到主 env
if env["vsproj"]:
env.Append(CPPPATH=["#modules/spine/spine-cpp/include"])

# 编译 spine-cpp 源文件
env_spine_godot.add_source_files(env.modules_sources, "spine-cpp/src/spine/*.cpp")

# 编译 spine_godot 绑定文件
env_spine_godot.add_source_files(env.modules_sources, "*.cpp")

# Clang 下禁用 override 警告

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comments in this file are in Chinese, while the rest of the project's code and comments appear to be in English. For consistency and to make the code more accessible to a wider range of contributors, it would be best to translate these comments to English.

Comment on lines +214 to +215
Ref<Animation> animation_ref = create_animation(animation, false);
Ref<Animation> animation_looped_ref = create_animation(animation, true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The variable names animation_ref and animation_looped_ref are swapped with respect to the animations they hold. This is because the logic in create_animation is inverted: create_animation(..., false) creates a looping animation, and create_animation(..., true) creates a non-looping one, which is very confusing.

I suggest making the logic in create_animation more intuitive and renaming the variables at this call site to match what they hold.

For example, in create_animation (lines 264 and 268), you could change the logic to:

animation_ref->set_name(name + (loop ? "_looped" : ""));
#if VERSION_MAJOR > 3
    // animation_ref->set_loop_mode(loop ? Animation::LOOP_LINEAR : Animation::LOOP_NONE);
#else
    animation_ref->set_loop(loop);
#endif

With that change, the variable assignments here would be correct as they are.

Ref<Animation> animation_looped_ref = create_animation(animation, false);
		Ref<Animation> animation_ref = create_animation(animation, true);

this->textures = spineAtlas->textures;
this->normal_maps = spineAtlas->normal_maps;
this->specular_maps = spineAtlas->specular_maps;
emit_signal(SNAME("skeleton_file_changed"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The copy_from method in SpineAtlasResource emits the skeleton_file_changed signal. This seems incorrect as this resource handles atlas data, not skeleton data. It should probably be a signal related to the atlas, or no signal at all if not needed.

@JiepengTan JiepengTan force-pushed the pr_support_spine branch 3 times, most recently from 03868fd to 2b6fe25 Compare January 15, 2026 06:43
@JiepengTan JiepengTan marked this pull request as ready for review January 15, 2026 15:49
@JiepengTan JiepengTan changed the title 【WIP】Feat: Add skeleton animation plugin spine Feat: Add skeleton animation plugin spine Jan 15, 2026
SPINE_CHECK(skeleton_data, nullptr)
auto skin = skeleton_data->getDefaultSkin();
if (skin)
return nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Bug: Inverted Logic

The condition is inverted - this returns nullptr when a skin exists and attempts to wrap a null pointer when no skin exists. Should be:

Suggested change
return nullptr;
if (!skin)

Ref<SpineAttachment> SpineSkin::get_attachment(int slot_index, const String &name) {
SPINE_CHECK(get_spine_object(), nullptr)
auto attachment = get_spine_object()->getAttachment(slot_index, SPINE_STRING(name));
if (attachment) return nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Bug: Inverted Logic

Same issue - returns null when attachment exists, wraps null pointer when it doesn't. Should be:

Suggested change
if (attachment) return nullptr;
if (!attachment) return nullptr;


void SpineBone::set_global_transform(Transform2D transform) {
SPINE_CHECK(get_spine_object(), )
if (!get_spine_owner()) set_transform(transform);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Bug: Null Pointer Dereference

Missing return statement - if get_spine_owner() is null, line 477 will dereference it. Should be:

Suggested change
if (!get_spine_owner()) set_transform(transform);
if (!get_spine_owner()) {
set_transform(transform);
return;
}

}

auto indices_changed = false;
if (mesh_instance->indices.size() == indices->size()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance Issue: Inefficient Per-Frame Comparison

This linear O(n) comparison runs every frame for every mesh. Consider using memcmp for better performance:

indices_changed = (memcmp(old_indices, new_indices, indices->size() * sizeof(unsigned short)) != 0);

if (indices->size() > 0) {
mesh_instance->set_light_mask(get_light_mask());
size_t num_vertices = vertices->size() / 2;
mesh_instance->vertices.resize((int) num_vertices);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance Issue: Excessive Per-Frame Allocations

Every frame, for every slot, these arrays are resized causing memory allocations. Consider pre-allocating buffers to max expected size and tracking actual usage separately to avoid GC pressure in complex animations.

@fennoai

fennoai Bot commented Jan 15, 2026

Copy link
Copy Markdown

Great Spine integration implementation! The code is well-structured and comprehensive. However, found 3 critical bugs and several performance concerns:

Critical Bugs:

  • Inverted null checks in get_default_skin() and get_attachment() - will crash
  • Null pointer dereference in SpineBone::set_global_transform()

Security: Binary parser lacks bounds checking - validates file size before parsing to prevent crashes from malformed files

Performance: Per-frame allocations in update_meshes() may cause stuttering with complex characters - consider buffer pre-allocation

See inline comments for details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant