SimpleHMEngine 0.4.0
SimpleHMEngine
A small, hand-made 2D game engine built on top of SFML.Net. It gives you a game loop, a scene/component tree, input binding, a camera, simple collision, an immediate-mode drawing helper, and a lightweight UI toolkit, so you can spend your time on the game rather than the plumbing.
⚠️ Disclaimer: This is a hobby engine. It is small, opinionated, and not battle-tested. It's good for prototypes, game jams, learning, and small 2D games, but it is not a production-grade engine like Unity, Godot, or MonoGame. Expect rough edges, breaking changes between versions, and missing features. Use it because it's simple and fun, not because it's complete.
Features
- Game loop with a fixed-timestep accumulator (decoupled update / fixed-update / render).
- Scene & component tree: everything is a
Componentwith its own lifecycle and children. - Auto-registration: declare a component as a field on your
Sceneand it's wired up for you. - Input binding: bind keyboard/mouse to
Pressed/Released/Heldactions. - Camera: follows a target with offset, zoom, and frame-rate-independent smoothing.
- Drawing helper: immediate-mode
Draw.Clear/Circle/Rectangle/Triangle/Line/Polyline/ConvexPolygon/Vertices/Text/Sprite, with aColor-to-DrawOptionsshorthand. - Transform stack: translate, scale and rotate a local drawing frame, with anchor modes for box primitives.
- UI toolkit:
UiCanvas,UiButton,UiText,UiSlider,UiProgressBar, layout panels, etc. - Embedded defaults: a built-in font and a fallback texture, so text and sprites work with no asset files.
Installation
dotnet add package SimpleHMEngine
This pulls in SFML.Net. Note that SFML requires its native runtime libraries (csfml-*) to be
present alongside your executable; these ship with the SFML.Net package.
Getting started
1. A window that draws something
using Core.Drawing;
using Core.Engine;
using SFML.Graphics;
using SFML.System;
public class MyGame(uint width, uint height, string title) : UserWindow(width, height, title)
{
protected override void Render()
{
Draw.Clear(Color.Black);
Draw.Circle(new Vector2f(width / 2f, height / 2f), 50, Color.Cyan);
}
}
public static class Program
{
public static void Main() => new MyGame(800, 600, "My Game").Run();
}
Override the lifecycle hooks you need: Start, Update, FixedUpdate, Render, DebugRender, Close.
Every Draw.* call takes an optional DrawOptions. For the common "just a color" case a Color
converts implicitly (as above); for richer styling build it fluently or with an initializer:
Draw.Rectangle(x, y, w, h, DrawOptions.Fill(Color.Blue).WithOutline(Color.White, 2).WithRotation(45));
Draw.Rectangle(x, y, w, h, new DrawOptions { FillColor = Color.Blue, Opacity = 0.5f }); // still works
2. A component with behaviour
Components carry a transform, children, and the same lifecycle hooks. Move things in FixedUpdate
(time-stepped) and draw them in Render.
using Core.Drawing;
using Core.Engine;
using Core.Entity;
using SFML.Graphics;
using SFML.System;
public class Ball : Component
{
private Vector2f _velocity = new(120, 90);
protected override void FixedUpdate()
=> Position += _velocity * GameContext.FixedDeltaTime;
protected override void Render()
=> Draw.Circle(Position, 20, Color.Red);
}
3. A scene
Any Component field on a scene is registered automatically, with no manual wiring.
using Core.Entity;
public class MainScene : Scene
{
// Auto-registered because it's a Component field on the scene.
private readonly Ball _ball = new() { Position = new(100, 100) };
public override void OnStart()
{
// scene setup goes here (input bindings, spawning, etc.)
}
}
Switch to it from your window's Start:
protected override void Start() => SceneManager.SwitchScene<MainScene>();
Switching is destructive: the current scene is torn down and the new one is built fresh. Two field attributes tune what the scene adopts:
[Detached]: skip a component field during auto-registration (for references the scene holds but does not own).[Persistent]: keep one shared instance of a component alive across scene switches, in the style of Unity'sDontDestroyOnLoad. The same instance is reused by every scene that declares a[Persistent]field of its type, so its state survives. Reach it anywhere withSceneManager.GetPersistentComponent<T>().
public class MainScene : Scene
{
[Persistent] private readonly PlayerData _player = new(); // shared across scenes
[Detached] private Hud _externalHud; // not lifecycle-managed here
}
4. Input
using Core.Input;
using SFML.Window;
InputManager.BindAction(Keyboard.Key.Space, ActionType.Pressed, () => /* jump */);
InputManager.BindAction(Keyboard.Key.D, ActionType.Held, () => /* move right */);
InputManager.BindAction(Mouse.Button.Left, ActionType.Pressed, () => /* shoot */);
Bindings are scene-scoped by default: they are cleared on the next scene switch, so each scene
binds its own input in OnStart. For input that must outlive switches (a global quit key, or a
[Persistent] component's controls) use the global tier instead:
InputManager.BindGlobalAction(Keyboard.Key.Escape, ActionType.Pressed, () => /* quit */);
Input callbacks should only mutate state. Never call
Draw.*orGameContext.CurrentWindow.Clearfrom them. Drawing happens once per frame fromRender; drawing from an input handler fights SFML's double buffering and flickers.
5. Text & fonts
Text uses an embedded default font, so it works with zero setup. To use your own font, point the UI
theme (global) or a specific draw call at a .ttf file:
using Core.Drawing.UserInterface;
UiTheme.FontPath = "Resources/myfont.ttf"; // leave null to use the embedded default
// or per draw call:
Draw.Text("Hello", new SFML.System.Vector2f(20, 20),
new TextDrawOptions { FontPath = "Resources/myfont.ttf", CharacterSize = 24 });
6. Sprites
Draw.Sprite draws a textured sprite right away, with no scene or component involved. The texture
comes from SpriteDrawOptions (an explicit Texture or a texture path through the resource cache),
and falls back to a small embedded default texture when none is set. A Texture or a path converts
implicitly, so the short forms below all work:
Draw.Sprite(new Vector2f(100, 100), "Resources/player.png");
Draw.Sprite(new Vector2f(100, 100), myTexture);
// Fluent options: source sub-rectangle, scale, origin, tint, rotation, opacity.
Draw.Sprite(new Vector2f(100, 100),
SpriteDrawOptions.FromPath("Resources/sheet.png")
.WithSource(new IntRect(0, 0, 32, 32))
.WithScale(2f)
.WithRotation(15f)
.WithTint(Color.White));
For a sprite that lives in the scene and moves over time, use the Sprite2D component instead. It
shares the cached texture rather than copying it, so many sprites of the same path cost one GPU
texture.
7. Transforms & anchors
Draw keeps a small transform stack for translating, scaling and rotating a local drawing frame.
It is applied only to Draw's own output through SFML render states, so it never moves the window
view, scenes, or components. The stack resets at the start of every frame, so a missed Pop only
affects the rest of that frame.
// Manual push/pop:
Draw.Push();
Draw.Translate(200, 150);
Draw.Rotate(30);
Draw.Rectangle(0, 0, 40, 40, Color.Cyan);
Draw.Pop();
// Leak-safe scope with fluent calls (restores on dispose, even on exception):
using (Draw.Pushed().Translate(200, 150).Rotate(30).Mode(Anchor.Center))
{
Draw.Rectangle(0, 0, 40, 40, Color.Cyan); // centered on (200, 150) in the rotated frame
}
Draw.DrawMode(Anchor) (or .Mode(...) on a scope) decides which point of a box primitive its
position refers to. It applies to Rectangle, Text and Sprite; the default is TopLeft, and
Center is handy for centering things. Circles, triangles, lines and polylines are defined by their
own points, so the anchor does not apply to them.
8. Resources
ResourceManager<T> loads and caches resources by path. Textures, fonts, sound buffers, sprites,
colour palettes, raw text (string) and raw bytes (byte[]) work out of the box:
var texture = ResourceManager<Texture>.GetResource("Resources/player.png");
var level = ResourceManager<string>.GetResource("Resources/level1.txt");
var blob = ResourceManager<byte[]>.GetResource("Resources/save.bin");
What counts as a resource is open, register your own loader once at startup (e.g. JSON, using the
in-box System.Text.Json):
ResourceManager.RegisterLoader<PlayerConfig>(
path => JsonSerializer.Deserialize<PlayerConfig>(File.ReadAllText(path)));
var config = ResourceManager<PlayerConfig>.GetResource("Resources/player.json");
Other entry points: TryGetResource (no throw on a missing/invalid file), GetResource(key, stream)
and GetEmbeddedResource(name) for non-file sources, Unload(path) and Clear() (both dispose native
handles). GetResource returns a registered fallback on failure — textures and fonts fall back to the
embedded defaults — or throws ResourceLoadException if none is registered. Register your own with
ResourceManager.RegisterFallback<T>(...).
9. Tile maps
TileMap is a Component that draws a grid of tiles cut from one sheet, grouped by layer (lowest
drawn first) and batched into one draw call per layer. Its Position and TileScale are applied
through a render transform, so moving or scaling the map is free.
public class Level : Scene
{
private readonly TileMap _map = new(new Vector2D<int>(16, 16), "Resources/tiles.png")
{
TileScale = new Vector2f(3, 3)
};
public override void OnStart()
{
// Floor on layer 0.
for (var x = 0; x < 20; x++)
for (var y = 0; y < 12; y++)
_map.SetTile(new Tile(new Vector2D<int>(0, 0), x, y));
// A tinted, flipped accent on layer 1, drawn on top.
_map.SetTile(new Tile(new Vector2D<int>(1, 0), 5, 5, layer: 1)
{
Tint = new Color(255, 220, 120, 210),
FlipX = true
});
}
}
Tiles are keyed by cell and layer: SetTile/AddTiles place (and replace), RemoveTile/TryGetTile
query, ClearTiles empties, and SetLayerVisible(layer, visible) toggles a layer. Rebuilds happen
lazily when tiles change, so you never call Rebuild() by hand. Each Tile supports a Tint, per-tile
Scale, and FlipX/FlipY. MapPixelSize reports the drawn extent (handy for centering a camera).
Namespaces at a glance
| Namespace | What's in it |
|---|---|
Core.Engine |
UserWindow, GameContext, Camera |
Core.Entity |
Scene, SceneManager, Component |
Core.Input |
InputManager, ActionType |
Core.Collider |
ColliderBase, BoxCollider, CircleCollider, manager |
Core.Drawing |
Draw, sprites, tilemaps |
Core.Drawing.DrawOption |
DrawOptions, TextDrawOptions, SpriteDrawOptions, Anchor |
Core.Drawing.UserInterface |
UiCanvas and the UI widgets/layout |
Core.Resources |
ResourceManager<T>, embedded resources |
License
No packages depend on SimpleHMEngine.
| Version | Downloads | Last updated |
|---|---|---|
| 0.4.0 | 1 | 06/19/2026 |