scene transitions via Scene object

This commit is contained in:
John McCardle 2026-01-03 13:53:18 -05:00
commit fc95fc2844
6 changed files with 322 additions and 35 deletions

View file

@ -9,6 +9,7 @@
#include "PyWindow.h" #include "PyWindow.h"
#include "PySceneObject.h" #include "PySceneObject.h"
#include "PyFOV.h" #include "PyFOV.h"
#include "PyTransition.h"
#include "PySound.h" #include "PySound.h"
#include "PyMusic.h" #include "PyMusic.h"
#include "PyKeyboard.h" #include "PyKeyboard.h"
@ -51,6 +52,14 @@ static PyObject* mcrfpy_module_getattr(PyObject* self, PyObject* args)
return McRFPy_API::api_get_scenes(); return McRFPy_API::api_get_scenes();
} }
if (strcmp(name, "default_transition") == 0) {
return PyTransition::to_python(PyTransition::default_transition);
}
if (strcmp(name, "default_transition_duration") == 0) {
return PyFloat_FromDouble(PyTransition::default_duration);
}
// Attribute not found - raise AttributeError // Attribute not found - raise AttributeError
PyErr_Format(PyExc_AttributeError, "module 'mcrfpy' has no attribute '%s'", name); PyErr_Format(PyExc_AttributeError, "module 'mcrfpy' has no attribute '%s'", name);
return NULL; return NULL;
@ -71,6 +80,33 @@ static int mcrfpy_module_setattro(PyObject* self, PyObject* name, PyObject* valu
return -1; return -1;
} }
if (strcmp(name_str, "default_transition") == 0) {
TransitionType trans;
if (!PyTransition::from_arg(value, &trans, nullptr)) {
return -1;
}
PyTransition::default_transition = trans;
return 0;
}
if (strcmp(name_str, "default_transition_duration") == 0) {
double duration;
if (PyFloat_Check(value)) {
duration = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
duration = PyLong_AsDouble(value);
} else {
PyErr_SetString(PyExc_TypeError, "default_transition_duration must be a number");
return -1;
}
if (duration < 0.0) {
PyErr_SetString(PyExc_ValueError, "default_transition_duration must be non-negative");
return -1;
}
PyTransition::default_duration = static_cast<float>(duration);
return 0;
}
// For other attributes, use default module setattr // For other attributes, use default module setattr
return PyObject_GenericSetAttr(self, name, value); return PyObject_GenericSetAttr(self, name, value);
} }
@ -392,7 +428,16 @@ PyObject* PyInit_mcrfpy()
// Fallback to integer if enum failed // Fallback to integer if enum failed
PyModule_AddIntConstant(m, "default_fov", FOV_BASIC); PyModule_AddIntConstant(m, "default_fov", FOV_BASIC);
} }
// Add Transition enum class (uses Python's IntEnum)
PyObject* transition_class = PyTransition::create_enum_class(m);
if (!transition_class) {
// If enum creation fails, continue without it (non-fatal)
PyErr_Clear();
}
// Note: default_transition and default_transition_duration are handled via
// mcrfpy_module_getattr/setattro using PyTransition::default_transition/default_duration
// Add automation submodule // Add automation submodule
PyObject* automation_module = McRFPy_Automation::init_automation_module(); PyObject* automation_module = McRFPy_Automation::init_automation_module();
if (automation_module != NULL) { if (automation_module != NULL) {

View file

@ -3,6 +3,7 @@
#include "GameEngine.h" #include "GameEngine.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "McRFPy_Doc.h" #include "McRFPy_Doc.h"
#include "PyTransition.h"
#include <iostream> #include <iostream>
// Static map to store Python scene objects by name // Static map to store Python scene objects by name
@ -75,13 +76,54 @@ PyObject* PySceneClass::__repr__(PySceneObject* self)
return PyUnicode_FromFormat("<Scene '%s'>", self->name.c_str()); return PyUnicode_FromFormat("<Scene '%s'>", self->name.c_str());
} }
PyObject* PySceneClass::activate(PySceneObject* self, PyObject* args) PyObject* PySceneClass::activate(PySceneObject* self, PyObject* args, PyObject* kwds)
{ {
// Call the static method from McRFPy_API static const char* keywords[] = {"transition", "duration", nullptr};
PyObject* py_args = Py_BuildValue("(s)", self->name.c_str()); PyObject* transition_arg = nullptr;
PyObject* result = McRFPy_API::_setScene(NULL, py_args); PyObject* duration_arg = nullptr;
Py_DECREF(py_args);
return result; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", const_cast<char**>(keywords),
&transition_arg, &duration_arg)) {
return NULL;
}
// Get transition type (use default if not provided)
TransitionType transition_type;
bool trans_was_none = false;
if (transition_arg) {
if (!PyTransition::from_arg(transition_arg, &transition_type, &trans_was_none)) {
return NULL;
}
} else {
transition_type = PyTransition::default_transition;
}
// Get duration (use default if not provided)
float duration;
if (duration_arg && duration_arg != Py_None) {
if (PyFloat_Check(duration_arg)) {
duration = static_cast<float>(PyFloat_AsDouble(duration_arg));
} else if (PyLong_Check(duration_arg)) {
duration = static_cast<float>(PyLong_AsLong(duration_arg));
} else {
PyErr_SetString(PyExc_TypeError, "duration must be a number");
return NULL;
}
} else {
duration = PyTransition::default_duration;
}
// Build transition string for _setScene (or call game->changeScene directly)
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine");
return NULL;
}
// Call game->changeScene directly with proper transition
game->changeScene(self->name, transition_type, duration);
Py_RETURN_NONE;
} }
// children property getter (replaces get_ui method) // children property getter (replaces get_ui method)
@ -455,12 +497,15 @@ PyGetSetDef PySceneClass::getsetters[] = {
// Methods // Methods
PyMethodDef PySceneClass::methods[] = { PyMethodDef PySceneClass::methods[] = {
{"activate", (PyCFunction)activate, METH_NOARGS, {"activate", (PyCFunction)activate, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(SceneClass, activate, MCRF_METHOD(SceneClass, activate,
MCRF_SIG("()", "None"), MCRF_SIG("(transition: Transition = None, duration: float = None)", "None"),
MCRF_DESC("Make this the active scene."), MCRF_DESC("Make this the active scene with optional transition effect."),
MCRF_ARGS_START
MCRF_ARG("transition", "Transition type (mcrfpy.Transition enum). Defaults to mcrfpy.default_transition")
MCRF_ARG("duration", "Transition duration in seconds. Defaults to mcrfpy.default_transition_duration")
MCRF_RETURNS("None") MCRF_RETURNS("None")
MCRF_NOTE("Deactivates the current scene and activates this one. Scene transitions and lifecycle callbacks are triggered.") MCRF_NOTE("Deactivates the current scene and activates this one. Lifecycle callbacks (on_exit, on_enter) are triggered.")
)}, )},
{"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS, {"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS,
MCRF_METHOD(SceneClass, register_keyboard, MCRF_METHOD(SceneClass, register_keyboard,
@ -575,9 +620,8 @@ int McRFPy_API::api_set_current_scene(PyObject* value)
return -1; return -1;
} }
std::string old_scene = game->scene; // Use changeScene with default transition settings
game->scene = scene_name; game->changeScene(scene_name, PyTransition::default_transition, PyTransition::default_duration);
McRFPy_API::triggerSceneChange(old_scene, scene_name);
return 0; return 0;
} }

View file

@ -26,7 +26,7 @@ public:
static PyObject* __repr__(PySceneObject* self); static PyObject* __repr__(PySceneObject* self);
// Scene methods // Scene methods
static PyObject* activate(PySceneObject* self, PyObject* args); static PyObject* activate(PySceneObject* self, PyObject* args, PyObject* kwds);
static PyObject* register_keyboard(PySceneObject* self, PyObject* args); static PyObject* register_keyboard(PySceneObject* self, PyObject* args);
// Properties // Properties

158
src/PyTransition.cpp Normal file
View file

@ -0,0 +1,158 @@
#include "PyTransition.h"
#include "McRFPy_API.h"
// Static storage
PyObject* PyTransition::transition_enum_class = nullptr;
TransitionType PyTransition::default_transition = TransitionType::None;
float PyTransition::default_duration = 1.0f;
PyObject* PyTransition::create_enum_class(PyObject* module) {
// Import IntEnum from enum module
PyObject* enum_module = PyImport_ImportModule("enum");
if (!enum_module) {
return NULL;
}
PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum");
Py_DECREF(enum_module);
if (!int_enum) {
return NULL;
}
// Create dict of enum members
PyObject* members = PyDict_New();
if (!members) {
Py_DECREF(int_enum);
return NULL;
}
// Add all Transition type members
// Values match the C++ TransitionType enum
struct {
const char* name;
int value;
} transition_members[] = {
{"NONE", static_cast<int>(TransitionType::None)},
{"FADE", static_cast<int>(TransitionType::Fade)},
{"SLIDE_LEFT", static_cast<int>(TransitionType::SlideLeft)},
{"SLIDE_RIGHT", static_cast<int>(TransitionType::SlideRight)},
{"SLIDE_UP", static_cast<int>(TransitionType::SlideUp)},
{"SLIDE_DOWN", static_cast<int>(TransitionType::SlideDown)},
};
for (const auto& m : transition_members) {
PyObject* value = PyLong_FromLong(m.value);
if (!value) {
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
if (PyDict_SetItemString(members, m.name, value) < 0) {
Py_DECREF(value);
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
Py_DECREF(value);
}
// Call IntEnum("Transition", members) to create the enum class
PyObject* name = PyUnicode_FromString("Transition");
if (!name) {
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
// IntEnum(name, members) using functional API
PyObject* args = PyTuple_Pack(2, name, members);
Py_DECREF(name);
Py_DECREF(members);
if (!args) {
Py_DECREF(int_enum);
return NULL;
}
PyObject* transition_class = PyObject_Call(int_enum, args, NULL);
Py_DECREF(args);
Py_DECREF(int_enum);
if (!transition_class) {
return NULL;
}
// Cache the reference for fast type checking
transition_enum_class = transition_class;
Py_INCREF(transition_enum_class);
// Add to module
if (PyModule_AddObject(module, "Transition", transition_class) < 0) {
Py_DECREF(transition_class);
transition_enum_class = nullptr;
return NULL;
}
return transition_class;
}
int PyTransition::from_arg(PyObject* arg, TransitionType* out_type, bool* was_none) {
if (was_none) *was_none = false;
// Accept None -> caller should use default
if (arg == Py_None || arg == NULL) {
if (was_none) *was_none = true;
*out_type = default_transition;
return 1;
}
// Accept Transition enum member (check if it's an instance of our enum)
if (transition_enum_class && PyObject_IsInstance(arg, transition_enum_class)) {
// IntEnum members have a 'value' attribute
PyObject* value = PyObject_GetAttrString(arg, "value");
if (!value) {
return 0;
}
long val = PyLong_AsLong(value);
Py_DECREF(value);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
*out_type = static_cast<TransitionType>(val);
return 1;
}
// Accept int (for flexibility)
if (PyLong_Check(arg)) {
long val = PyLong_AsLong(arg);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
if (val < 0 || val > static_cast<int>(TransitionType::SlideDown)) {
PyErr_Format(PyExc_ValueError,
"Invalid Transition value: %ld. Must be 0-5 or use mcrfpy.Transition enum.",
val);
return 0;
}
*out_type = static_cast<TransitionType>(val);
return 1;
}
PyErr_SetString(PyExc_TypeError,
"transition must be mcrfpy.Transition enum member, int, or None");
return 0;
}
PyObject* PyTransition::to_python(TransitionType type) {
if (!transition_enum_class) {
PyErr_SetString(PyExc_RuntimeError, "Transition enum not initialized");
return NULL;
}
// Get the enum member by value
PyObject* value = PyLong_FromLong(static_cast<int>(type));
if (!value) return NULL;
PyObject* result = PyObject_CallFunctionObjArgs(transition_enum_class, value, NULL);
Py_DECREF(value);
return result;
}

29
src/PyTransition.h Normal file
View file

@ -0,0 +1,29 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "SceneTransition.h"
// Module-level Transition enum class (created at runtime using Python's IntEnum)
// Stored as a module attribute: mcrfpy.Transition
class PyTransition {
public:
// Create the Transition enum class and add to module
// Returns the enum class (new reference), or NULL on error
static PyObject* create_enum_class(PyObject* module);
// Helper to extract transition type from Python arg (accepts Transition enum, int, or None)
// Returns 1 on success, 0 on error (with exception set)
// If arg is None, sets *out_type to the default and sets *was_none to true
static int from_arg(PyObject* arg, TransitionType* out_type, bool* was_none = nullptr);
// Convert TransitionType to Python enum member
static PyObject* to_python(TransitionType type);
// Cached reference to the Transition enum class for fast type checking
static PyObject* transition_enum_class;
// Module-level defaults
static TransitionType default_transition;
static float default_duration;
};

View file

@ -63,7 +63,7 @@ def create_test_scenes():
print("Created test scenes: red_scene, blue_scene, green_scene, menu_scene") print("Created test scenes: red_scene, blue_scene, green_scene, menu_scene")
# Track current transition type # Track current transition type
current_transition = "fade" current_transition = mcrfpy.Transition.FADE
transition_duration = 1.0 transition_duration = 1.0
def handle_key(key, action): def handle_key(key, action):
@ -76,24 +76,35 @@ def handle_key(key, action):
current_scene = (mcrfpy.current_scene.name if mcrfpy.current_scene else None) current_scene = (mcrfpy.current_scene.name if mcrfpy.current_scene else None)
# Number keys set transition type # Number keys set transition type
if key == "Num1": keyselections = {
current_transition = "fade" "Num1": mcrfpy.Transition.FADE,
print("Transition set to: fade") "Num2": mcrfpy.Transition.SLIDE_LEFT,
elif key == "Num2": "Num3": mcrfpy.Transition.SLIDE_RIGHT,
current_transition = "slide_left" "Num4": mcrfpy.Transition.SLIDE_UP,
print("Transition set to: slide_left") "Num5": mcrfpy.Transition.SLIDE_DOWN,
elif key == "Num3": "Num6": mcrfpy.Transition.NONE
current_transition = "slide_right" }
print("Transition set to: slide_right") if key in keyselections:
elif key == "Num4": current_transition = keyselections[key]
current_transition = "slide_up" print(f"Transition set to: {current_transition}")
print("Transition set to: slide_up") #if key == "Num1":
elif key == "Num5": # current_transition = "fade"
current_transition = "slide_down" # print("Transition set to: fade")
print("Transition set to: slide_down") #elif key == "Num2":
elif key == "Num6": # current_transition = "slide_left"
current_transition = None # Instant # print("Transition set to: slide_left")
print("Transition set to: instant") #elif key == "Num3":
# current_transition = "slide_right"
# print("Transition set to: slide_right")
#elif key == "Num4":
# current_transition = "slide_up"
# print("Transition set to: slide_up")
#elif key == "Num5":
# current_transition = "slide_down"
# print("Transition set to: slide_down")
#elif key == "Num6":
# current_transition = None # Instant
# print("Transition set to: instant")
# Letter keys change scene # Letter keys change scene
keytransitions = { keytransitions = {
@ -104,7 +115,7 @@ def handle_key(key, action):
} }
if key in keytransitions: if key in keytransitions:
if mcrfpy.current_scene != keytransitions[key]: if mcrfpy.current_scene != keytransitions[key]:
keytransitions[key].activate() keytransitions[key].activate(current_transition, transition_duration)
#elif key == "R": #elif key == "R":
# if current_scene != "red_scene": # if current_scene != "red_scene":
# print(f"Transitioning to red_scene with {current_transition}") # print(f"Transitioning to red_scene with {current_transition}")