Add animation completion callbacks (#119)
Implement callbacks that fire when animations complete, enabling direct causality between animation end and game state changes. This eliminates race conditions from parallel timer workarounds. - Add optional callback parameter to Animation constructor - Callbacks execute synchronously when animation completes - Proper Python reference counting with GIL safety - Callbacks receive (anim, target) parameters (currently None) - Exception handling prevents crashes from Python errors Example usage: ```python def on_complete(anim, target): player_moving = False anim = mcrfpy.Animation("x", 300.0, 1.0, "easeOut", callback=on_complete) anim.start(player) ``` 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9fb428dd01
commit
eb88c7b3aa
|
@ -14,13 +14,28 @@ Animation::Animation(const std::string& targetProperty,
|
|||
const AnimationValue& targetValue,
|
||||
float duration,
|
||||
EasingFunction easingFunc,
|
||||
bool delta)
|
||||
bool delta,
|
||||
PyObject* callback)
|
||||
: targetProperty(targetProperty)
|
||||
, targetValue(targetValue)
|
||||
, duration(duration)
|
||||
, easingFunc(easingFunc)
|
||||
, delta(delta)
|
||||
, pythonCallback(callback)
|
||||
{
|
||||
// Increase reference count for Python callback
|
||||
if (pythonCallback) {
|
||||
Py_INCREF(pythonCallback);
|
||||
}
|
||||
}
|
||||
|
||||
Animation::~Animation() {
|
||||
// Decrease reference count for Python callback
|
||||
if (pythonCallback) {
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
Py_DECREF(pythonCallback);
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
}
|
||||
|
||||
void Animation::start(std::shared_ptr<UIDrawable> target) {
|
||||
|
@ -149,6 +164,12 @@ bool Animation::update(float deltaTime) {
|
|||
applyValue(entity.get(), currentValue);
|
||||
}
|
||||
|
||||
// Trigger callback when animation completes
|
||||
if (isComplete() && !callbackTriggered && pythonCallback) {
|
||||
triggerCallback();
|
||||
callbackTriggered = true;
|
||||
}
|
||||
|
||||
return !isComplete();
|
||||
}
|
||||
|
||||
|
@ -295,6 +316,35 @@ void Animation::applyValue(UIEntity* entity, const AnimationValue& value) {
|
|||
}, value);
|
||||
}
|
||||
|
||||
void Animation::triggerCallback() {
|
||||
if (!pythonCallback) return;
|
||||
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
|
||||
// We need to create PyAnimation wrapper for this animation
|
||||
// and PyObject wrapper for the target
|
||||
// For now, we'll pass None for both as a simple implementation
|
||||
// TODO: In future, wrap the animation and target objects properly
|
||||
|
||||
PyObject* args = PyTuple_New(2);
|
||||
Py_INCREF(Py_None);
|
||||
Py_INCREF(Py_None);
|
||||
PyTuple_SetItem(args, 0, Py_None); // animation parameter
|
||||
PyTuple_SetItem(args, 1, Py_None); // target parameter
|
||||
|
||||
PyObject* result = PyObject_CallObject(pythonCallback, args);
|
||||
Py_DECREF(args);
|
||||
|
||||
if (!result) {
|
||||
// Print error but don't crash
|
||||
PyErr_Print();
|
||||
} else {
|
||||
Py_DECREF(result);
|
||||
}
|
||||
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
|
||||
// Easing functions implementation
|
||||
namespace EasingFunctions {
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <variant>
|
||||
#include <vector>
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include "Python.h"
|
||||
|
||||
// Forward declarations
|
||||
class UIDrawable;
|
||||
|
@ -36,7 +37,11 @@ public:
|
|||
const AnimationValue& targetValue,
|
||||
float duration,
|
||||
EasingFunction easingFunc = EasingFunctions::linear,
|
||||
bool delta = false);
|
||||
bool delta = false,
|
||||
PyObject* callback = nullptr);
|
||||
|
||||
// Destructor - cleanup Python callback reference
|
||||
~Animation();
|
||||
|
||||
// Apply this animation to a drawable
|
||||
void start(std::shared_ptr<UIDrawable> target);
|
||||
|
@ -77,12 +82,19 @@ private:
|
|||
std::weak_ptr<UIDrawable> targetWeak;
|
||||
std::weak_ptr<UIEntity> entityTargetWeak;
|
||||
|
||||
// Callback support
|
||||
PyObject* pythonCallback = nullptr; // Python callback function
|
||||
bool callbackTriggered = false; // Ensure callback only fires once
|
||||
|
||||
// Helper to interpolate between values
|
||||
AnimationValue interpolate(float t) const;
|
||||
|
||||
// Helper to apply value to target
|
||||
void applyValue(UIDrawable* target, const AnimationValue& value);
|
||||
void applyValue(UIEntity* entity, const AnimationValue& value);
|
||||
|
||||
// Trigger callback when animation completes
|
||||
void triggerCallback();
|
||||
};
|
||||
|
||||
// Easing functions library
|
||||
|
|
|
@ -18,19 +18,31 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds
|
|||
}
|
||||
|
||||
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr};
|
||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr};
|
||||
|
||||
const char* property_name;
|
||||
PyObject* target_value;
|
||||
float duration;
|
||||
const char* easing_name = "linear";
|
||||
int delta = 0;
|
||||
PyObject* callback = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast<char**>(keywords),
|
||||
&property_name, &target_value, &duration, &easing_name, &delta)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast<char**>(keywords),
|
||||
&property_name, &target_value, &duration, &easing_name, &delta, &callback)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate callback is callable if provided
|
||||
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
|
||||
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert None to nullptr for C++
|
||||
if (callback == Py_None) {
|
||||
callback = nullptr;
|
||||
}
|
||||
|
||||
// Convert Python target value to AnimationValue
|
||||
AnimationValue animValue;
|
||||
|
||||
|
@ -90,7 +102,7 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
|||
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
|
||||
|
||||
// Create the Animation
|
||||
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0);
|
||||
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demonstration of animation callbacks solving race conditions.
|
||||
Shows how callbacks enable direct causality for game state changes.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Game state
|
||||
player_moving = False
|
||||
move_queue = []
|
||||
|
||||
def movement_complete(anim, target):
|
||||
"""Called when player movement animation completes"""
|
||||
global player_moving, move_queue
|
||||
|
||||
print("Movement animation completed!")
|
||||
player_moving = False
|
||||
|
||||
# Process next move if queued
|
||||
if move_queue:
|
||||
next_pos = move_queue.pop(0)
|
||||
move_player_to(next_pos)
|
||||
else:
|
||||
print("Player is now idle and ready for input")
|
||||
|
||||
def move_player_to(new_pos):
|
||||
"""Move player with animation and proper state management"""
|
||||
global player_moving
|
||||
|
||||
if player_moving:
|
||||
print(f"Queueing move to {new_pos}")
|
||||
move_queue.append(new_pos)
|
||||
return
|
||||
|
||||
player_moving = True
|
||||
print(f"Moving player to {new_pos}")
|
||||
|
||||
# Get player entity (placeholder for demo)
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
player = ui[0] # Assume first element is player
|
||||
|
||||
# Animate movement with callback
|
||||
x, y = new_pos
|
||||
anim_x = mcrfpy.Animation("x", float(x), 0.5, "easeInOutQuad", callback=movement_complete)
|
||||
anim_y = mcrfpy.Animation("y", float(y), 0.5, "easeInOutQuad")
|
||||
|
||||
anim_x.start(player)
|
||||
anim_y.start(player)
|
||||
|
||||
def setup_demo():
|
||||
"""Set up the demo scene"""
|
||||
# Create scene
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
# Create player sprite
|
||||
player = mcrfpy.Frame((100, 100), (32, 32), fill_color=(0, 255, 0))
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
ui.append(player)
|
||||
|
||||
print("Demo: Animation callbacks for movement queue")
|
||||
print("=" * 40)
|
||||
|
||||
# Simulate rapid movement commands
|
||||
mcrfpy.setTimer("move1", lambda r: move_player_to((200, 100)), 100)
|
||||
mcrfpy.setTimer("move2", lambda r: move_player_to((200, 200)), 200) # Will be queued
|
||||
mcrfpy.setTimer("move3", lambda r: move_player_to((100, 200)), 300) # Will be queued
|
||||
|
||||
# Exit after demo
|
||||
mcrfpy.setTimer("exit", lambda r: exit_demo(), 3000)
|
||||
|
||||
def exit_demo():
|
||||
"""Exit the demo"""
|
||||
print("\nDemo completed successfully!")
|
||||
print("Callbacks ensure proper movement sequencing without race conditions")
|
||||
import sys
|
||||
sys.exit(0)
|
||||
|
||||
# Run the demo
|
||||
setup_demo()
|
Loading…
Reference in New Issue