feat(viewport): complete viewport-based rendering system (#8)
Implements a comprehensive viewport system that allows fixed game resolution with flexible window scaling, addressing the primary wishes for issues #34, #49, and #8. Key Features: - Fixed game resolution independent of window size (window.game_resolution property) - Three scaling modes accessible via window.scaling_mode: - "center": 1:1 pixels, viewport centered in window - "stretch": viewport fills window, ignores aspect ratio - "fit": maintains aspect ratio with black bars - Automatic window-to-game coordinate transformation for mouse input - Full Python API integration with PyWindow properties Technical Implementation: - GameEngine::ViewportMode enum with Center, Stretch, Fit modes - SFML View system for efficient GPU-based viewport scaling - updateViewport() recalculates on window resize or mode change - windowToGameCoords() transforms mouse coordinates correctly - PyScene mouse input automatically uses transformed coordinates Tests: - test_viewport_simple.py: Basic API functionality - test_viewport_visual.py: Visual verification with screenshots - test_viewport_scaling.py: Interactive mode switching and resizing This completes the viewport-based rendering task and provides the foundation for resolution-independent game development as requested for Crypt of Sokoban. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
93256b96c6
commit
5a49cb7b6d
9 changed files with 626 additions and 12 deletions
|
|
@ -2,6 +2,7 @@
|
|||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <cstring>
|
||||
|
||||
// Singleton instance - static variable, not a class member
|
||||
static PyWindowObject* window_instance = nullptr;
|
||||
|
|
@ -404,6 +405,82 @@ PyObject* PyWindow::screenshot(PyWindowObject* self, PyObject* args, PyObject* k
|
|||
return PyBytes_FromStringAndSize((const char*)pixels, size.x * size.y * 4);
|
||||
}
|
||||
|
||||
PyObject* PyWindow::get_game_resolution(PyWindowObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto resolution = game->getGameResolution();
|
||||
return Py_BuildValue("(ii)", resolution.x, resolution.y);
|
||||
}
|
||||
|
||||
int PyWindow::set_game_resolution(PyWindowObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int width, height;
|
||||
if (!PyArg_ParseTuple(value, "ii", &width, &height)) {
|
||||
PyErr_SetString(PyExc_TypeError, "game_resolution must be a tuple of two integers (width, height)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "Game resolution dimensions must be positive");
|
||||
return -1;
|
||||
}
|
||||
|
||||
game->setGameResolution(width, height);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyWindow::get_scaling_mode(PyWindowObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyUnicode_FromString(game->getViewportModeString().c_str());
|
||||
}
|
||||
|
||||
int PyWindow::set_scaling_mode(PyWindowObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* mode_str = PyUnicode_AsUTF8(value);
|
||||
if (!mode_str) {
|
||||
PyErr_SetString(PyExc_TypeError, "scaling_mode must be a string");
|
||||
return -1;
|
||||
}
|
||||
|
||||
GameEngine::ViewportMode mode;
|
||||
if (strcmp(mode_str, "center") == 0) {
|
||||
mode = GameEngine::ViewportMode::Center;
|
||||
} else if (strcmp(mode_str, "stretch") == 0) {
|
||||
mode = GameEngine::ViewportMode::Stretch;
|
||||
} else if (strcmp(mode_str, "fit") == 0) {
|
||||
mode = GameEngine::ViewportMode::Fit;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_ValueError, "scaling_mode must be 'center', 'stretch', or 'fit'");
|
||||
return -1;
|
||||
}
|
||||
|
||||
game->setViewportMode(mode);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Property definitions
|
||||
PyGetSetDef PyWindow::getsetters[] = {
|
||||
{"resolution", (getter)get_resolution, (setter)set_resolution,
|
||||
|
|
@ -418,6 +495,10 @@ PyGetSetDef PyWindow::getsetters[] = {
|
|||
"Window visibility state", NULL},
|
||||
{"framerate_limit", (getter)get_framerate_limit, (setter)set_framerate_limit,
|
||||
"Frame rate limit (0 for unlimited)", NULL},
|
||||
{"game_resolution", (getter)get_game_resolution, (setter)set_game_resolution,
|
||||
"Fixed game resolution as (width, height) tuple", NULL},
|
||||
{"scaling_mode", (getter)get_scaling_mode, (setter)set_scaling_mode,
|
||||
"Viewport scaling mode: 'center', 'stretch', or 'fit'", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue