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
4 changed files with 161 additions and 6 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue