Compare commits
No commits in common. "e41f83a5b3bd747f375c8586d36763677fbba1d1" and "8153fd250356bc86d674b6dcbc5ebc8bb8f15566" have entirely different histories.
e41f83a5b3
...
8153fd2503
458
CLAUDE.md
458
CLAUDE.md
|
|
@ -1,458 +0,0 @@
|
||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Gitea-First Workflow
|
|
||||||
|
|
||||||
**IMPORTANT**: This project uses Gitea for issue tracking, documentation, and project management. Always consult and update Gitea resources before and during development work.
|
|
||||||
|
|
||||||
**Gitea Instance**: https://gamedev.ffwf.net/gitea/john/McRogueFace
|
|
||||||
|
|
||||||
### Core Principles
|
|
||||||
|
|
||||||
1. **Gitea is the Single Source of Truth**
|
|
||||||
- Issue tracker contains current tasks, bugs, and feature requests
|
|
||||||
- Wiki contains living documentation and architecture decisions
|
|
||||||
- Use Gitea MCP tools to query and update issues programmatically
|
|
||||||
|
|
||||||
2. **Always Check Gitea First**
|
|
||||||
- Before starting work: Check open issues for related tasks or blockers
|
|
||||||
- When using `/roadmap` command: Query Gitea for up-to-date issue status
|
|
||||||
- When researching a feature: Search Gitea wiki and issues before grepping codebase
|
|
||||||
- When encountering a bug: Check if an issue already exists
|
|
||||||
|
|
||||||
3. **Create Granular Issues**
|
|
||||||
- Break large features into separate, focused issues
|
|
||||||
- Each issue should address one specific problem or enhancement
|
|
||||||
- Tag issues appropriately: `[Bugfix]`, `[Major Feature]`, `[Minor Feature]`, etc.
|
|
||||||
- Link related issues using dependencies or blocking relationships
|
|
||||||
|
|
||||||
4. **Document as You Go**
|
|
||||||
- When work on one issue interacts with another system: Add notes to related issues
|
|
||||||
- When discovering undocumented behavior: Create task to document it
|
|
||||||
- When documentation misleads you: Create task to correct or expand it
|
|
||||||
- When implementing a feature: Update the Gitea wiki if appropriate
|
|
||||||
|
|
||||||
5. **Cross-Reference Everything**
|
|
||||||
- Commit messages should reference issue numbers (e.g., "Fixes #104", "Addresses #125")
|
|
||||||
- Issue comments should link to commits when work is done
|
|
||||||
- Wiki pages should reference relevant issues for implementation details
|
|
||||||
- Issues should link to each other when dependencies exist
|
|
||||||
|
|
||||||
### Workflow Pattern
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 1. Check Gitea Issues & Wiki │
|
|
||||||
│ - Is there an existing issue for this? │
|
|
||||||
│ - What's the current status? │
|
|
||||||
│ - Are there related issues or blockers? │
|
|
||||||
└─────────────────┬───────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 2. Create Issues (if needed) │
|
|
||||||
│ - Break work into granular tasks │
|
|
||||||
│ - Tag appropriately │
|
|
||||||
│ - Link dependencies │
|
|
||||||
└─────────────────┬───────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 3. Do the Work │
|
|
||||||
│ - Implement/fix/document │
|
|
||||||
│ - Write tests first (TDD) │
|
|
||||||
│ - Add inline documentation │
|
|
||||||
└─────────────────┬───────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 4. Update Gitea │
|
|
||||||
│ - Add notes to affected issues │
|
|
||||||
│ - Create follow-up issues for discovered work │
|
|
||||||
│ - Update wiki if architecture/APIs changed │
|
|
||||||
│ - Add documentation correction tasks │
|
|
||||||
└─────────────────┬───────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 5. Commit & Reference │
|
|
||||||
│ - Commit messages reference issue numbers │
|
|
||||||
│ - Close issues or update status │
|
|
||||||
│ - Add commit links to issue comments │
|
|
||||||
└─────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefits of Gitea-First Approach
|
|
||||||
|
|
||||||
- **Reduced Context Switching**: Check brief issue descriptions instead of re-reading entire codebase
|
|
||||||
- **Better Planning**: Issues provide roadmap; avoid duplicate or contradictory work
|
|
||||||
- **Living Documentation**: Wiki and issues stay current as work progresses
|
|
||||||
- **Historical Context**: Issue comments capture why decisions were made
|
|
||||||
- **Efficiency**: MCP tools allow programmatic access to project state
|
|
||||||
|
|
||||||
### MCP Tools Available
|
|
||||||
|
|
||||||
Claude Code has access to Gitea MCP tools for:
|
|
||||||
- `list_repo_issues` - Query current issues with filtering
|
|
||||||
- `get_issue` - Get detailed issue information
|
|
||||||
- `create_issue` - Create new issues programmatically
|
|
||||||
- `create_issue_comment` - Add comments to issues
|
|
||||||
- `edit_issue` - Update issue status, title, body
|
|
||||||
- `add_issue_labels` - Tag issues appropriately
|
|
||||||
- `add_issue_dependency` / `add_issue_blocking` - Link related issues
|
|
||||||
- Plus wiki, milestone, and label management tools
|
|
||||||
|
|
||||||
Use these tools liberally to keep the project organized!
|
|
||||||
|
|
||||||
## Build Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the project (compiles to ./build directory)
|
|
||||||
make
|
|
||||||
|
|
||||||
# Or use the build script directly
|
|
||||||
./build.sh
|
|
||||||
|
|
||||||
# Run the game
|
|
||||||
make run
|
|
||||||
|
|
||||||
# Clean build artifacts
|
|
||||||
make clean
|
|
||||||
|
|
||||||
# The executable and all assets are in ./build/
|
|
||||||
cd build
|
|
||||||
./mcrogueface
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Architecture
|
|
||||||
|
|
||||||
McRogueFace is a C++ game engine with Python scripting support, designed for creating roguelike games. The architecture consists of:
|
|
||||||
|
|
||||||
### Core Engine (C++)
|
|
||||||
- **Entry Point**: `src/main.cpp` initializes the game engine
|
|
||||||
- **Scene System**: `Scene.h/cpp` manages game states
|
|
||||||
- **Entity System**: `UIEntity.h/cpp` provides game objects
|
|
||||||
- **Python Integration**: `McRFPy_API.h/cpp` exposes engine functionality to Python
|
|
||||||
- **UI Components**: `UIFrame`, `UICaption`, `UISprite`, `UIGrid` for rendering
|
|
||||||
|
|
||||||
### Game Logic (Python)
|
|
||||||
- **Main Script**: `src/scripts/game.py` contains game initialization and scene setup
|
|
||||||
- **Entity System**: `src/scripts/cos_entities.py` implements game entities (Player, Enemy, Boulder, etc.)
|
|
||||||
- **Level Generation**: `src/scripts/cos_level.py` uses BSP for procedural dungeon generation
|
|
||||||
- **Tile System**: `src/scripts/cos_tiles.py` implements Wave Function Collapse for tile placement
|
|
||||||
|
|
||||||
### Key Python API (`mcrfpy` module)
|
|
||||||
The C++ engine exposes these primary functions to Python:
|
|
||||||
- Scene Management: `createScene()`, `setScene()`, `sceneUI()`
|
|
||||||
- Entity Creation: `Entity()` with position and sprite properties
|
|
||||||
- Grid Management: `Grid()` for tilemap rendering
|
|
||||||
- Input Handling: `keypressScene()` for keyboard events
|
|
||||||
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
|
|
||||||
- Timers: `setTimer()`, `delTimer()` for event scheduling
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
### Running the Game
|
|
||||||
After building, the executable expects:
|
|
||||||
- `assets/` directory with sprites, fonts, and audio
|
|
||||||
- `scripts/` directory with Python game files
|
|
||||||
- Python 3.12 shared libraries in `./lib/`
|
|
||||||
|
|
||||||
### Modifying Game Logic
|
|
||||||
- Game scripts are in `src/scripts/`
|
|
||||||
- Main game entry is `game.py`
|
|
||||||
- Entity behavior in `cos_entities.py`
|
|
||||||
- Level generation in `cos_level.py`
|
|
||||||
|
|
||||||
### Adding New Features
|
|
||||||
1. C++ API additions go in `src/McRFPy_API.cpp`
|
|
||||||
2. Expose to Python using the existing binding pattern
|
|
||||||
3. Update Python scripts to use new functionality
|
|
||||||
|
|
||||||
## Testing Game Changes
|
|
||||||
|
|
||||||
Currently no automated test suite. Manual testing workflow:
|
|
||||||
1. Build with `make`
|
|
||||||
2. Run `make run` or `cd build && ./mcrogueface`
|
|
||||||
3. Test specific features through gameplay
|
|
||||||
4. Check console output for Python errors
|
|
||||||
|
|
||||||
### Quick Testing Commands
|
|
||||||
```bash
|
|
||||||
# Test basic functionality
|
|
||||||
make test
|
|
||||||
|
|
||||||
# Run in Python interactive mode
|
|
||||||
make python
|
|
||||||
|
|
||||||
# Test headless mode
|
|
||||||
cd build
|
|
||||||
./mcrogueface --headless -c "import mcrfpy; print('Headless test')"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Development Tasks
|
|
||||||
|
|
||||||
### Compiling McRogueFace
|
|
||||||
```bash
|
|
||||||
# Standard build (to ./build directory)
|
|
||||||
make
|
|
||||||
|
|
||||||
# Full rebuild
|
|
||||||
make clean && make
|
|
||||||
|
|
||||||
# Manual CMake build
|
|
||||||
mkdir build && cd build
|
|
||||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
|
||||||
make -j$(nproc)
|
|
||||||
|
|
||||||
# The library path issue: if linking fails, check that libraries are in __lib/
|
|
||||||
# CMakeLists.txt expects: link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running and Capturing Output
|
|
||||||
```bash
|
|
||||||
# Run with timeout and capture output
|
|
||||||
cd build
|
|
||||||
timeout 5 ./mcrogueface 2>&1 | tee output.log
|
|
||||||
|
|
||||||
# Run in background and kill after delay
|
|
||||||
./mcrogueface > output.txt 2>&1 & PID=$!; sleep 3; kill $PID 2>/dev/null
|
|
||||||
|
|
||||||
# Just capture first N lines (useful for crashes)
|
|
||||||
./mcrogueface 2>&1 | head -50
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debugging with GDB
|
|
||||||
```bash
|
|
||||||
# Interactive debugging
|
|
||||||
gdb ./mcrogueface
|
|
||||||
(gdb) run
|
|
||||||
(gdb) bt # backtrace after crash
|
|
||||||
|
|
||||||
# Batch mode debugging (non-interactive)
|
|
||||||
gdb -batch -ex run -ex where -ex quit ./mcrogueface 2>&1
|
|
||||||
|
|
||||||
# Get just the backtrace after a crash
|
|
||||||
gdb -batch -ex "run" -ex "bt" ./mcrogueface 2>&1 | head -50
|
|
||||||
|
|
||||||
# Debug with specific commands
|
|
||||||
echo -e "run\nbt 5\nquit\ny" | gdb ./mcrogueface 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing Different Python Scripts
|
|
||||||
```bash
|
|
||||||
# The game automatically runs build/scripts/game.py on startup
|
|
||||||
# To test different behavior:
|
|
||||||
|
|
||||||
# Option 1: Replace game.py temporarily
|
|
||||||
cd build
|
|
||||||
cp scripts/my_test_script.py scripts/game.py
|
|
||||||
./mcrogueface
|
|
||||||
|
|
||||||
# Option 2: Backup original and test
|
|
||||||
mv scripts/game.py scripts/game.py.bak
|
|
||||||
cp my_test.py scripts/game.py
|
|
||||||
./mcrogueface
|
|
||||||
mv scripts/game.py.bak scripts/game.py
|
|
||||||
|
|
||||||
# Option 3: For quick tests, create minimal game.py
|
|
||||||
echo 'import mcrfpy; print("Test"); mcrfpy.createScene("test")' > scripts/game.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Understanding Key Macros and Patterns
|
|
||||||
|
|
||||||
#### RET_PY_INSTANCE Macro (UIDrawable.h)
|
|
||||||
This macro handles converting C++ UI objects to their Python equivalents:
|
|
||||||
```cpp
|
|
||||||
RET_PY_INSTANCE(target);
|
|
||||||
// Expands to a switch on target->derived_type() that:
|
|
||||||
// 1. Allocates the correct Python object type (Frame, Caption, Sprite, Grid)
|
|
||||||
// 2. Sets the shared_ptr data member
|
|
||||||
// 3. Returns the PyObject*
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Collection Patterns
|
|
||||||
- `UICollection` wraps `std::vector<std::shared_ptr<UIDrawable>>`
|
|
||||||
- `UIEntityCollection` wraps `std::list<std::shared_ptr<UIEntity>>`
|
|
||||||
- Different containers require different iteration code (vector vs list)
|
|
||||||
|
|
||||||
#### Python Object Creation Patterns
|
|
||||||
```cpp
|
|
||||||
// Pattern 1: Using tp_alloc (most common)
|
|
||||||
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
|
|
||||||
o->data = std::make_shared<UIFrame>();
|
|
||||||
|
|
||||||
// Pattern 2: Getting type from module
|
|
||||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
||||||
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
|
||||||
|
|
||||||
// Pattern 3: Direct shared_ptr assignment
|
|
||||||
iterObj->data = self->data; // Shares the C++ object
|
|
||||||
```
|
|
||||||
|
|
||||||
### Working Directory Structure
|
|
||||||
```
|
|
||||||
build/
|
|
||||||
├── mcrogueface # The executable
|
|
||||||
├── scripts/
|
|
||||||
│ └── game.py # Auto-loaded Python script
|
|
||||||
├── assets/ # Copied from source during build
|
|
||||||
└── lib/ # Python libraries (copied from __lib/)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quick Iteration Tips
|
|
||||||
- Keep a test script ready for quick experiments
|
|
||||||
- Use `timeout` to auto-kill hanging processes
|
|
||||||
- The game expects a window manager; use Xvfb for headless testing
|
|
||||||
- Python errors go to stderr, game output to stdout
|
|
||||||
- Segfaults usually mean Python type initialization issues
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- The project uses SFML for graphics/audio and libtcod for roguelike utilities
|
|
||||||
- Python scripts are loaded at runtime from the `scripts/` directory
|
|
||||||
- Asset loading expects specific paths relative to the executable
|
|
||||||
- The game was created for 7DRL 2025 as "Crypt of Sokoban"
|
|
||||||
- Iterator implementations require careful handling of C++/Python boundaries
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
|
|
||||||
### Test-Driven Development
|
|
||||||
- **Always write tests first**: Create automation tests in `./tests/` for all bugs and new features
|
|
||||||
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix is applied
|
|
||||||
- **Close the loop**: Reproduce issue → change code → recompile → verify behavior change
|
|
||||||
|
|
||||||
### Two Types of Tests
|
|
||||||
|
|
||||||
#### 1. Direct Execution Tests (No Game Loop)
|
|
||||||
For tests that only need class initialization or direct code execution:
|
|
||||||
```python
|
|
||||||
# These tests can treat McRogueFace like a Python interpreter
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Test code here
|
|
||||||
result = mcrfpy.some_function()
|
|
||||||
assert result == expected_value
|
|
||||||
print("PASS" if condition else "FAIL")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Game Loop Tests (Timer-Based)
|
|
||||||
For tests requiring rendering, game state, or elapsed time:
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def run_test(runtime):
|
|
||||||
"""Timer callback - runs after game loop starts"""
|
|
||||||
# Now rendering is active, screenshots will work
|
|
||||||
automation.screenshot("test_result.png")
|
|
||||||
|
|
||||||
# Run your tests here
|
|
||||||
automation.click(100, 100)
|
|
||||||
|
|
||||||
# Always exit at the end
|
|
||||||
print("PASS" if success else "FAIL")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Set up the test scene
|
|
||||||
mcrfpy.createScene("test")
|
|
||||||
# ... add UI elements ...
|
|
||||||
|
|
||||||
# Schedule test to run after game loop starts
|
|
||||||
mcrfpy.setTimer("test", run_test, 100) # 0.1 seconds
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Testing Principles
|
|
||||||
- **Timer callbacks are essential**: Screenshots and UI interactions only work after the render loop starts
|
|
||||||
- **Use automation API**: Always create and examine screenshots when visual feedback is required
|
|
||||||
- **Exit properly**: Call `sys.exit()` at the end of timer-based tests to prevent hanging
|
|
||||||
- **Headless mode**: Use `--exec` flag for automated testing: `./mcrogueface --headless --exec tests/my_test.py`
|
|
||||||
|
|
||||||
### Example Test Pattern
|
|
||||||
```bash
|
|
||||||
# Run a test that requires game loop
|
|
||||||
./build/mcrogueface --headless --exec tests/issue_78_middle_click_test.py
|
|
||||||
|
|
||||||
# The test will:
|
|
||||||
# 1. Set up the scene during script execution
|
|
||||||
# 2. Register a timer callback
|
|
||||||
# 3. Game loop starts
|
|
||||||
# 4. Timer fires after 100ms
|
|
||||||
# 5. Test runs with full rendering available
|
|
||||||
# 6. Test takes screenshots and validates behavior
|
|
||||||
# 7. Test calls sys.exit() to terminate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Best Practices
|
|
||||||
|
|
||||||
### Testing and Deployment
|
|
||||||
- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, and tests shouldn't be included
|
|
||||||
|
|
||||||
## Documentation Guidelines
|
|
||||||
|
|
||||||
### Inline C++ Documentation Format
|
|
||||||
|
|
||||||
When adding new methods or modifying existing ones in C++ source files, use this documentation format in PyMethodDef arrays:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
{"method_name", (PyCFunction)Class::method, METH_VARARGS | METH_KEYWORDS,
|
|
||||||
"method_name(arg1: type, arg2: type = default) -> return_type\n\n"
|
|
||||||
"Brief description of what the method does.\n\n"
|
|
||||||
"Args:\n"
|
|
||||||
" arg1: Description of first argument\n"
|
|
||||||
" arg2: Description of second argument (default: value)\n\n"
|
|
||||||
"Returns:\n"
|
|
||||||
" Description of return value\n\n"
|
|
||||||
"Example:\n"
|
|
||||||
" result = obj.method_name(value1, value2)\n\n"
|
|
||||||
"Note:\n"
|
|
||||||
" Any important notes or caveats"},
|
|
||||||
```
|
|
||||||
|
|
||||||
For properties in PyGetSetDef arrays:
|
|
||||||
```cpp
|
|
||||||
{"property_name", (getter)getter_func, (setter)setter_func,
|
|
||||||
"Brief description of the property. "
|
|
||||||
"Additional details about valid values, side effects, etc.", NULL},
|
|
||||||
```
|
|
||||||
|
|
||||||
### Regenerating Documentation
|
|
||||||
|
|
||||||
After modifying C++ inline documentation:
|
|
||||||
|
|
||||||
1. **Rebuild the project**: `make -j$(nproc)`
|
|
||||||
|
|
||||||
2. **Generate stub files** (for IDE support):
|
|
||||||
```bash
|
|
||||||
./build/mcrogueface --exec generate_stubs.py
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Generate dynamic documentation** (recommended):
|
|
||||||
```bash
|
|
||||||
./build/mcrogueface --exec generate_dynamic_docs.py
|
|
||||||
```
|
|
||||||
This creates:
|
|
||||||
- `docs/api_reference_dynamic.html`
|
|
||||||
- `docs/API_REFERENCE_DYNAMIC.md`
|
|
||||||
|
|
||||||
4. **Update hardcoded documentation** (if still using old system):
|
|
||||||
- `generate_complete_api_docs.py` - Update method dictionaries
|
|
||||||
- `generate_complete_markdown_docs.py` - Update method dictionaries
|
|
||||||
|
|
||||||
### Important Notes
|
|
||||||
|
|
||||||
- **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python
|
|
||||||
- **Use --exec flag**: `./build/mcrogueface --exec script.py` or `--headless --exec` for CI/automation
|
|
||||||
- **Dynamic is better**: The new `generate_dynamic_docs.py` extracts documentation directly from compiled module
|
|
||||||
- **Keep docstrings consistent**: Follow the format above for automatic parsing
|
|
||||||
|
|
||||||
### Documentation Pipeline Architecture
|
|
||||||
|
|
||||||
1. **C++ Source** → PyMethodDef/PyGetSetDef arrays with docstrings
|
|
||||||
2. **Compilation** → Docstrings embedded in compiled module
|
|
||||||
3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract
|
|
||||||
4. **Generation** → HTML/Markdown/Stub files created
|
|
||||||
|
|
||||||
The documentation is only as good as the C++ inline docstrings!
|
|
||||||
38
README.md
38
README.md
|
|
@ -57,28 +57,18 @@ mcrfpy.setScene("intro")
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
### 📚 Developer Documentation
|
### 📚 Full Documentation Site
|
||||||
|
|
||||||
For comprehensive documentation about systems, architecture, and development workflows:
|
For comprehensive documentation, tutorials, and API reference, visit:
|
||||||
|
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
|
||||||
|
|
||||||
**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)**
|
The documentation site includes:
|
||||||
|
|
||||||
Key wiki pages:
|
- **[Quickstart Guide](https://mcrogueface.github.io/quickstart)** - Get running in 5 minutes
|
||||||
|
- **[McRogueFace Does The Entire Roguelike Tutorial](https://mcrogueface.github.io/tutorials)** - Step-by-step game building
|
||||||
- **[Home](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Home)** - Documentation hub with multiple entry points
|
- **[Complete API Reference](https://mcrogueface.github.io/api)** - Every function documented
|
||||||
- **[Grid System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Grid-System)** - Three-layer grid architecture
|
- **[Cookbook](https://mcrogueface.github.io/cookbook)** - Ready-to-use code recipes
|
||||||
- **[Python Binding System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Python-Binding-System)** - C++/Python integration
|
- **[C++ Extension Guide](https://mcrogueface.github.io/extending-cpp)** - For C++ developers: Add engine features
|
||||||
- **[Performance and Profiling](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Performance-and-Profiling)** - Optimization tools
|
|
||||||
- **[Adding Python Bindings](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Adding-Python-Bindings)** - Step-by-step binding guide
|
|
||||||
- **[Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap)** - All 46 open issues organized by system
|
|
||||||
|
|
||||||
### 📖 Development Guides
|
|
||||||
|
|
||||||
In the repository root:
|
|
||||||
|
|
||||||
- **[CLAUDE.md](CLAUDE.md)** - Build instructions, testing guidelines, common tasks
|
|
||||||
- **[ROADMAP.md](ROADMAP.md)** - Strategic vision and development phases
|
|
||||||
- **[roguelike_tutorial/](roguelike_tutorial/)** - Complete roguelike tutorial implementations
|
|
||||||
|
|
||||||
## Build Requirements
|
## Build Requirements
|
||||||
|
|
||||||
|
|
@ -124,15 +114,7 @@ If you are writing a game in Python using McRogueFace, you only need to rename a
|
||||||
|
|
||||||
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
|
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
|
||||||
|
|
||||||
### Issue Tracking
|
The project has a private roadmap and issue list. Reach out via email or social media if you have bugs or feature requests.
|
||||||
|
|
||||||
The project uses [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for task tracking and bug reports. Issues are organized with labels:
|
|
||||||
|
|
||||||
- **System labels** (grid, animation, python-binding, etc.) - identify which codebase area
|
|
||||||
- **Priority labels** (tier1-active, tier2-foundation, tier3-future) - development timeline
|
|
||||||
- **Type labels** (Major Feature, Minor Feature, Bugfix, etc.) - effort and scope
|
|
||||||
|
|
||||||
See the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap) on the wiki for organized view of all open tasks.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
222
ROADMAP.md
222
ROADMAP.md
|
|
@ -1,222 +0,0 @@
|
||||||
# McRogueFace - Development Roadmap
|
|
||||||
|
|
||||||
## Project Status
|
|
||||||
|
|
||||||
**Current State**: Active development - C++ game engine with Python scripting
|
|
||||||
**Latest Release**: Alpha 0.1
|
|
||||||
**Issue Tracking**: See [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current tasks and bugs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Strategic Vision
|
|
||||||
|
|
||||||
### Engine Philosophy
|
|
||||||
|
|
||||||
- **C++ First**: Performance-critical code stays in C++
|
|
||||||
- **Python Close Behind**: Rich scripting without frame-rate impact
|
|
||||||
- **Game-Ready**: Each improvement should benefit actual game development
|
|
||||||
|
|
||||||
### Architecture Goals
|
|
||||||
|
|
||||||
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
|
|
||||||
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
|
|
||||||
3. **Resource Management**: RAII everywhere, proper lifecycle handling
|
|
||||||
4. **Multi-Platform**: Windows/Linux feature parity maintained
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Architecture Decisions
|
|
||||||
|
|
||||||
### Three-Layer Grid Architecture
|
|
||||||
Following successful roguelike patterns (Caves of Qud, Cogmind, DCSS):
|
|
||||||
|
|
||||||
1. **Visual Layer** (UIGridPoint) - Sprites, colors, animations
|
|
||||||
2. **World State Layer** (TCODMap) - Walkability, transparency, physics
|
|
||||||
3. **Entity Perspective Layer** (UIGridPointState) - Per-entity FOV, knowledge
|
|
||||||
|
|
||||||
### Performance Architecture
|
|
||||||
Critical for large maps (1000x1000):
|
|
||||||
|
|
||||||
- **Spatial Hashing** for entity queries (not quadtrees!)
|
|
||||||
- **Batch Operations** with context managers (10-100x speedup)
|
|
||||||
- **Memory Pooling** for entities and components
|
|
||||||
- **Dirty Flag System** to avoid unnecessary updates
|
|
||||||
- **Zero-Copy NumPy Integration** via buffer protocol
|
|
||||||
|
|
||||||
### Key Insight from Research
|
|
||||||
"Minimizing Python/C++ boundary crossings matters more than individual function complexity"
|
|
||||||
- Batch everything possible
|
|
||||||
- Use context managers for logical operations
|
|
||||||
- Expose arrays, not individual cells
|
|
||||||
- Profile and optimize hot paths only
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Development Phases
|
|
||||||
|
|
||||||
For detailed task tracking and current priorities, see the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).
|
|
||||||
|
|
||||||
### Phase 1: Foundation Stabilization ✅
|
|
||||||
**Status**: Complete
|
|
||||||
**Key Issues**: #7 (Safe Constructors), #71 (Base Class), #87 (Visibility), #88 (Opacity)
|
|
||||||
|
|
||||||
### Phase 2: Constructor & API Polish ✅
|
|
||||||
**Status**: Complete
|
|
||||||
**Key Features**: Pythonic API, tuple support, standardized defaults
|
|
||||||
|
|
||||||
### Phase 3: Entity Lifecycle Management ✅
|
|
||||||
**Status**: Complete
|
|
||||||
**Key Issues**: #30 (Entity.die()), #93 (Vector methods), #94 (Color helpers), #103 (Timer objects)
|
|
||||||
|
|
||||||
### Phase 4: Visibility & Performance ✅
|
|
||||||
**Status**: Complete
|
|
||||||
**Key Features**: AABB culling, name system, profiling tools
|
|
||||||
|
|
||||||
### Phase 5: Window/Scene Architecture ✅
|
|
||||||
**Status**: Complete
|
|
||||||
**Key Issues**: #34 (Window object), #61 (Scene object), #1 (Resize events), #105 (Scene transitions)
|
|
||||||
|
|
||||||
### Phase 6: Rendering Revolution ✅
|
|
||||||
**Status**: Complete
|
|
||||||
**Key Issues**: #50 (Grid backgrounds), #6 (RenderTexture), #8 (Viewport rendering)
|
|
||||||
|
|
||||||
### Phase 7: Documentation & Distribution
|
|
||||||
**Status**: In Progress
|
|
||||||
**Key Issues**: #85 (Docstrings), #86 (Parameter docs), #108 (Type stubs), #97 (API docs)
|
|
||||||
|
|
||||||
See [current open issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues?state=open) for active work.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔮 Future Vision: Pure Python Extension Architecture
|
|
||||||
|
|
||||||
### Concept: McRogueFace as a Traditional Python Package
|
|
||||||
**Status**: Long-term vision
|
|
||||||
**Complexity**: Major architectural overhaul
|
|
||||||
|
|
||||||
Instead of being a C++ application that embeds Python, McRogueFace could be redesigned as a pure Python extension module that can be installed via `pip install mcrogueface`.
|
|
||||||
|
|
||||||
### Technical Approach
|
|
||||||
|
|
||||||
1. **Separate Core Engine from Python Embedding**
|
|
||||||
- Extract SFML rendering, audio, and input into C++ extension modules
|
|
||||||
- Remove embedded CPython interpreter
|
|
||||||
- Use Python's C API to expose functionality
|
|
||||||
|
|
||||||
2. **Module Structure**
|
|
||||||
```
|
|
||||||
mcrfpy/
|
|
||||||
├── __init__.py # Pure Python coordinator
|
|
||||||
├── _core.so # C++ rendering/game loop extension
|
|
||||||
├── _sfml.so # SFML bindings
|
|
||||||
├── _audio.so # Audio system bindings
|
|
||||||
└── engine.py # Python game engine logic
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Inverted Control Flow**
|
|
||||||
- Python drives the main loop instead of C++
|
|
||||||
- C++ extensions handle performance-critical operations
|
|
||||||
- Python manages game logic, scenes, and entity systems
|
|
||||||
|
|
||||||
### Benefits
|
|
||||||
|
|
||||||
- **Standard Python Packaging**: `pip install mcrogueface`
|
|
||||||
- **Virtual Environment Support**: Works with venv, conda, poetry
|
|
||||||
- **Better IDE Integration**: Standard Python development workflow
|
|
||||||
- **Easier Testing**: Use pytest, standard Python testing tools
|
|
||||||
- **Cross-Python Compatibility**: Support multiple Python versions
|
|
||||||
- **Modular Architecture**: Users can import only what they need
|
|
||||||
|
|
||||||
### Challenges
|
|
||||||
|
|
||||||
- **Major Refactoring**: Complete restructure of codebase
|
|
||||||
- **Performance Considerations**: Python-driven main loop overhead
|
|
||||||
- **Build Complexity**: Multiple extension modules to compile
|
|
||||||
- **Platform Support**: Need wheels for many platform/Python combinations
|
|
||||||
- **API Stability**: Would need careful design to maintain compatibility
|
|
||||||
|
|
||||||
### Example Usage (Future Vision)
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import Scene, Frame, Sprite, Grid
|
|
||||||
|
|
||||||
# Create game directly in Python
|
|
||||||
game = mcrfpy.Game(width=1024, height=768)
|
|
||||||
|
|
||||||
# Define scenes using Python classes
|
|
||||||
class MainMenu(Scene):
|
|
||||||
def on_enter(self):
|
|
||||||
self.ui.append(Frame(100, 100, 200, 50))
|
|
||||||
self.ui.append(Sprite("logo.png", x=400, y=100))
|
|
||||||
|
|
||||||
def on_keypress(self, key, pressed):
|
|
||||||
if key == "ENTER" and pressed:
|
|
||||||
self.game.set_scene("game")
|
|
||||||
|
|
||||||
# Run the game
|
|
||||||
game.add_scene("menu", MainMenu())
|
|
||||||
game.run()
|
|
||||||
```
|
|
||||||
|
|
||||||
This architecture would make McRogueFace a first-class Python citizen, following standard Python packaging conventions while maintaining high performance through C++ extensions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Major Feature Areas
|
|
||||||
|
|
||||||
For current status and detailed tasks, see the corresponding Gitea issue labels:
|
|
||||||
|
|
||||||
### Core Systems
|
|
||||||
- **UI/Rendering System**: Issues tagged `[Major Feature]` related to rendering
|
|
||||||
- **Grid/Entity System**: Pathfinding, FOV, entity management
|
|
||||||
- **Animation System**: Property animation, easing functions, callbacks
|
|
||||||
- **Scene/Window Management**: Scene lifecycle, transitions, viewport
|
|
||||||
|
|
||||||
### Performance Optimization
|
|
||||||
- **#115**: SpatialHash for 10,000+ entities
|
|
||||||
- **#116**: Dirty flag system
|
|
||||||
- **#113**: Batch operations for NumPy-style access
|
|
||||||
- **#117**: Memory pool for entities
|
|
||||||
|
|
||||||
### Advanced Features
|
|
||||||
- **#118**: Scene as Drawable (scenes can be drawn/animated)
|
|
||||||
- **#122**: Parent-Child UI System
|
|
||||||
- **#123**: Grid Subgrid System (256x256 chunks)
|
|
||||||
- **#124**: Grid Point Animation
|
|
||||||
- **#106**: Shader support
|
|
||||||
- **#107**: Particle system
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- **#92**: Inline C++ documentation system
|
|
||||||
- **#91**: Python type stub files (.pyi)
|
|
||||||
- **#97**: Automated API documentation extraction
|
|
||||||
- **#126**: Generate perfectly consistent Python interface
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Resources
|
|
||||||
|
|
||||||
- **Issue Tracker**: [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues)
|
|
||||||
- **Source Code**: [Gitea Repository](https://gamedev.ffwf.net/gitea/john/McRogueFace)
|
|
||||||
- **Documentation**: See `CLAUDE.md` for build instructions and development guide
|
|
||||||
- **Tutorial**: See `roguelike_tutorial/` for implementation examples
|
|
||||||
- **Workflow**: See "Gitea-First Workflow" section in `CLAUDE.md` for issue management best practices
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Development Workflow
|
|
||||||
|
|
||||||
**Gitea is the Single Source of Truth** for this project. Before starting any work:
|
|
||||||
|
|
||||||
1. **Check Gitea Issues** for existing tasks, bugs, or related work
|
|
||||||
2. **Create granular issues** for new features or problems
|
|
||||||
3. **Update issues** when work affects other systems
|
|
||||||
4. **Document discoveries** - if something is undocumented or misleading, create a task to fix it
|
|
||||||
5. **Cross-reference commits** with issue numbers (e.g., "Fixes #104")
|
|
||||||
|
|
||||||
See the "Gitea-First Workflow" section in `CLAUDE.md` for detailed guidelines on efficient development practices using the Gitea MCP tools.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*For current priorities, task tracking, and bug reports, please use the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).*
|
|
||||||
|
|
@ -42,11 +42,8 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
||||||
updateViewport();
|
updateViewport();
|
||||||
scene = "uitest";
|
scene = "uitest";
|
||||||
scenes["uitest"] = new UITestScene(this);
|
scenes["uitest"] = new UITestScene(this);
|
||||||
|
|
||||||
McRFPy_API::game = this;
|
McRFPy_API::game = this;
|
||||||
|
|
||||||
// Initialize profiler overlay
|
|
||||||
profilerOverlay = new ProfilerOverlay(Resources::font);
|
|
||||||
|
|
||||||
// Only load game.py if no custom script/command/module/exec is specified
|
// Only load game.py if no custom script/command/module/exec is specified
|
||||||
bool should_load_game = config.script_path.empty() &&
|
bool should_load_game = config.script_path.empty() &&
|
||||||
|
|
@ -88,7 +85,6 @@ GameEngine::~GameEngine()
|
||||||
for (auto& [name, scene] : scenes) {
|
for (auto& [name, scene] : scenes) {
|
||||||
delete scene;
|
delete scene;
|
||||||
}
|
}
|
||||||
delete profilerOverlay;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameEngine::cleanup()
|
void GameEngine::cleanup()
|
||||||
|
|
@ -203,14 +199,10 @@ void GameEngine::run()
|
||||||
testTimers();
|
testTimers();
|
||||||
|
|
||||||
// Update Python scenes
|
// Update Python scenes
|
||||||
{
|
McRFPy_API::updatePythonScenes(frameTime);
|
||||||
ScopedTimer pyTimer(metrics.pythonScriptTime);
|
|
||||||
McRFPy_API::updatePythonScenes(frameTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update animations (only if frameTime is valid)
|
// Update animations (only if frameTime is valid)
|
||||||
if (frameTime > 0.0f && frameTime < 1.0f) {
|
if (frameTime > 0.0f && frameTime < 1.0f) {
|
||||||
ScopedTimer animTimer(metrics.animationTime);
|
|
||||||
AnimationManager::getInstance().update(frameTime);
|
AnimationManager::getInstance().update(frameTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,12 +240,6 @@ void GameEngine::run()
|
||||||
currentScene()->render();
|
currentScene()->render();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update and render profiler overlay (if enabled)
|
|
||||||
if (profilerOverlay && !headless) {
|
|
||||||
profilerOverlay->update(metrics);
|
|
||||||
profilerOverlay->render(*render_target);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display the frame
|
// Display the frame
|
||||||
if (headless) {
|
if (headless) {
|
||||||
headless_renderer->display();
|
headless_renderer->display();
|
||||||
|
|
@ -344,14 +330,6 @@ void GameEngine::processEvent(const sf::Event& event)
|
||||||
int actionCode = 0;
|
int actionCode = 0;
|
||||||
|
|
||||||
if (event.type == sf::Event::Closed) { running = false; return; }
|
if (event.type == sf::Event::Closed) { running = false; return; }
|
||||||
|
|
||||||
// Handle F3 for profiler overlay toggle
|
|
||||||
if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F3) {
|
|
||||||
if (profilerOverlay) {
|
|
||||||
profilerOverlay->toggle();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Handle window resize events
|
// Handle window resize events
|
||||||
else if (event.type == sf::Event::Resized) {
|
else if (event.type == sf::Event::Resized) {
|
||||||
// Update the viewport to handle the new window size
|
// Update the viewport to handle the new window size
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,11 @@
|
||||||
#include "McRogueFaceConfig.h"
|
#include "McRogueFaceConfig.h"
|
||||||
#include "HeadlessRenderer.h"
|
#include "HeadlessRenderer.h"
|
||||||
#include "SceneTransition.h"
|
#include "SceneTransition.h"
|
||||||
#include "Profiler.h"
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
class GameEngine
|
class GameEngine
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Forward declare nested class so private section can use it
|
|
||||||
class ProfilerOverlay;
|
|
||||||
|
|
||||||
// Viewport modes (moved here so private section can use it)
|
// Viewport modes (moved here so private section can use it)
|
||||||
enum class ViewportMode {
|
enum class ViewportMode {
|
||||||
Center, // 1:1 pixels, viewport centered in window
|
Center, // 1:1 pixels, viewport centered in window
|
||||||
|
|
@ -56,12 +51,7 @@ private:
|
||||||
sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution
|
sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution
|
||||||
sf::View gameView; // View for the game content
|
sf::View gameView; // View for the game content
|
||||||
ViewportMode viewportMode = ViewportMode::Fit;
|
ViewportMode viewportMode = ViewportMode::Fit;
|
||||||
|
|
||||||
// Profiling overlay
|
|
||||||
bool showProfilerOverlay = false; // F3 key toggles this
|
|
||||||
int overlayUpdateCounter = 0; // Only update overlay every N frames
|
|
||||||
ProfilerOverlay* profilerOverlay = nullptr; // The actual overlay renderer
|
|
||||||
|
|
||||||
void updateViewport();
|
void updateViewport();
|
||||||
|
|
||||||
void testTimers();
|
void testTimers();
|
||||||
|
|
@ -79,29 +69,17 @@ public:
|
||||||
int drawCalls = 0; // Draw calls per frame
|
int drawCalls = 0; // Draw calls per frame
|
||||||
int uiElements = 0; // Number of UI elements rendered
|
int uiElements = 0; // Number of UI elements rendered
|
||||||
int visibleElements = 0; // Number of visible elements
|
int visibleElements = 0; // Number of visible elements
|
||||||
|
|
||||||
// Detailed timing breakdowns (added for profiling system)
|
|
||||||
float gridRenderTime = 0.0f; // Time spent rendering grids (ms)
|
|
||||||
float entityRenderTime = 0.0f; // Time spent rendering entities (ms)
|
|
||||||
float fovOverlayTime = 0.0f; // Time spent rendering FOV overlays (ms)
|
|
||||||
float pythonScriptTime = 0.0f; // Time spent in Python callbacks (ms)
|
|
||||||
float animationTime = 0.0f; // Time spent updating animations (ms)
|
|
||||||
|
|
||||||
// Grid-specific metrics
|
|
||||||
int gridCellsRendered = 0; // Number of grid cells drawn this frame
|
|
||||||
int entitiesRendered = 0; // Number of entities drawn this frame
|
|
||||||
int totalEntities = 0; // Total entities in scene
|
|
||||||
|
|
||||||
// Frame time history for averaging
|
// Frame time history for averaging
|
||||||
static constexpr int HISTORY_SIZE = 60;
|
static constexpr int HISTORY_SIZE = 60;
|
||||||
float frameTimeHistory[HISTORY_SIZE] = {0};
|
float frameTimeHistory[HISTORY_SIZE] = {0};
|
||||||
int historyIndex = 0;
|
int historyIndex = 0;
|
||||||
|
|
||||||
void updateFrameTime(float deltaMs) {
|
void updateFrameTime(float deltaMs) {
|
||||||
frameTime = deltaMs;
|
frameTime = deltaMs;
|
||||||
frameTimeHistory[historyIndex] = deltaMs;
|
frameTimeHistory[historyIndex] = deltaMs;
|
||||||
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
|
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
|
||||||
|
|
||||||
// Calculate average
|
// Calculate average
|
||||||
float sum = 0.0f;
|
float sum = 0.0f;
|
||||||
for (int i = 0; i < HISTORY_SIZE; ++i) {
|
for (int i = 0; i < HISTORY_SIZE; ++i) {
|
||||||
|
|
@ -110,26 +88,13 @@ public:
|
||||||
avgFrameTime = sum / HISTORY_SIZE;
|
avgFrameTime = sum / HISTORY_SIZE;
|
||||||
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
|
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetPerFrame() {
|
void resetPerFrame() {
|
||||||
drawCalls = 0;
|
drawCalls = 0;
|
||||||
uiElements = 0;
|
uiElements = 0;
|
||||||
visibleElements = 0;
|
visibleElements = 0;
|
||||||
|
|
||||||
// Reset per-frame timing metrics
|
|
||||||
gridRenderTime = 0.0f;
|
|
||||||
entityRenderTime = 0.0f;
|
|
||||||
fovOverlayTime = 0.0f;
|
|
||||||
pythonScriptTime = 0.0f;
|
|
||||||
animationTime = 0.0f;
|
|
||||||
|
|
||||||
// Reset per-frame counters
|
|
||||||
gridCellsRendered = 0;
|
|
||||||
entitiesRendered = 0;
|
|
||||||
totalEntities = 0;
|
|
||||||
}
|
}
|
||||||
} metrics;
|
} metrics;
|
||||||
|
|
||||||
GameEngine();
|
GameEngine();
|
||||||
GameEngine(const McRogueFaceConfig& cfg);
|
GameEngine(const McRogueFaceConfig& cfg);
|
||||||
~GameEngine();
|
~GameEngine();
|
||||||
|
|
@ -179,30 +144,5 @@ public:
|
||||||
sf::Music music;
|
sf::Music music;
|
||||||
sf::Sound sfx;
|
sf::Sound sfx;
|
||||||
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> scene_ui(std::string scene);
|
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> scene_ui(std::string scene);
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Visual overlay that displays real-time profiling metrics
|
|
||||||
*/
|
|
||||||
class GameEngine::ProfilerOverlay {
|
|
||||||
private:
|
|
||||||
sf::Font& font;
|
|
||||||
sf::Text text;
|
|
||||||
sf::RectangleShape background;
|
|
||||||
bool visible;
|
|
||||||
int updateInterval;
|
|
||||||
int frameCounter;
|
|
||||||
|
|
||||||
sf::Color getPerformanceColor(float frameTimeMs);
|
|
||||||
std::string formatFloat(float value, int precision = 1);
|
|
||||||
std::string formatPercentage(float part, float total);
|
|
||||||
|
|
||||||
public:
|
|
||||||
ProfilerOverlay(sf::Font& fontRef);
|
|
||||||
void toggle();
|
|
||||||
void setVisible(bool vis);
|
|
||||||
bool isVisible() const;
|
|
||||||
void update(const ProfilingMetrics& metrics);
|
|
||||||
void render(sf::RenderTarget& target);
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
#include "Profiler.h"
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
ProfilingLogger::ProfilingLogger()
|
|
||||||
: headers_written(false)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
ProfilingLogger::~ProfilingLogger() {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ProfilingLogger::open(const std::string& filename, const std::vector<std::string>& columns) {
|
|
||||||
column_names = columns;
|
|
||||||
file.open(filename);
|
|
||||||
|
|
||||||
if (!file.is_open()) {
|
|
||||||
std::cerr << "Failed to open profiling log file: " << filename << std::endl;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write CSV header
|
|
||||||
for (size_t i = 0; i < columns.size(); ++i) {
|
|
||||||
file << columns[i];
|
|
||||||
if (i < columns.size() - 1) {
|
|
||||||
file << ",";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file << "\n";
|
|
||||||
file.flush();
|
|
||||||
|
|
||||||
headers_written = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProfilingLogger::writeRow(const std::vector<float>& values) {
|
|
||||||
if (!file.is_open()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.size() != column_names.size()) {
|
|
||||||
std::cerr << "ProfilingLogger: value count (" << values.size()
|
|
||||||
<< ") doesn't match column count (" << column_names.size() << ")" << std::endl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (size_t i = 0; i < values.size(); ++i) {
|
|
||||||
file << values[i];
|
|
||||||
if (i < values.size() - 1) {
|
|
||||||
file << ",";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProfilingLogger::close() {
|
|
||||||
if (file.is_open()) {
|
|
||||||
file.flush();
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
111
src/Profiler.h
111
src/Profiler.h
|
|
@ -1,111 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Simple RAII-based profiling timer for measuring code execution time
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* float timing = 0.0f;
|
|
||||||
* {
|
|
||||||
* ScopedTimer timer(timing);
|
|
||||||
* // ... code to profile ...
|
|
||||||
* } // timing now contains elapsed milliseconds
|
|
||||||
*/
|
|
||||||
class ScopedTimer {
|
|
||||||
private:
|
|
||||||
std::chrono::high_resolution_clock::time_point start;
|
|
||||||
float& target_ms;
|
|
||||||
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* @brief Construct a new Scoped Timer and start timing
|
|
||||||
* @param target Reference to float that will receive elapsed time in milliseconds
|
|
||||||
*/
|
|
||||||
explicit ScopedTimer(float& target)
|
|
||||||
: target_ms(target)
|
|
||||||
{
|
|
||||||
start = std::chrono::high_resolution_clock::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Destructor automatically records elapsed time
|
|
||||||
*/
|
|
||||||
~ScopedTimer() {
|
|
||||||
auto end = std::chrono::high_resolution_clock::now();
|
|
||||||
target_ms = std::chrono::duration<float, std::milli>(end - start).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent copying
|
|
||||||
ScopedTimer(const ScopedTimer&) = delete;
|
|
||||||
ScopedTimer& operator=(const ScopedTimer&) = delete;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Accumulating timer that adds elapsed time to existing value
|
|
||||||
*
|
|
||||||
* Useful for measuring total time across multiple calls in a single frame
|
|
||||||
*/
|
|
||||||
class AccumulatingTimer {
|
|
||||||
private:
|
|
||||||
std::chrono::high_resolution_clock::time_point start;
|
|
||||||
float& target_ms;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit AccumulatingTimer(float& target)
|
|
||||||
: target_ms(target)
|
|
||||||
{
|
|
||||||
start = std::chrono::high_resolution_clock::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
~AccumulatingTimer() {
|
|
||||||
auto end = std::chrono::high_resolution_clock::now();
|
|
||||||
target_ms += std::chrono::duration<float, std::milli>(end - start).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
AccumulatingTimer(const AccumulatingTimer&) = delete;
|
|
||||||
AccumulatingTimer& operator=(const AccumulatingTimer&) = delete;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief CSV profiling data logger for batch analysis
|
|
||||||
*
|
|
||||||
* Writes profiling data to CSV file for later analysis with Python/pandas/Excel
|
|
||||||
*/
|
|
||||||
class ProfilingLogger {
|
|
||||||
private:
|
|
||||||
std::ofstream file;
|
|
||||||
bool headers_written;
|
|
||||||
std::vector<std::string> column_names;
|
|
||||||
|
|
||||||
public:
|
|
||||||
ProfilingLogger();
|
|
||||||
~ProfilingLogger();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Open a CSV file for writing profiling data
|
|
||||||
* @param filename Path to CSV file
|
|
||||||
* @param columns Column names for the CSV header
|
|
||||||
* @return true if file opened successfully
|
|
||||||
*/
|
|
||||||
bool open(const std::string& filename, const std::vector<std::string>& columns);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Write a row of profiling data
|
|
||||||
* @param values Data values (must match column count)
|
|
||||||
*/
|
|
||||||
void writeRow(const std::vector<float>& values);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Close the file and flush data
|
|
||||||
*/
|
|
||||||
void close();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if logger is ready to write
|
|
||||||
*/
|
|
||||||
bool isOpen() const { return file.is_open(); }
|
|
||||||
};
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
#include "GameEngine.h"
|
|
||||||
#include <sstream>
|
|
||||||
#include <iomanip>
|
|
||||||
|
|
||||||
GameEngine::ProfilerOverlay::ProfilerOverlay(sf::Font& fontRef)
|
|
||||||
: font(fontRef), visible(false), updateInterval(10), frameCounter(0)
|
|
||||||
{
|
|
||||||
text.setFont(font);
|
|
||||||
text.setCharacterSize(14);
|
|
||||||
text.setFillColor(sf::Color::White);
|
|
||||||
text.setPosition(10.0f, 10.0f);
|
|
||||||
|
|
||||||
// Semi-transparent dark background
|
|
||||||
background.setFillColor(sf::Color(0, 0, 0, 180));
|
|
||||||
background.setPosition(5.0f, 5.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::ProfilerOverlay::toggle() {
|
|
||||||
visible = !visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::ProfilerOverlay::setVisible(bool vis) {
|
|
||||||
visible = vis;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool GameEngine::ProfilerOverlay::isVisible() const {
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
sf::Color GameEngine::ProfilerOverlay::getPerformanceColor(float frameTimeMs) {
|
|
||||||
if (frameTimeMs < 16.6f) {
|
|
||||||
return sf::Color::Green; // 60+ FPS
|
|
||||||
} else if (frameTimeMs < 33.3f) {
|
|
||||||
return sf::Color::Yellow; // 30-60 FPS
|
|
||||||
} else {
|
|
||||||
return sf::Color::Red; // <30 FPS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string GameEngine::ProfilerOverlay::formatFloat(float value, int precision) {
|
|
||||||
std::stringstream ss;
|
|
||||||
ss << std::fixed << std::setprecision(precision) << value;
|
|
||||||
return ss.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string GameEngine::ProfilerOverlay::formatPercentage(float part, float total) {
|
|
||||||
if (total <= 0.0f) return "0%";
|
|
||||||
float pct = (part / total) * 100.0f;
|
|
||||||
return formatFloat(pct, 0) + "%";
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::ProfilerOverlay::update(const ProfilingMetrics& metrics) {
|
|
||||||
if (!visible) return;
|
|
||||||
|
|
||||||
// Only update text every N frames to reduce overhead
|
|
||||||
frameCounter++;
|
|
||||||
if (frameCounter < updateInterval) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
frameCounter = 0;
|
|
||||||
|
|
||||||
std::stringstream ss;
|
|
||||||
ss << "McRogueFace Performance Monitor\n";
|
|
||||||
ss << "================================\n";
|
|
||||||
|
|
||||||
// Frame time and FPS
|
|
||||||
float frameMs = metrics.avgFrameTime;
|
|
||||||
ss << "FPS: " << metrics.fps << " (" << formatFloat(frameMs, 1) << "ms/frame)\n";
|
|
||||||
|
|
||||||
// Performance warning
|
|
||||||
if (frameMs > 33.3f) {
|
|
||||||
ss << "WARNING: Frame time exceeds 30 FPS target!\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
ss << "\n";
|
|
||||||
|
|
||||||
// Timing breakdown
|
|
||||||
ss << "Frame Time Breakdown:\n";
|
|
||||||
ss << " Grid Render: " << formatFloat(metrics.gridRenderTime, 1) << "ms ("
|
|
||||||
<< formatPercentage(metrics.gridRenderTime, frameMs) << ")\n";
|
|
||||||
ss << " Cells: " << metrics.gridCellsRendered << " rendered\n";
|
|
||||||
ss << " Entities: " << metrics.entitiesRendered << " / " << metrics.totalEntities << " drawn\n";
|
|
||||||
|
|
||||||
if (metrics.fovOverlayTime > 0.01f) {
|
|
||||||
ss << " FOV Overlay: " << formatFloat(metrics.fovOverlayTime, 1) << "ms\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metrics.entityRenderTime > 0.01f) {
|
|
||||||
ss << " Entity Render: " << formatFloat(metrics.entityRenderTime, 1) << "ms ("
|
|
||||||
<< formatPercentage(metrics.entityRenderTime, frameMs) << ")\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metrics.pythonScriptTime > 0.01f) {
|
|
||||||
ss << " Python: " << formatFloat(metrics.pythonScriptTime, 1) << "ms ("
|
|
||||||
<< formatPercentage(metrics.pythonScriptTime, frameMs) << ")\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metrics.animationTime > 0.01f) {
|
|
||||||
ss << " Animations: " << formatFloat(metrics.animationTime, 1) << "ms ("
|
|
||||||
<< formatPercentage(metrics.animationTime, frameMs) << ")\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
ss << "\n";
|
|
||||||
|
|
||||||
// Other metrics
|
|
||||||
ss << "Draw Calls: " << metrics.drawCalls << "\n";
|
|
||||||
ss << "UI Elements: " << metrics.uiElements << " (" << metrics.visibleElements << " visible)\n";
|
|
||||||
|
|
||||||
// Calculate unaccounted time
|
|
||||||
float accountedTime = metrics.gridRenderTime + metrics.entityRenderTime +
|
|
||||||
metrics.pythonScriptTime + metrics.animationTime;
|
|
||||||
float unaccountedTime = frameMs - accountedTime;
|
|
||||||
|
|
||||||
if (unaccountedTime > 1.0f) {
|
|
||||||
ss << "\n";
|
|
||||||
ss << "Other: " << formatFloat(unaccountedTime, 1) << "ms ("
|
|
||||||
<< formatPercentage(unaccountedTime, frameMs) << ")\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
ss << "\n";
|
|
||||||
ss << "Press F3 to hide this overlay";
|
|
||||||
|
|
||||||
text.setString(ss.str());
|
|
||||||
|
|
||||||
// Update background size to fit text
|
|
||||||
sf::FloatRect textBounds = text.getLocalBounds();
|
|
||||||
background.setSize(sf::Vector2f(textBounds.width + 20.0f, textBounds.height + 20.0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::ProfilerOverlay::render(sf::RenderTarget& target) {
|
|
||||||
if (!visible) return;
|
|
||||||
|
|
||||||
target.draw(background);
|
|
||||||
target.draw(text);
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "UIEntity.h"
|
#include "UIEntity.h"
|
||||||
#include "Profiler.h"
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
|
|
||||||
|
|
@ -96,14 +95,11 @@ void UIGrid::update() {}
|
||||||
|
|
||||||
void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
{
|
{
|
||||||
// Profile total grid rendering time
|
|
||||||
ScopedTimer gridTimer(Resources::game->metrics.gridRenderTime);
|
|
||||||
|
|
||||||
// Check visibility
|
// Check visibility
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
|
|
||||||
// TODO: Apply opacity to output sprite
|
// TODO: Apply opacity to output sprite
|
||||||
|
|
||||||
output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing
|
output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing
|
||||||
// output size can change; update size when drawing
|
// output size can change; update size when drawing
|
||||||
output.setTextureRect(
|
output.setTextureRect(
|
||||||
|
|
@ -139,12 +135,11 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
if (y_limit > grid_y) y_limit = grid_y;
|
if (y_limit > grid_y) y_limit = grid_y;
|
||||||
|
|
||||||
// base layer - bottom color, tile sprite ("ground")
|
// base layer - bottom color, tile sprite ("ground")
|
||||||
int cellsRendered = 0;
|
|
||||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
||||||
x < x_limit; //x < view_width;
|
x < x_limit; //x < view_width;
|
||||||
x+=1)
|
x+=1)
|
||||||
{
|
{
|
||||||
//for (float y = (top_edge >= 0 ? top_edge : 0);
|
//for (float y = (top_edge >= 0 ? top_edge : 0);
|
||||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
||||||
y < y_limit; //y < view_height;
|
y < y_limit; //y < view_height;
|
||||||
y+=1)
|
y+=1)
|
||||||
|
|
@ -168,53 +163,35 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);;
|
sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);;
|
||||||
renderTexture.draw(sprite);
|
renderTexture.draw(sprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
cellsRendered++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record how many cells were rendered
|
|
||||||
Resources::game->metrics.gridCellsRendered += cellsRendered;
|
|
||||||
|
|
||||||
// middle layer - entities
|
// middle layer - entities
|
||||||
// disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window)
|
// disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window)
|
||||||
{
|
for (auto e : *entities) {
|
||||||
ScopedTimer entityTimer(Resources::game->metrics.entityRenderTime);
|
// Skip out-of-bounds entities for performance
|
||||||
int entitiesRendered = 0;
|
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities)
|
||||||
int totalEntities = entities->size();
|
if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 ||
|
||||||
|
e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) {
|
||||||
for (auto e : *entities) {
|
continue; // Skip this entity as it's not visible
|
||||||
// Skip out-of-bounds entities for performance
|
|
||||||
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities)
|
|
||||||
if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 ||
|
|
||||||
e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) {
|
|
||||||
continue; // Skip this entity as it's not visible
|
|
||||||
}
|
|
||||||
|
|
||||||
//auto drawent = e->cGrid->indexsprite.drawable();
|
|
||||||
auto& drawent = e->sprite;
|
|
||||||
//drawent.setScale(zoom, zoom);
|
|
||||||
drawent.setScale(sf::Vector2f(zoom, zoom));
|
|
||||||
auto pixel_pos = sf::Vector2f(
|
|
||||||
(e->position.x*cell_width - left_spritepixels) * zoom,
|
|
||||||
(e->position.y*cell_height - top_spritepixels) * zoom );
|
|
||||||
//drawent.setPosition(pixel_pos);
|
|
||||||
//renderTexture.draw(drawent);
|
|
||||||
drawent.render(pixel_pos, renderTexture);
|
|
||||||
|
|
||||||
entitiesRendered++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record entity rendering stats
|
//auto drawent = e->cGrid->indexsprite.drawable();
|
||||||
Resources::game->metrics.entitiesRendered += entitiesRendered;
|
auto& drawent = e->sprite;
|
||||||
Resources::game->metrics.totalEntities += totalEntities;
|
//drawent.setScale(zoom, zoom);
|
||||||
|
drawent.setScale(sf::Vector2f(zoom, zoom));
|
||||||
|
auto pixel_pos = sf::Vector2f(
|
||||||
|
(e->position.x*cell_width - left_spritepixels) * zoom,
|
||||||
|
(e->position.y*cell_height - top_spritepixels) * zoom );
|
||||||
|
//drawent.setPosition(pixel_pos);
|
||||||
|
//renderTexture.draw(drawent);
|
||||||
|
drawent.render(pixel_pos, renderTexture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// top layer - opacity for discovered / visible status based on perspective
|
// top layer - opacity for discovered / visible status based on perspective
|
||||||
// Only render visibility overlay if perspective is enabled
|
// Only render visibility overlay if perspective is enabled
|
||||||
if (perspective_enabled) {
|
if (perspective_enabled) {
|
||||||
ScopedTimer fovTimer(Resources::game->metrics.fovOverlayTime);
|
|
||||||
auto entity = perspective_entity.lock();
|
auto entity = perspective_entity.lock();
|
||||||
|
|
||||||
// Create rectangle for overlays
|
// Create rectangle for overlays
|
||||||
|
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
"""
|
|
||||||
Benchmark: Moving Entities Performance Test
|
|
||||||
|
|
||||||
This benchmark measures McRogueFace's performance with 50 randomly moving
|
|
||||||
entities on a 100x100 grid.
|
|
||||||
|
|
||||||
Expected results:
|
|
||||||
- Should maintain 60 FPS
|
|
||||||
- Entity render time should be <3ms
|
|
||||||
- Grid render time will be higher due to constant updates (no dirty flag benefit)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
./build/mcrogueface --exec tests/benchmark_moving_entities.py
|
|
||||||
|
|
||||||
Press F3 to toggle performance overlay
|
|
||||||
Press ESC to exit
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create the benchmark scene
|
|
||||||
mcrfpy.createScene("benchmark")
|
|
||||||
mcrfpy.setScene("benchmark")
|
|
||||||
|
|
||||||
# Get scene UI
|
|
||||||
ui = mcrfpy.sceneUI("benchmark")
|
|
||||||
|
|
||||||
# Create a 100x100 grid
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
grid_size=(100, 100),
|
|
||||||
pos=(0, 0),
|
|
||||||
size=(1024, 768)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Simple floor pattern
|
|
||||||
for x in range(100):
|
|
||||||
for y in range(100):
|
|
||||||
cell = grid.at((x, y))
|
|
||||||
cell.tilesprite = 0
|
|
||||||
cell.color = (40, 40, 40, 255)
|
|
||||||
|
|
||||||
# Create 50 entities with random positions and velocities
|
|
||||||
entities = []
|
|
||||||
ENTITY_COUNT = 50
|
|
||||||
|
|
||||||
for i in range(ENTITY_COUNT):
|
|
||||||
entity = mcrfpy.Entity(
|
|
||||||
grid_pos=(random.randint(0, 99), random.randint(0, 99)),
|
|
||||||
sprite_index=random.randint(10, 20) # Use varied sprites
|
|
||||||
)
|
|
||||||
|
|
||||||
# Give each entity a random velocity
|
|
||||||
entity.velocity_x = random.uniform(-0.5, 0.5)
|
|
||||||
entity.velocity_y = random.uniform(-0.5, 0.5)
|
|
||||||
|
|
||||||
grid.entities.append(entity)
|
|
||||||
entities.append(entity)
|
|
||||||
|
|
||||||
ui.append(grid)
|
|
||||||
|
|
||||||
# Instructions caption
|
|
||||||
instructions = mcrfpy.Caption(
|
|
||||||
text=f"Moving Entities Benchmark ({ENTITY_COUNT} entities)\n"
|
|
||||||
"Press F3 for performance overlay\n"
|
|
||||||
"Press ESC to exit\n"
|
|
||||||
"Goal: 60 FPS with entities moving",
|
|
||||||
pos=(10, 10),
|
|
||||||
fill_color=(255, 255, 0, 255)
|
|
||||||
)
|
|
||||||
ui.append(instructions)
|
|
||||||
|
|
||||||
# Benchmark info
|
|
||||||
print("=" * 60)
|
|
||||||
print("MOVING ENTITIES BENCHMARK")
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"Entity count: {ENTITY_COUNT}")
|
|
||||||
print("Grid size: 100x100 cells")
|
|
||||||
print("Expected FPS: 60")
|
|
||||||
print("")
|
|
||||||
print("Entities move randomly and bounce off walls.")
|
|
||||||
print("This tests entity rendering performance and position updates.")
|
|
||||||
print("")
|
|
||||||
print("Press F3 in-game to see real-time performance metrics.")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Exit handler
|
|
||||||
def handle_key(key, state):
|
|
||||||
if key == "Escape" and state:
|
|
||||||
print("\nBenchmark ended by user")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_key)
|
|
||||||
|
|
||||||
# Update entity positions
|
|
||||||
def update_entities(ms):
|
|
||||||
dt = ms / 1000.0 # Convert to seconds
|
|
||||||
|
|
||||||
for entity in entities:
|
|
||||||
# Update position
|
|
||||||
new_x = entity.x + entity.velocity_x
|
|
||||||
new_y = entity.y + entity.velocity_y
|
|
||||||
|
|
||||||
# Bounce off walls
|
|
||||||
if new_x < 0 or new_x >= 100:
|
|
||||||
entity.velocity_x = -entity.velocity_x
|
|
||||||
new_x = max(0, min(99, new_x))
|
|
||||||
|
|
||||||
if new_y < 0 or new_y >= 100:
|
|
||||||
entity.velocity_y = -entity.velocity_y
|
|
||||||
new_y = max(0, min(99, new_y))
|
|
||||||
|
|
||||||
# Update entity position
|
|
||||||
entity.x = new_x
|
|
||||||
entity.y = new_y
|
|
||||||
|
|
||||||
# Run movement update every frame (16ms)
|
|
||||||
mcrfpy.setTimer("movement", update_entities, 16)
|
|
||||||
|
|
||||||
# Benchmark statistics
|
|
||||||
frame_count = 0
|
|
||||||
start_time = None
|
|
||||||
|
|
||||||
def benchmark_timer(ms):
|
|
||||||
global frame_count, start_time
|
|
||||||
|
|
||||||
if start_time is None:
|
|
||||||
import time
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
frame_count += 1
|
|
||||||
|
|
||||||
# After 10 seconds, print summary
|
|
||||||
import time
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
|
|
||||||
if elapsed >= 10.0:
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("BENCHMARK COMPLETE")
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"Frames rendered: {frame_count}")
|
|
||||||
print(f"Time elapsed: {elapsed:.2f}s")
|
|
||||||
print(f"Average FPS: {frame_count / elapsed:.1f}")
|
|
||||||
print(f"Entities: {ENTITY_COUNT}")
|
|
||||||
print("")
|
|
||||||
print("Check profiler overlay (F3) for detailed timing breakdown.")
|
|
||||||
print("Entity render time and total frame time are key metrics.")
|
|
||||||
print("=" * 60)
|
|
||||||
# Don't exit - let user review
|
|
||||||
|
|
||||||
mcrfpy.setTimer("benchmark", benchmark_timer, 100)
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
"""
|
|
||||||
Benchmark: Static Grid Performance Test
|
|
||||||
|
|
||||||
This benchmark measures McRogueFace's grid rendering performance with a static
|
|
||||||
100x100 grid. The goal is 60 FPS with minimal CPU usage.
|
|
||||||
|
|
||||||
Expected results:
|
|
||||||
- 60 FPS (16.6ms per frame)
|
|
||||||
- Grid render time should be <2ms after dirty flag optimization
|
|
||||||
- Currently will be higher (likely 8-12ms) - this establishes baseline
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
./build/mcrogueface --exec tests/benchmark_static_grid.py
|
|
||||||
|
|
||||||
Press F3 to toggle performance overlay
|
|
||||||
Press ESC to exit
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Create the benchmark scene
|
|
||||||
mcrfpy.createScene("benchmark")
|
|
||||||
mcrfpy.setScene("benchmark")
|
|
||||||
|
|
||||||
# Get scene UI
|
|
||||||
ui = mcrfpy.sceneUI("benchmark")
|
|
||||||
|
|
||||||
# Create a 100x100 grid with default texture
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
grid_size=(100, 100),
|
|
||||||
pos=(0, 0),
|
|
||||||
size=(1024, 768)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fill grid with varied tile patterns to ensure realistic rendering
|
|
||||||
for x in range(100):
|
|
||||||
for y in range(100):
|
|
||||||
cell = grid.at((x, y))
|
|
||||||
# Checkerboard pattern with different sprites
|
|
||||||
if (x + y) % 2 == 0:
|
|
||||||
cell.tilesprite = 0
|
|
||||||
cell.color = (50, 50, 50, 255)
|
|
||||||
else:
|
|
||||||
cell.tilesprite = 1
|
|
||||||
cell.color = (70, 70, 70, 255)
|
|
||||||
|
|
||||||
# Add some variation
|
|
||||||
if x % 10 == 0 or y % 10 == 0:
|
|
||||||
cell.tilesprite = 2
|
|
||||||
cell.color = (100, 100, 100, 255)
|
|
||||||
|
|
||||||
# Add grid to scene
|
|
||||||
ui.append(grid)
|
|
||||||
|
|
||||||
# Instructions caption
|
|
||||||
instructions = mcrfpy.Caption(
|
|
||||||
text="Static Grid Benchmark (100x100)\n"
|
|
||||||
"Press F3 for performance overlay\n"
|
|
||||||
"Press ESC to exit\n"
|
|
||||||
"Goal: 60 FPS with low grid render time",
|
|
||||||
pos=(10, 10),
|
|
||||||
fill_color=(255, 255, 0, 255)
|
|
||||||
)
|
|
||||||
ui.append(instructions)
|
|
||||||
|
|
||||||
# Benchmark info
|
|
||||||
print("=" * 60)
|
|
||||||
print("STATIC GRID BENCHMARK")
|
|
||||||
print("=" * 60)
|
|
||||||
print("Grid size: 100x100 cells")
|
|
||||||
print("Expected FPS: 60")
|
|
||||||
print("Tiles rendered: ~1024 visible cells per frame")
|
|
||||||
print("")
|
|
||||||
print("This benchmark establishes baseline grid rendering performance.")
|
|
||||||
print("After dirty flag optimization, grid render time should drop")
|
|
||||||
print("significantly for static content.")
|
|
||||||
print("")
|
|
||||||
print("Press F3 in-game to see real-time performance metrics.")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Exit handler
|
|
||||||
def handle_key(key, state):
|
|
||||||
if key == "Escape" and state:
|
|
||||||
print("\nBenchmark ended by user")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_key)
|
|
||||||
|
|
||||||
# Run for 10 seconds then provide summary
|
|
||||||
frame_count = 0
|
|
||||||
start_time = None
|
|
||||||
|
|
||||||
def benchmark_timer(ms):
|
|
||||||
global frame_count, start_time
|
|
||||||
|
|
||||||
if start_time is None:
|
|
||||||
import time
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
frame_count += 1
|
|
||||||
|
|
||||||
# After 10 seconds, print summary and exit
|
|
||||||
import time
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
|
|
||||||
if elapsed >= 10.0:
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("BENCHMARK COMPLETE")
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"Frames rendered: {frame_count}")
|
|
||||||
print(f"Time elapsed: {elapsed:.2f}s")
|
|
||||||
print(f"Average FPS: {frame_count / elapsed:.1f}")
|
|
||||||
print("")
|
|
||||||
print("Check profiler overlay (F3) for detailed timing breakdown.")
|
|
||||||
print("Grid render time is the key metric for optimization.")
|
|
||||||
print("=" * 60)
|
|
||||||
# Don't exit automatically - let user review with F3
|
|
||||||
# sys.exit(0)
|
|
||||||
|
|
||||||
# Update every 100ms
|
|
||||||
mcrfpy.setTimer("benchmark", benchmark_timer, 100)
|
|
||||||
Loading…
Reference in New Issue