Core concepts
The rules, lifecycle and traps every Moksha game needs to know.
Hard rules
- C++20. No exceptions, no RTTI: no
try/catch/throw,dynamic_cast,typeid(the build rejects them). Report failure withbool, null pointers or an error enum. - Never block the frame: no endless loops, no sleeping, no waiting on I/O. Do one frame’s work in
Update/FixedUpdateand return. The engine must not require threads. - Files only through
ME::Vfs(moksha/shared/file_io/vfs.h), with paths relative to a root (Resources,Dlc,UserData). Nofopen,std::ifstreamor absolute paths. - No OS calls (
windows.h, POSIX, Cocoa) in game code; the engine covers the platforms. - Game code lives in the game’s own namespace, never in
ME. Engine headers are included qualified:#include "moksha/client/game/game.h".
Repo shape
<game>/CMakeLists.txt FetchContent the engine, then moksha_add_game(...)
<game>/game_main.cpp MOKSHA_GAME(MyGame::GameMain, "Title")
<game>/game/, <game>/scene/ Game subclass, Scene and SceneUI subclasses
<game>/resources/res_list.json assets to cook, paths relative to this file
<game>/resources/config/settings.ini
moksha_add_game(<Target> SOURCES ... RES_LIST resources/res_list.json [HEADLESS]) builds the executable and
cooks the engine’s resources plus the game’s next to it. HEADLESS adds <Target>_Headless (no window,
renderer, input or audio; same game code). After adding or removing a .cpp, re-run cmake -B build.
On iOS the game is an app bundle: optional DISPLAY_NAME, BUNDLE_ID (default MOKSHA_BUNDLE_ID_PREFIX plus the
target) and ORIENTATION PORTRAIT|LANDSCAPE|ANY fill its Info.plist; other platforms ignore them.
On web the game is a page, with DISPLAY_NAME as its title. It has no sockets, so gameClient never connects and
contentClient fails and carries on. Quit() only logs, since a page can’t close itself. UserData persists in the
browser after each write, per site address; a tab killed right after a write can lose that write.
Lifecycle
CreateGame(viaMOKSHA_GAME) constructs the game before any engine system exists. Do nothing there.Init(ME::Engine&): callGame::Init(engine)first, then create bothsceneanduiSceneand callInit()on each. The engine reads them right afterInitreturns; a null scene crashes. Also set up physics here (engine->physics->SetScene,SetCollisionListener(this)).Start(): everything exists, including the renderer and audio.- Every frame: N ×
FixedUpdate(fixedDt)(60 Hz by default; timers, physics and animation run in the same step) →Update(dt)→ UI → render. Gameplay that must be deterministic goes inFixedUpdate. End(): clear your timers, delete what you created (scene,uiScene, physics scene), then callGame::End().OnPause/OnResumeon minimize (macOS: also while the window is fully covered; mobile: leaving the foreground); on mobile and web the app can die withoutEnd.
What ME::Engine gives you (moksha/client/app/engine.h)
time (frame clock), timers (delayed/repeating Delegate calls), input (keys, mouse), physics (AABB
collision reports), animation, audio, ui, gameClient (UDP to a game server), contentClient (DLC
sync), config (final settings), window (actual size), Quit(), IsHeadless(). Keep the pointer the
base class stores in engine; never put engine systems in globals.
Scenes
ME::Sceneis plain arrays (ME::Span<T>:data+count), not an ECS.Scene::Init()callsCreateResources,BuildLights,BuildCamera, then theBuild*Transforms/Build*Renderersvirtuals. Override only what you use and call the base first: baseCreateResourcesallocates every array.- Arrays have fixed capacities (
moksha/shared/misc/engine_limits.h); you set.count.Span::operator[]does not bounds-check, so check against the capacity yourself. - Drawable on every graphics backend: sprites (
spriteTransforms/spriteRenderers), the two instanced sprite sets, UI sprites and text.meshRenderers(3D) are not drawn yet. ME::SceneUIholds the UI tree (Panel,Label,Image,Button), built inBuildUIElements().
Traps
- Texture slots are fixed.
spriteTexturePaths[i]pairs withtextureAtlasProperties[i]. Plain sprites use their owntextureId/textureAtlasPropsId; instanced set 0 always draws slot 1 and set 1 always slot 2, so fill those slots when you use the sets. ASceneUIneeds the UI atlas at slot 0 and the font at slot 1 (copy the cookbook’s UI recipe). - Instanced sets are sized once, from
.countat startup. Allocate the maximum you will need and park unused instances off-screen. After changing an instance’s transform or atlas index, set its renderer’sbDirty = true, or the change isn’t uploaded. Plain sprites are re-read every frame. - Collision results are yours to delete: in a
CollisionCallbackoverride,delete result(or callGame::CollisionCallback, which does). Physics only reports overlaps; it never moves anything, so update collider bounds yourself (ColliderAABB::UpdateTransform). - Audio ids are array indices into
scene->sfxPaths/scene->musicPaths, loaded once afterInit. Fill them inCreateResources. - Coordinates: world space is Y-up.
spriteCamerais orthographic:orthographicSizeis half the view height in world units, and itsaspectRatiodefaults to 1.33, so set it fromengine->window. UI space is Y-down from the top-left, in pixels. - Debug overlay slots 0-2 are the engine’s (FPS, game name, scene name). Use slots 3-15. The overlay is off in
Release unless
[debug] debugOverlay=1is set, so player-facing text belongs in the game’s own UI. - Textures are cooked: list
textures/x.pnginres_list.json, loadtextures/x.ddsin code. Other files go undercopy. A file missing from the list doesn’t exist at runtime. GetKeyDown= held;GetKeyPressed= went down this frame.
Where to look next
- The Cookbook has a minimal game and copyable snippets for each system.
- The samples repository has
evolution(instanced sprites, procedural map, camera),ui_demo(UI tree, button), andnet_testwithserver(game server, custom verbs, headless bots). - Engine headers each start with a short contract block. Read the header, not the
.cpp.