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>
89 lines
2.8 KiB
C++
89 lines
2.8 KiB
C++
#include "PyScene.h"
|
|
#include "ActionCode.h"
|
|
#include "Resources.h"
|
|
#include "PyCallable.h"
|
|
#include <algorithm>
|
|
|
|
PyScene::PyScene(GameEngine* g) : Scene(g)
|
|
{
|
|
// mouse events
|
|
registerAction(ActionCode::MOUSEBUTTON + sf::Mouse::Left, "left");
|
|
registerAction(ActionCode::MOUSEBUTTON + sf::Mouse::Right, "right");
|
|
registerAction(ActionCode::MOUSEWHEEL + ActionCode::WHEEL_DEL, "wheel_up");
|
|
registerAction(ActionCode::MOUSEWHEEL + ActionCode::WHEEL_NEG + ActionCode::WHEEL_DEL, "wheel_down");
|
|
|
|
// console (` / ~ key) - don't hard code.
|
|
//registerAction(ActionCode::KEY + sf::Keyboard::Grave, "debug_menu");
|
|
}
|
|
|
|
void PyScene::update()
|
|
{
|
|
}
|
|
|
|
void PyScene::do_mouse_input(std::string button, std::string type)
|
|
{
|
|
// In headless mode, mouse input is not available
|
|
if (game->isHeadless()) {
|
|
return;
|
|
}
|
|
|
|
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
|
|
// Convert window coordinates to game coordinates using the viewport
|
|
auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos));
|
|
|
|
// Create a sorted copy by z-index (highest first)
|
|
std::vector<std::shared_ptr<UIDrawable>> sorted_elements(*ui_elements);
|
|
std::sort(sorted_elements.begin(), sorted_elements.end(),
|
|
[](const auto& a, const auto& b) { return a->z_index > b->z_index; });
|
|
|
|
// Check elements in z-order (top to bottom)
|
|
for (const auto& element : sorted_elements) {
|
|
if (!element->visible) continue;
|
|
|
|
if (auto target = element->click_at(sf::Vector2f(mousepos))) {
|
|
target->click_callable->call(mousepos, button, type);
|
|
return; // Stop after first handler
|
|
}
|
|
}
|
|
}
|
|
|
|
void PyScene::doAction(std::string name, std::string type)
|
|
{
|
|
if (name.compare("left") == 0 || name.compare("rclick") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) {
|
|
do_mouse_input(name, type);
|
|
}
|
|
else if ACTIONONCE("debug_menu") {
|
|
McRFPy_API::REPL();
|
|
}
|
|
}
|
|
|
|
void PyScene::render()
|
|
{
|
|
game->getRenderTarget().clear();
|
|
|
|
// Only sort if z_index values have changed
|
|
if (ui_elements_need_sort) {
|
|
std::sort(ui_elements->begin(), ui_elements->end(),
|
|
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
|
|
return a->z_index < b->z_index;
|
|
});
|
|
ui_elements_need_sort = false;
|
|
}
|
|
|
|
// Render in sorted order (no need to copy anymore)
|
|
for (auto e: *ui_elements)
|
|
{
|
|
if (e) {
|
|
// Track metrics
|
|
game->metrics.uiElements++;
|
|
if (e->visible) {
|
|
game->metrics.visibleElements++;
|
|
// Count this as a draw call (each visible element = 1+ draw calls)
|
|
game->metrics.drawCalls++;
|
|
}
|
|
e->render();
|
|
}
|
|
}
|
|
|
|
// Display is handled by GameEngine
|
|
}
|