Weird Engine is a C++20 game engine designed for 2D and 3D Signed Distance Field (SDF) rendering.
- Ray Marching Renderer: Renders 2D and 3D Signed Distance Fields using custom OpenGL ES shaders.
- Physics Engine: Calculates 2D Position-Based Dynamics (PBD) with SDF collision detection.
- Entity Component System (ECS): Manages entities, component storage, and system dispatching.
- Service Architecture: Provides decoupled engine services through a single provider interface.
Install the SDL3 development library for your operating system before building. Refer to the SDL3 Linux README for Linux package names.
Weird Engine provides a project template in examples/empty-project.
Use this template to start building a new game.
- Copy the
examples/empty-projectdirectory to your project location. - Open
CMakeLists.txtin your new project directory. - Configure the engine source location:
- Local Engine (Default): Set
USE_LOCAL_WEIRD_ENGINEtoON. SetWEIRD_ENGINE_LOCAL_PATHto your local engine directory. - Automatic Download: Set
USE_LOCAL_WEIRD_ENGINEtoOFF. CMake automatically downloads Weird Engine from GitHub.
- Local Engine (Default): Set
- Place your header files in
include/and source files insrc/. - Place your game assets in
assets/. - Configure and build the project using CMake:
cmake -B build -S .
cmake --build build- Run the compiled executable from the build directory.
For detailed guides, refer to:
Inherit from one of the scene base classes in include/weird-engine/Scene.h:
Scene2D: Uses 2D ray marching and 2D physics.Scene3D: Uses 3D ray marching.SceneBoth: Combines 2D and 3D ray marching paths.
Register your scene in main() with the SceneManager instance:
#include <weird-engine.h>
using namespace WeirdEngine;
class MyScene : public Scene2D
{
public:
MyScene()
{
addStartSystem(onStartSystem);
}
};
int main(int argc, char* argv[])
{
SceneManager& sceneManager = SceneManager::getInstance();
sceneManager.registerScene<MyScene>("my-scene");
start(sceneManager, {}, {}, {}, argc, argv);
}Entities and physics simulation IDs are strongly-typed 4-byte structures (Entity and SimulationID). They cannot be mistakenly cross-assigned or compared at compile-time and incur zero runtime or memory overhead. The Registry class manages entities and stores components.
Call registry.createEntity() to make a new entity:
Entity entity = registry.createEntity();
if (entity == INVALID_ENTITY)
{
// Capacity (MAX_ENTITIES = 10,000) reached; handled gracefully with error logging
return;
}Call registry.addComponent<T>(entity) to attach a component to an entity:
auto& transform = registry.addComponent<Transform>(entity);
transform.position = vec3(0.0f, 10.0f, 0.0f);
auto& dot = registry.addComponent<Dot>(entity);
dot.materialId = services.materials2D().getHandle("my_mat").id;If you modify a component after creation, mark it dirty if required:
registry.setComponentDirty(transform);Define custom components as plain C++ structures (pure aggregates) with in-class default member initializers (avoid user-declared constructors so C++20 aggregate initialization works):
struct Health
{
int current = 100;
int max = 100;
};The Registry automatically registers new component types when first accessed.
You can also register component types explicitly:
registry.registerComponent<Health>();Store scene variables in a scene state instead of global variables. A scene state is one instance per type, owned by the registry, and lives for the lifetime of the scene. It is runtime data only: scene states are never serialized.
struct State
{
int score = 0;
float timer = 0.0f;
};
void stateInitSystem(Registry& registry, ServiceProvider& services)
{
registry.emplaceState<State>();
}
inline State& getState(Registry& registry)
{
State* state = registry.getState<State>();
WEIRD_ASSERT(state != nullptr, "State is missing: stateInitSystem must run first");
return *state;
}Register stateInitSystem as the first addStartSystem entry so it runs
before the systems that read the state.
Add game logic using the System Dispatcher or legacy callbacks.
Systems are plain free functions or lambdas with this signature:
void system(Registry& registry, ServiceProvider& services);Register systems inside your scene constructor:
MyScene()
{
addStartSystem(onStartSystem);
addUpdateSystem(movementSystem);
addUpdateSystem(combatSystem);
addImGuiRenderSystem(uiSystem);
addEntityCollisionSystem(onCollisionSystem);
addEntityShapeCollisionSystem(onShapeCollisionSystem);
addDestroySystem(onDestroySystem);
}Systems registered to the same stage run sequentially in registration order.
Systems access engine subsystems through the ServiceProvider facade:
services.input(): Read keyboard, mouse, and gamepad inputs.services.physics(): Change gravity, damping, pause state, or run raycasts.services.render(): Control camera, lights, and force shader updates.services.shapes(): Register custom SDFs and add geometric shapes.services.materials2D(): Create, share, and query 2D materials.services.materials3D(): Create, share, and query 3D materials.services.audio(): Play sounds and check friction audio levels.services.tags(): Assign unique string tags to entities and look up entities by tag.services.serialization(): Save or load.weirdscene files and blacklist entities.services.time(): Read frame delta time and total simulation time.services.resources(): Resolve asset paths and file input/output.services.sceneControl(): Trigger scene transitions.
Override virtual methods in Scene to use legacy callbacks:
class MyScene : public Scene2D
{
protected:
void onStart(Registry& registry, ServiceProvider& services) override {}
void onUpdate(Registry& registry, ServiceProvider& services) override {}
void onRender(Registry& registry, ServiceProvider& services, WeirdRenderer::RenderTarget& target) override {}
};Note: Use onRender specifically when you need custom 3D render pipeline operations.
Physics simulation steps run on a dedicated thread. Override these virtual methods to execute logic mid-step:
onPhysicsStep(Simulation2D& simulation)onPhysicsRigidBodyCollision(Simulation2D& simulation, PhysicsCollisionEvent& event)onPhysicsShapeCollision(Simulation2D& simulation, PhysicsShapeCollisionEvent& event)
Physics callbacks receive Simulation2D& only.
Physics callbacks cannot access Registry or ServiceProvider because the main thread owns the ECS.
To associate custom data with physics bodies, derive from BodyUserData:
struct CharacterData : BodyUserData
{
static constexpr int TYPE = 1;
CharacterData() { type = TYPE; }
float jumpStrength = 10.0f;
};
// Hand off ownership to the simulation:
services.physics().setUserData(rb.simulationId, std::make_unique<CharacterData>());
// Query data back in physics callbacks:
if (auto* data = simulation.getUserDataAs<CharacterData>(bodyId))
{
simulation.addImpulseForce(bodyId, vec2(0.0f, data->jumpStrength));
}Weird Engine includes scripts for building and deploying games to Anbernic handhelds running muOS.
Find these scripts in scripts/anbernic/.
- Install Podman on your PC.
- Mount the console SD card over USB using MTP (for example
mtp:/RG35XX-H/SD2).
Run deploy-muos.sh with your project path and MTP destination:
/path/to/weird-engine/scripts/anbernic/deploy-muos.sh . mtp:/RG35XX-H/SD2Pull log files and screenshots from the device:
/path/to/weird-engine/scripts/anbernic/fetch-logs.sh . mtp:/RG35XX-H/SD2Logs are saved to device-logs/ inside your project directory.