mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Compare commits
5 Commits
a973b3cf8f
...
2d3715c5ff
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d3715c5ff | |||
| fe44599eb4 | |||
| 46e9d5b51e | |||
| dd97bf621b | |||
| c0adf86740 |
155
gameengine/README.md
Normal file
155
gameengine/README.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# GameEngine
|
||||
|
||||
SDL3 GPU 2D/3D game engine with a JSON-driven workflow system. Built with C++20, using Conan for dependency management and CMake + Ninja for builds.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- C++20 compiler (MSVC, Clang, or GCC)
|
||||
- [CMake](https://cmake.org/) 3.24+
|
||||
- [Conan](https://conan.io/) 2.x
|
||||
- [Ninja](https://ninja-build.org/) (recommended)
|
||||
- Python 3.9+ (build helper)
|
||||
|
||||
### Build & Run (Recommended)
|
||||
|
||||
The `dev_commands.py` build helper runs the full pipeline: Conan install, CMake generate, configure, and build.
|
||||
|
||||
```bash
|
||||
# Build and run the seed demo (auto-detects platform bootstrap)
|
||||
python python/dev_commands.py all --run
|
||||
|
||||
# Equivalent explicit form on Windows
|
||||
python python/dev_commands.py all --run --bootstrap bootstrap_windows --game seed
|
||||
|
||||
# macOS
|
||||
python python/dev_commands.py all --run --bootstrap bootstrap_mac --game seed
|
||||
|
||||
# Linux
|
||||
python python/dev_commands.py all --run --bootstrap bootstrap_linux --game seed
|
||||
```
|
||||
|
||||
### Build Steps (Manual)
|
||||
|
||||
```bash
|
||||
# 1. Install Conan dependencies
|
||||
python python/dev_commands.py dependencies
|
||||
|
||||
# 2. Generate CMakeLists.txt from cmake_config.json
|
||||
python python/dev_commands.py generate
|
||||
|
||||
# 3. Configure CMake
|
||||
python python/dev_commands.py configure --preset conan-default
|
||||
|
||||
# 4. Build
|
||||
python python/dev_commands.py build
|
||||
|
||||
# 5. Run
|
||||
python python/dev_commands.py run -- --bootstrap bootstrap_windows --game seed
|
||||
```
|
||||
|
||||
### Makefile (Unix)
|
||||
|
||||
```bash
|
||||
make build # Build sdl3_app
|
||||
make build TARGET=sdl3_app # Build specific target
|
||||
make test # Build and run all tests
|
||||
make rebuild # Clean + build
|
||||
make list-targets # Show available targets
|
||||
```
|
||||
|
||||
## CLI Arguments
|
||||
|
||||
The compiled `sdl3_app` binary accepts:
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--bootstrap` | `bootstrap_mac` | Platform bootstrap package |
|
||||
| `--game` | `standalone_cubes` | Game package to load |
|
||||
| `--project-root` | Current directory | Path to gameengine root |
|
||||
|
||||
## Packages
|
||||
|
||||
Game content and platform initialization are defined as JSON packages in `packages/`:
|
||||
|
||||
| Package | Type | Description |
|
||||
|---------|------|-------------|
|
||||
| `bootstrap_windows` | bootloader | Windows init, SDL3 GPU with D3D12 |
|
||||
| `bootstrap_mac` | bootloader | macOS init, SDL3 GPU with Metal |
|
||||
| `bootstrap_linux` | bootloader | Linux init, SDL3 GPU with Vulkan |
|
||||
| `seed` | game | FPS demo: room with spinning cube, WASD movement, mouse look, jump. Bullet3 physics |
|
||||
| `standalone_cubes` | game | 11x11 grid of spinning colored cubes with per-cube animation offsets |
|
||||
| `quake3` | game | Quake 3 BSP map viewer with FPS controls |
|
||||
| `soundboard` | game | Audio cues + GUI controls |
|
||||
| `engine_tester` | game | Validation tour with teleport checkpoints, captures, and diagnostics |
|
||||
| `asset_loader` | library | Universal asset loading with cross-engine unit conversion (JSON-driven) |
|
||||
| `assets` | library | Shared runtime assets (audio, fonts, images) |
|
||||
| `materialx` | library | MaterialX integration |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
main.cpp # Entry point, CLI parsing, workflow bootstrap
|
||||
services/
|
||||
interfaces/ # Abstract service interfaces (16 services)
|
||||
i_audio_service.hpp
|
||||
i_graphics_service.hpp
|
||||
i_input_service.hpp
|
||||
i_physics_service.hpp
|
||||
i_scene_service.hpp
|
||||
i_window_service.hpp
|
||||
i_workflow_executor.hpp
|
||||
...
|
||||
impl/ # Concrete implementations
|
||||
workflow/ # Workflow step implementations
|
||||
diagnostics/ # Logging
|
||||
app/ # CLI, platform services
|
||||
|
||||
packages/ # JSON-driven game content
|
||||
{package}/
|
||||
package.json # Package metadata, config, shader list
|
||||
workflows/*.json # Workflow definitions (v2.2.0 format)
|
||||
shaders/spirv/ # SPIR-V shaders (Vulkan, D3D12)
|
||||
shaders/msl/ # Metal shaders (macOS)
|
||||
scene/*.json # Scene definitions
|
||||
assets/ # Package-specific assets
|
||||
|
||||
python/
|
||||
dev_commands.py # Build helper CLI
|
||||
generate_cmake.py # CMakeLists.txt generator from cmake_config.json
|
||||
export_room_gltf.py # Seed demo room glTF exporter
|
||||
```
|
||||
|
||||
## Dependencies (Conan)
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| SDL | 3.2.20 | Windowing, input, GPU API |
|
||||
| Bullet3 | 3.25 | 3D physics |
|
||||
| Box2D | 3.1.1 | 2D physics |
|
||||
| EnTT | 3.16.0 | Entity Component System |
|
||||
| Assimp | 6.0.2 | 3D model import |
|
||||
| glm | 1.0.1 | Math library |
|
||||
| nlohmann_json | 3.11.3 | JSON parsing |
|
||||
| RapidJSON | cci.20230929 | JSON parsing (performance) |
|
||||
| FFmpeg | 8.0.1 | Audio/video decoding |
|
||||
| FreeType | 2.13.2 | Font rendering |
|
||||
| Cairo | 1.18.0 | 2D vector graphics |
|
||||
| stb | cci.20230920 | Image loading |
|
||||
| LunaSVG | 3.0.1 | SVG rendering |
|
||||
| libzip | 1.10.1 | ZIP archive support |
|
||||
| cpptrace | 1.0.4 | Stack traces |
|
||||
| CLI11 | 2.6.0 | Command-line parsing |
|
||||
| GTest | 1.17.0 | Testing framework |
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Via Makefile
|
||||
make test
|
||||
|
||||
# Via dev_commands.py
|
||||
python python/dev_commands.py build --target test_exit_step
|
||||
```
|
||||
@@ -34,11 +34,123 @@ import argparse
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Sequence
|
||||
|
||||
IS_WINDOWS = platform.system() == "Windows"
|
||||
|
||||
|
||||
def _conan_cmd() -> list[str]:
|
||||
"""Return the command prefix for invoking Conan.
|
||||
|
||||
Searches for the ``conan`` executable using several strategies so the
|
||||
script works regardless of how Python/Conan were installed.
|
||||
|
||||
**Windows coverage**:
|
||||
|
||||
- Windows Store Python (``pip install --user``) — ``nt_user`` sysconfig
|
||||
- python.org installer (global or per-user) — interpreter scripts dir
|
||||
- python.org / Chocolatey ``pip install --user`` — ``%APPDATA%\\Python``
|
||||
- Chocolatey (global) — typically on PATH or interpreter scripts dir
|
||||
|
||||
**macOS coverage**:
|
||||
|
||||
- Homebrew Python — on PATH (``/opt/homebrew/bin`` or ``/usr/local/bin``)
|
||||
- System Python ``pip install --user`` — ``~/Library/Python/X.Y/bin``
|
||||
- pyenv / conda / virtualenv — on PATH when activated, scripts dir otherwise
|
||||
|
||||
**Linux coverage**:
|
||||
|
||||
- apt / dnf system Python — on PATH (``/usr/bin`` or ``/usr/local/bin``)
|
||||
- ``pip install --user`` — ``~/.local/bin``
|
||||
- pyenv / conda / virtualenv — on PATH when activated, scripts dir otherwise
|
||||
|
||||
Strategy order:
|
||||
|
||||
1. ``shutil.which`` — anything already on PATH.
|
||||
2. Python-adjacent Scripts/bin dirs — interpreter-level, user-level,
|
||||
``%APPDATA%\\Python`` (Windows), ``~/Library/Python`` (macOS),
|
||||
``~/.local/bin`` (Linux), and site-packages siblings.
|
||||
3. ``sys.executable -m conans.conan`` — package importable but no script.
|
||||
|
||||
Raises ``FileNotFoundError`` with install instructions if nothing works.
|
||||
"""
|
||||
import shutil
|
||||
import sysconfig
|
||||
import site
|
||||
|
||||
# 1. Already on PATH (system install, brew, pyenv, conda, activated venv,
|
||||
# choco with PATH, apt/dnf)
|
||||
if shutil.which("conan"):
|
||||
return ["conan"]
|
||||
|
||||
# 2. Search Scripts/bin dirs adjacent to the running interpreter
|
||||
exe_name = "conan.exe" if IS_WINDOWS else "conan"
|
||||
bin_dir = "Scripts" if IS_WINDOWS else "bin"
|
||||
search_dirs: list[str] = []
|
||||
|
||||
# Interpreter-level scripts (venv/Scripts, C:\PythonXXX\Scripts,
|
||||
# C:\Users\X\AppData\Local\Programs\Python\PythonXXX\Scripts,
|
||||
# /usr/local/bin, /opt/homebrew/bin)
|
||||
interp_scripts = sysconfig.get_path("scripts")
|
||||
if interp_scripts:
|
||||
search_dirs.append(interp_scripts)
|
||||
|
||||
# User-level scripts via sysconfig (Windows Store Python, ~/.local/bin)
|
||||
scheme = "nt_user" if IS_WINDOWS else "posix_user"
|
||||
try:
|
||||
user_scripts = sysconfig.get_path("scripts", scheme)
|
||||
if user_scripts:
|
||||
search_dirs.append(user_scripts)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if IS_WINDOWS:
|
||||
# python.org / Chocolatey "pip install --user" puts scripts under
|
||||
# %APPDATA%\Python\PythonXYZ\Scripts — not covered by nt_user when
|
||||
# running from Windows Store Python
|
||||
appdata = os.environ.get("APPDATA", "")
|
||||
if appdata:
|
||||
ver = f"Python{sys.version_info.major}{sys.version_info.minor}"
|
||||
search_dirs.append(os.path.join(appdata, "Python", ver, "Scripts"))
|
||||
# Also check unversioned (older pip layouts)
|
||||
search_dirs.append(os.path.join(appdata, "Python", "Scripts"))
|
||||
else:
|
||||
# macOS system Python: ~/Library/Python/X.Y/bin
|
||||
if platform.system() == "Darwin":
|
||||
ver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
search_dirs.append(
|
||||
os.path.expanduser(f"~/Library/Python/{ver}/bin")
|
||||
)
|
||||
# Linux/macOS: ~/.local/bin (pip install --user default)
|
||||
search_dirs.append(os.path.expanduser("~/.local/bin"))
|
||||
|
||||
# Site-packages sibling dirs (covers additional layouts)
|
||||
try:
|
||||
for sp in site.getsitepackages():
|
||||
search_dirs.append(os.path.join(os.path.dirname(sp), bin_dir))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
for d in dict.fromkeys(search_dirs): # dedupe, preserve order
|
||||
candidate = os.path.join(d, exe_name)
|
||||
if os.path.isfile(candidate):
|
||||
return [candidate]
|
||||
|
||||
# 3. Module invocation (conan is importable but no script on disk)
|
||||
try:
|
||||
from importlib.metadata import distribution
|
||||
distribution("conan") # raises PackageNotFoundError if absent
|
||||
return [sys.executable, "-m", "conans.conan"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise FileNotFoundError(
|
||||
"Could not find Conan. Install it with: pip install conan"
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_GENERATOR = "ninja-msvc" if IS_WINDOWS else "ninja"
|
||||
GENERATOR_DEFAULT_DIR = {
|
||||
"vs": "build",
|
||||
@@ -196,8 +308,9 @@ def _has_cmake_cache(build_dir: str) -> bool:
|
||||
|
||||
def dependencies(args: argparse.Namespace) -> None:
|
||||
"""Run Conan profile detection and install dependencies with C++20."""
|
||||
cmd_detect = ["conan", "profile", "detect", "-f"]
|
||||
cmd_install = ["conan", "install", ".", "-of", "build-ninja", "-b", "missing",
|
||||
conan = _conan_cmd()
|
||||
cmd_detect = [*conan, "profile", "detect", "-f"]
|
||||
cmd_install = [*conan, "install", ".", "-of", "build-ninja", "-b", "missing",
|
||||
"-s", "compiler.cppstd=20"]
|
||||
conan_install_args = _strip_leading_double_dash(args.conan_install_args)
|
||||
if conan_install_args:
|
||||
@@ -225,7 +338,14 @@ def generate(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def _fix_cmake_user_presets() -> None:
|
||||
"""Ensure CMakeUserPresets.json only includes existing preset files."""
|
||||
"""Ensure CMakeUserPresets.json only includes existing, non-conflicting preset files.
|
||||
|
||||
Conan regenerates preset files when the build layout changes (e.g.
|
||||
``build-ninja/build/generators/`` vs ``build-ninja/build/Release/generators/``).
|
||||
If the old file still exists, CMake fails with "Duplicate preset". This
|
||||
function removes missing includes *and* detects preset name collisions,
|
||||
keeping only the newest file when duplicates are found.
|
||||
"""
|
||||
import json as json_mod
|
||||
presets_path = Path("CMakeUserPresets.json")
|
||||
if not presets_path.exists():
|
||||
@@ -233,11 +353,39 @@ def _fix_cmake_user_presets() -> None:
|
||||
try:
|
||||
data = json_mod.loads(presets_path.read_text())
|
||||
includes = data.get("include", [])
|
||||
|
||||
# Drop missing files
|
||||
valid = [p for p in includes if Path(p).exists()]
|
||||
if len(valid) != len(includes):
|
||||
data["include"] = valid
|
||||
|
||||
# Detect and resolve duplicate preset names across included files
|
||||
seen_names: dict[str, str] = {} # preset_name -> include_path
|
||||
duplicates: set[str] = set()
|
||||
for inc_path in valid:
|
||||
try:
|
||||
inc_data = json_mod.loads(Path(inc_path).read_text())
|
||||
except (json_mod.JSONDecodeError, OSError):
|
||||
continue
|
||||
for key in ("configurePresets", "buildPresets", "testPresets"):
|
||||
for preset in inc_data.get(key, []):
|
||||
name = preset.get("name", "")
|
||||
if name in seen_names and seen_names[name] != inc_path:
|
||||
# Keep the newer file, drop the older one
|
||||
older = seen_names[name]
|
||||
newer = inc_path
|
||||
if Path(older).stat().st_mtime > Path(newer).stat().st_mtime:
|
||||
older, newer = newer, older
|
||||
duplicates.add(older)
|
||||
seen_names[name] = newer
|
||||
else:
|
||||
seen_names[name] = inc_path
|
||||
|
||||
deduped = [p for p in valid if p not in duplicates]
|
||||
|
||||
if deduped != includes:
|
||||
data["include"] = deduped
|
||||
presets_path.write_text(json_mod.dumps(data, indent=4) + "\n")
|
||||
print(f" Fixed CMakeUserPresets.json: kept {len(valid)}/{len(includes)} includes")
|
||||
removed = len(includes) - len(deduped)
|
||||
print(f" Fixed CMakeUserPresets.json: removed {removed} stale/duplicate include(s)")
|
||||
except (json_mod.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user