-
Notifications
You must be signed in to change notification settings - Fork 1
Engine Architecture
LittleEngine uses two core threads: the main thread loops at tickRate, updating objects using delta time (also fixed time slice integration, eg, for physics), and a render thread attempts to run as fast as possible (capped at the display refresh rate via VSYNC and frame-limited to twice that) and interpolates between render states to draw entities on screen, though async rendering can be disabled via GameConfig. Depending on CPU availability, there will be a file logging thread as well, and a number of job workers for gameplay code to delegate tasks without blocking the main thread. As an example of long-running tasks, asset manifests are loaded entirely via the job system's JobCatalog (collection of tasks with an optional main-thread callback); whereas Quads and PSEmitter objects exploit the Jobs::ForEach() API to distribute the updation of thousands of quads / particles among all the available workers within the time of a single render / game frame.
The Engine offers multiple build configurations for debugging / authoring content / public redistribution. It uses an Entity/Component architecture for organising gameplay objects, where either can be subclassed. All Entities and Components are spawned and owned at game-time by GameManager, and destroyed on the active World's deactivation - which can be changed at any time, and any number of Worlds can be added to the Engine before running it. Assets are expected to be cooked into a zip archive and will be loaded in-memory at game-time; Debug andDevelop builds will first attempt to directly load assets from the filesystem as an authoring convenience (you needn't cook assets you are actively working on).
Note: On
DEBUGGINGbuilds,GameConfigwill load.game.conf, which can be used to experiment with default values that will be used forSHIPPINGbuilds
SFMLAPI provides wrapper classes for and implementations of SFML libraries, a Primitive drawable abstract class with a hierarchy, and all supported Assets - Texture, Sound, Font, Text. Music is streamed directly from the filesystem, thus has no Asset derived class for it.
LittleEngine uses multiple Cartesian spaces, all of which have their origins in the centre, and +X right / +Y up:
- Viewport: size matches the window/context, in pixels
-
World: height matches
GameConfig::WorldHeight()on start, width is adjusted to match viewport aspect ratio; this can be changed at game time via the World Camera -
UI: size matches
GameConfig::UISpace()exactly, and remains unchanged throughout a session; if aspect ratio doesn't matchViewport's, the gutters remain black (letterbox) - Overlay: size matches at least one UI space dimension, the other derived from viewport aspect ratio (full-screen and unchanging with world space, useful for FX)
Note: Texture UVs follow the GL spec: origin at top-left, +Y down, normalised to [0, 1].
All spaces and projection APIs are consolidated in Engine/GFX. Note that normalised positions are in the range [-1, 1], with (0, 0) being the centre.
PrimitiveFactory maintains a fixed number of layers in which it populates new Primitives (Primitive::m_layer is constant). The renderer draws each layer sequentially; LayerID::UI and greater are drawn in UI space, while most other layers are drawn in World space. Background, UnderlayFX and OverlayFX are drawn in Overlay space. When DEBUGGING, LayerID::TopWorld is drawn in world space.
Note: If granular space control per-Primitive is desired over such a hard
LayerIDboundary, do let me know! I'll refactor the renderer.
The core rendering model is for each Primitive type to maintain two copies of any state modifiable by gameplay. A particular state variable may itself be a pair, storing two copies of a datatype, for the render loop to interpolate between. The game state is copied into the render state at the end of a game frame via SwapState(), and - if an interpolation state - reconciled at the start of the next, via ReconcileGameState(). Each Primitive issues one draw call; Quads can thus be used for batching sprites, like in ParticleSystem and Tilemap. When debugging while using async rendering, changes between render frames will only be interpolation of those primitives; to view change in game state reflected in the renderer, break before and after a game frame submit.
Primitive class hierarchy and corresponding render states:
Ownership tree:
At a high level, LittleEngine runs two "main" threads - the core game/event loop (LEApp/GameLoop) and the render loop (Engine/Rendering/LERenderer). The game loop steps at a fixed time slice (set in GameConfig) and although the default behaviour is to launch a render thread, that can be disabled via GameConfig too - note that the render framerate will be (at best) equal to tickRate in that case (which can be useful when, eg, debugging an object through its frame lifetime, rather than having to set m_bDEBUG and conditional breakpoints.) PrimitiveFactory is the owner of all Primitives for an LEContext. StartFrame() reconciles all the Primtives and updates the input state machine; once the Step(fdt) integration and Tick(dt) passes complete, SubmitFrame() begins: PrimitiveFactory locks the main buffer, destroys any stale primitives, and swaps their states. This is equivalent to a standard double buffer swap, and hence both gameplay and rendering will be blocked until it is complete. Any interpolated state X is reflected via a SetX(T value, bool bImmediate) API, where passing immediate=true will reset the interpolated state to that value. This is useful when initialising objects, otherwise they will interpolate from their identity states to their final states until the next game frame swap.
Note: In order to avoid locking the primitive collection during game time,
PrimitiveFactorymaintains astandbybuffer which is moved into the main buffer duringSubmitFrame()
Throughout game-time, GameManager is a global object (accessible in game code via g_pGameManager), which is capable of spawning new Entitys and its subclasses, as well as new Components and subclasses, attached to existing entities. Only one World can be active at a time, which will clean up all its assets and spawned GameObjects on deactivation / unloading. The Engine will call World::Tick(Time dt) using delta time, which will cascade to Entity::Tick() and Component::Tick() on all active/enabled game objects. There are several entities and components ready for use in Game/Framework. All entities contain a Transform object, which stores and updates a mutable Matrix3 and provides efficient convenience functions like SetOrientation(), caching local values and delaying computation of the final matrix until explicitly requested.
Note: expect time dilation and warnings if
Tick()consistently takes too long; offload heavy work to jobs.
GameManager also owns an instance of UIManager (game instance accessible via g_pGameManager->UI()), which manages a stack of UIContext objects. Each UIContext can contain numerous UIElements (base UI object) and UIWidgets (navigatable and interactable UIElements), and can also be cached instead of being destroyed, to be reused (to help minimise memory fragmentation). There are several existing widgets and contexts ready for use in Game/Framework/UI.
LERepository (application instance accessible via g_pRepository) is the asset manager and exposes the ability to T* Load<T>(), std::future<T*> LoadAsync<T>() individual assets, as well as ManifestLoader* LoadManifest<T>().
LEShaders is the shader manager namespace and exposes the ability to T* Load<T>() and compile shaders, and to T* GetShader() at game-time.
LEInput (game instance accessible via g_pGameManager->Input()) exposes bool Callback(const LEInput::Frame& frame) for any function to subscribe to. The stack is walked backwards when firing callbacks, and stopped whenever a callback returns true. A forced callback for every game frame can be requested during registration, if desired (eg, for controllers that need to poll mouse/joystick axis positions).
Note:
UIContextexploits returningtruewhich blocks all callbacks registered before it when one is active.
An LEInput::Frame contains APIs to retrieve pressed/held/released keys as well as mouse-pointer/joystick-axis position values. If Settings.ini contains a property value for CUSTOM_INPUT_MAP, that file will be loaded and the property values in it will be used to override default joystick button bindings.
LEPhysics (game instance accessible via g_pGameManager->Physics()) provides APIs to add Circle / AABB colliders, and for them to receive OnHit notifications. CollisionComponent integrates with this system.
LEContext (game instance accessible via g_pGameManager->Context()) is the core OpenGL context that owns and manages instances of LEInput,Viewport, and LERenderer.
WorldStateMachine (instance not directly accessible, owned by GameKernel) creates, owns, and switches between active Worlds.
GameManager is the global gameplay-level reference owner and provider, and owns all Entitys, Components, UIManager, LEPhysics, and (World) Camera.
LEAudio (application instance accessible via g_pAudio) can be used to play SFX and music, and crossfade between two tracks; it also supports Mix volumes for Master, Music, and SFX.
Note: Set the working directory for the application project (
LEApp) as$(ProjectDir)/Runtime, to debug/run from the IDE.
There are a few features that are only compiled for builds with DEBUGGING enabled.
-
Asserts: Calling
Assert(condition, message)will display a WindowsMessageBoxasking whether to assert (will break instead ifMSVCdebugger is attached), ignore, or ignore all asserts. -
Console: Press the backtick (`/~) key to bring down the console. Type
helpfor more. Custom commands can be added throughConsole::Commands. -
Tweakables: Use
TweakX(id, *pTarget)to create a console variable (accessible throughset) and to bind its value totarget. Collider debug shapes, debug profiler, render stats, audio HUD, etc, are toggled/set throughTweakables.Xcan beBool,S32,F32orString. -
RenderStats: FPS, Primitive count, etc. Accessible through console
Tweakable. -
Profiler: Use
PROFILE_START()andPROFILE_STOP()to measure the performance of any part of code within each game frame. The profiler is toggled through a consoleTweakable.

