just_game_engine 2.0.0-dev.3
just_game_engine: ^2.0.0-dev.3 copied to clipboard
A game engine for Flutter: 2D, with 3D graphics and physics to opt into; ECS, level maps, sprites and animator graphs, timelines, virtual cameras, particles, audio and input.
Just Game Engine #
A game engine for Flutter: 2D, with 3D graphics and physics to opt into. A game is entities, components and systems (ECS) in a World; its levels are JSON scenes loaded at runtime. The engine brings level maps, sprites and animator graphs, timelines, virtual cameras, physics, particles, audio and action-based input, and draws inside an ordinary Flutter widget. It runs on Android, iOS, Windows, macOS, Linux and the web (JavaScript or WebAssembly).
2.0.0 is a breaking release, out first as dev releases, now 2.0.0-dev.3. Coming from 1.x? Read MIGRATION.md.
| 2D | 3D |
|---|---|
![]() |
![]() |
![]() |
![]() |

The levels in these pictures are scenes the editor saves, loaded and run by the engine. They are in the templates: showcase_2d, showcase_2d_night (platformer) and showcase_3d (third person). The night level's particles are added in code, since particle emitters are not saved in scenes.
Install #
dependencies:
just_game_engine: ^2.0.0-dev.3
Needs Flutter 3.47.1 or newer. To build for Linux, also install what the audio plugin links: OpenAL Soft and libsndfile (sudo apt install libopenal-dev libsndfile1-dev on Debian and Ubuntu; players need libopenal1 and libsndfile1).
Quick start #
import 'package:flutter/material.dart';
import 'package:just_game_engine/just_game_engine.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// The engine, with its standard systems (see "Default systems").
final engine = Engine(
config: const EngineConfig(defaultSystems: DefaultSystems.all),
);
await engine.initialize();
// A floor and a ball that falls onto it. y points down, and (0, 0) is
// the middle of the screen until a virtual camera takes over.
engine.world
..createEntityWithComponents([
TransformComponent(position: Vector3(0, 200, 0)),
RectangleComponent(width: 600, height: 24),
// A collider alone is a static body; this one is the drawn rectangle.
Collider2D(shape: ColliderShape2D.matchShape),
], name: 'Floor')
..createEntityWithComponents([
TransformComponent(position: Vector3(0, -150, 0)),
CircleComponent(radius: 24),
RigidBody2D(),
Collider2D(shape: ColliderShape2D.matchShape),
], name: 'Ball');
engine.start();
runApp(MaterialApp(home: GameWidget(engine: engine)));
}
Engine()makes a new engine each time.initialize()builds its subsystems and world; finish it before using the engine. If a stage fails, it undoes what it built and throws anEngineInitException.engine.start()starts the game loop.GameWidgetruns it on Flutter's frame ticker, draws the world, and passes keys and pointers toengine.input.- When you are done,
await engine.dispose(). Disposal is final: to start over, make a newEngine().
example/example.dart goes further: a level from scene JSON, input actions, a virtual camera and a system of the game's own.
Loading a level #
Levels are scene files: assets/scenes/<name>.scene.json, listed in play order in assets/scenes/scenes.json. Declare the folder under assets: in your pubspec.yaml.
Future<void> loadFirstLevel(Engine engine) async {
final world = engine.world;
final names = await SceneLoader.loadManifest(); // assets/scenes/scenes.json
await SceneLoader.loadAsset(world, SceneAssets.pathFor(names.first));
// Load the level's maps and tilesets before the first frame, so nothing
// falls through ground whose collision is still loading.
await LevelMaps.preload(world);
}
- A component in a scene is read through its
ComponentDefinitioninengine.context. The engine's own components are always known. A package's components are known once itsContextExtensionis listed inEngineConfig(extensions: [...]). An unknown component type stops the load with anUnknownComponentTypeExceptionnaming the type and the entity. - The current scene format is 9 (
SceneFormat.current). Older files are migrated in memory as they load (onMigratedreports what changed); a file from a newer engine throwsUnsupportedFormatVersionException. SceneLoader.loadStringandSceneLoader.loadtake JSON text or a decoded map.
Default systems #
EngineConfig.defaultSystems is empty unless you set it, so a world starts with no systems. Each DefaultSystem adds:
DefaultSystem |
Systems |
|---|---|
input |
InputSystem (the player map's actions onto each InputComponent) |
physics |
PhysicsSystem, PhysicsJointBindingSystem |
movement |
MovementSystem |
audio |
AudioSystem |
rendering |
RenderSystem, ParallaxRenderSystem |
sprites |
SpriteLoadSystem, AnimatorSystem, SpriteAnimationSystem, TimelineTriggerSystem, TimelineSystem |
levelMaps |
LevelMapSystem |
mapCollision |
MapCollisionSystem |
cameras |
CameraTargetGroupSystem, CameraTriggerSystem, CameraBrainSystem |
DefaultSystems.all is every one; DefaultSystems.headless leaves out rendering and audio (for a server, a simulation or a test). registerDefaultSystems(world, engine, include: ...) adds them to any world and skips systems it already has, so a game can add its own configured instance first.
Add these yourself when you use them: ParticleSystemECS, EffectSystemECS, PostProcessSystem, HealthSystem, LifetimeSystem, CheckpointSystem, RaycastSystem, DialogueSystem, ViewCullingSystem. Systems run in priority order (SystemPriorities), not in the order they were added.
Features #
World and scenes #
World,Entity,Component,System, with archetype storage and cached queries (world.query).world.eventsis a typedEventBus;world.commandsdefers changes made during an update;EntityPrefabandworld.instantiatestamp out copies.- Parents and children with a live transform hierarchy: a
ParentComponenthangs an entity under another, and the world keeps the parent'sChildrenComponentin step.TransformComponent.positionandscaleareVector3;angleis the 2-D turn. - A
ComponentDefinitiondescribes a component's fields once, for saving, loading and editor inspectors. Each engine keeps its definitions, loaders and registries in itsEngineContext(engine.context). - Signal-based views of the ECS, on
just_signals:ComponentSignal,EntitySignal,WorldSignal,ReactiveSystem. - Prefab files (
*.prefab.json): a scene entry can place one, its changes kept as overrides, entities removed or added, prefabs nested in prefabs.PrefabLibrary.of(engine.context)loads them and makes one at run time (instantiate);Spawner.prefabspawns one.
Level maps #
LevelMapComponent: a map of tile layers at its entity's position, saved in the scene, in its own.map.json, or read from a Tiled.tmx/.tmj.TiledInteropimports and exports Tiled maps and tilesets; its signatures usepackage:just_tiledmodels (TiledMap,Tileset, …), so import that package to call it.- Orthogonal, isometric, staggered and hexagonal maps (
MapGeometry); animated, flipped and rotated tiles; layers that sort among the level's sprites, with parallax, tint and opacity. - Collision from the tiles' shapes (
MapCollisionSystem), withsolidandoneWaybuilt in and a game's own kinds inMapCollisionKinds. - Entities on a map layer (
LayerComponent.mapLayer) draw between that layer's tiles and the next layer's. - Change tiles while the game runs:
LevelMaps.of(world, entity)returns aLevelMapHandle(setTile,setTerrain); collision follows. - Terrain painting with Wang sets (
WangFiller), and a built-in tileset of basic shapes (BuiltInTilesets.basicShapes) to block out a level before there is art.
Sprites and animation #
SpriteComponentfor one image;SpriteAnimationComponentplays clips from an atlas;AnimatorComponentruns anAnimatorGraph, a state machine over clips driven by parameters.- Atlases from TexturePacker (JSON array or hash, multi-page) and Aseprite (
SpriteAtlas,AtlasParser);SpriteBatchdraws many sprites of one atlas in one call. - Shapes that draw themselves:
RectangleComponent,CircleComponent,CapsuleComponent,PolygonComponent,LineComponent, styled withShapePaintStyleandShapeGradient.
Timelines #
- A
TimelineAssetanimates any saved component field with keyframes and curves, and plays sprite clips, camera shots, audio, activation, nested timelines and events. - Play one with
TimelinePlayerComponent; start one when something enters an area withTimelineTriggerComponent.
Cameras #
- A shot is an entity with a
VirtualCameraComponent. The camera brain shows the enabled one with the highest priority and blends between shots. - Components on the same entity shape the shot: framing with dead and soft zones (
CameraFramingComponent), confining to bounds, noise, impulses, target groups, trigger areas, a dolly path, and state-driven, blend-list and mixer cameras. - With no virtual camera in the world,
engine.cameraSystem.mainCamerais yours to move.
import 'package:flutter/painting.dart' show Offset;
import 'package:just_game_engine/just_game_engine.dart';
void addCameras(Engine engine) {
final world = engine.world;
world.createEntityWithComponents([
TransformComponent(),
VirtualCameraComponent(follow: const EntityTarget.tag('player')),
CameraFramingComponent(deadZoneWidth: 0.1, lookaheadTime: 0.25),
CameraImpulseListenerComponent(),
], name: 'MainCamera');
// Blend shots over 0.6 s, and shake the cameras that listen for impulses.
engine.cameraSystem.brain
..defaultBlend = const CameraBlend(duration: 0.6)
..emitImpulse(CameraImpulse(position: Offset.zero, amplitude: 24));
}
Physics #
- The same model as 3-D (
package:just_game_engine/physics_3d.dart), with a2Dsuffix (butPhysicsSystemandBodyType, which keep their 1.x names):RigidBody2Dis how a body moves (static, kinematic or dynamic; mass, friction, bounce, damping, gravity scale, sensor, layer and filter) andCollider2Dits shape (box, circle, capsule, polygon, segment, chain, or match shape: whatever the entity draws; offset, rotation, one-way). A collider alone is a static body.PhysicsSystemstepsengine.physicsand syncs transforms and velocities back. - Push a body from code with
addForce,addImpulse,addTorque,setVelocityand the rest onRigidBody2D; it reportsisGrounded,groundNormaland its velocities after each step. CharacterBody2Dis a character gameplay moves instead (3-D'sCharacterBody3D): set itsmoveVelocity, calljump, and it walks slopes, climbs steps, rides platforms and stands on one-way ones, the same on every platform. It needs no rigid body or collider.- Physics comes from
just_physics_engine: Box2D v3 through FFI where its native library loads, and a pure-Dart engine with the same API on the web or wherever it does not. - Joints (
DistanceJoint2D,FixedJoint2D,SliderJoint2D,WheelJoint2D), queries throughPhysics2DQueries(raycast, circle cast, overlaps, by layer), and ECS events:CollisionEnter2DEvent,CollisionStay2DEvent(opt in withPhysicsSystem.reportContactStay),CollisionExit2DEvent,SensorEnter2DEvent,SensorExit2DEvent. Some joint options wait on the physics engine; see the known limitations.
3-D graphics #
import 'package:flutter/painting.dart' show Offset;
import 'package:just_game_engine/graphics_3d.dart';
import 'package:just_game_engine/just_game_engine.dart';
Future<Engine> start3D() async {
final engine = Engine(config: EngineConfig(graphics3D: Graphics3D()));
await engine.initialize();
await SceneLoader.loadAsset(engine.world, 'assets/scenes/level.scene.json');
// A perspective camera, in metres (+Y is down, so -Y is up).
engine.rendering.camera
..lens = const CameraLens.perspective(near: 0.1, far: 500)
..position = const Offset(0, -3)
..z = -8
..pitch = -0.36;
engine.start();
return engine;
}
EngineConfig(graphics3D: Graphics3D()), frompackage:just_game_engine/graphics_3d.dart, draws 3-D scenes with just_graphics_engine on Flutter GPU:ModelComponent(a.glbor.gltfmodel; itsboundsfill in once it loads),LightComponent(directional, point or spot, shining along the entity's +Z; a scene with no lights gets a default sun),EnvironmentComponent(a gradient sky or an.hdrskybox, ambient light, reflections, fog, exposure),AnimationPlayerComponent(glTF clips with crossfades and blends; animator graphs and timelines play on a model's skeleton) andSocketComponent(pins a child entity to a joint).- Eleven shapes made from their own sizes, no model file:
Cube3D,Sphere3D,Plane3D,Cylinder3D,Capsule3D,Cone3D,Torus3D,Ramp3D,Stairs3D,Arch3DandPolygon3D(basePrimitive3D), each with a material (colour, metallic, roughness, glow, opacity, unlit, a texture tiled per face or per metre). Equal shapes share a mesh and a material, so a hundred equal crates are one draw, andCollider3D(shape: ColliderShape3D.matchShape)gives one the same shape in 3-D physics. - Material files (
*.material.json): one material shared by every shape drawn with it (Primitive3D.material) and laid over a model's own by name (ModelComponent.materials); texture coordinates natural, in metres or from the world; a layered material blends four layers by the vertex colour (a terrain's). - Render providers: a package draws a component of its own — a terrain, a road along a spline, a field of grass — as meshes and instance batches with
RenderProviders.of(engine.context).register(...);just_world_builder_kitis built on them. - Bodies and characters moved by physics are drawn between their last two poses, so they move smoothly at any frame rate.
- Flutter GPU must be switched on in each platform's runner: see just_graphics_engine's README (Getting started).
engine.graphics3Dis the one given: itsrenderer(renderer.statstells what the last frame did),modelsandsceneRenderer(dynamicResolution).Graphics3D(renderScale: 0.75)renders fewer pixels.- A game without a
Graphics3Dis 2-D only: it makes no GPU device and loads no shaders, and a scene holding 3-D components fails to load, saying how to turn 3-D on. Where Flutter GPU can't draw (the web, Android without Vulkan) the game runs anyway: scenes and models load, the 3-D view shows a short note,unsupportedReasonsays why andGraphics3D(onUnsupported: ...)hears it.
3-D physics #
-
EngineConfig(physics3D: Physics3D()), frompackage:just_game_engine/physics_3d.dart:RigidBody3D,Collider3D(box, sphere, capsule, cylinder, convex hull, mesh, height field, or matching a 3-D shape),CharacterBody3Dand six joints, simulated on Jolt Physics, with the same names, fields and events as 2-D (3Dfor2D). -
The app's pubspec builds Jolt:
hooks: user_defines: just_jolt: build: true -
A game without a
Physics3Dis 2-D only: its release build holds no 3-D code and no Jolt. The 3-D world is made with the first 3-D body. Where 3-D cannot run (the web, Jolt not built),initialize()fails with anEngineInitExceptionwhosecauseis aPhysics3DUnsupportedError;Physics3D.isSupportedasks first, so a game for the web and native platforms passesphysics3D: Physics3D.isSupported ? Physics3D() : null.
Particles, effects and post-processing #
ParticleEmitterwith forces, sub-emitters and renderers;ParticleEffectshas presets (explosion, fire, smoke, rain, snow, …). Put one on an entity withParticleEmitterComponentand addParticleSystemECS.- Deterministic, tick-based effects (
MoveEffect,ScaleEffect,FadeEffect,ShakeEffect,SequenceEffect, …) that serialize for networking and rollback; run byEffectSystemECS. - Fragment-shader passes over the frame (
PostProcessPass) and per entity (ShaderComponent). They need Impeller; on the web and on Skia they are skipped and the content draws unshaded. - Parallax backgrounds:
ParallaxComponent, drawn byParallaxRenderSystem.
Audio #
engine.audio,engine.musicandengine.sfx, fromjust_audio_engine: channels, buses, effects, streams and 3-D sound.AudioSourceComponentandAudioSystemplay sound from entities; withis3da source is heard from where it is — fading betweenminDistanceandmaxDistance, panned by side, with doppler and muffling — relative to theAudioListenerComponent(or the camera), the same on every platform.- On the web, audio starts at the first key or pointer press, because browsers keep audio made before a user gesture silent (
EngineConfig.deferAudioUntilInput).
Input #
engine.input is an InputService from just_inputs: named actions bound to keyboard, mouse, gamepads and touch, read the same whatever the device. Actions and bindings live in an input asset (a .input.json file). It also handles local multiplayer (PlayerInputManager), rebinding with saved overrides, and on-screen touch controls.
Future<void> setUpInput(Engine engine) async {
final text = await rootBundle.loadString('assets/input/game.input.json');
engine.input.loadAsset(InputActionAsset.parse(text));
await engine.input.attachGamepads(); // opt-in
}
// In a system of your own: InputSystem has copied the player's actions
// onto the entity's InputComponent, by name.
void control(Entity player) {
final input = player.getComponent<InputComponent>()!;
final velocity = player.getComponent<VelocityComponent>()!.velocity;
velocity.x = input.vector('move').dx * 250; // y points down
if (input.wasPressed('jump')) velocity.y = -600;
}
rootBundle comes from package:flutter/services.dart. The engine re-exports the parts of just_inputs a game uses at run time; the asset-building types (InputActionDef, …) and validation are in package:just_inputs/just_inputs.dart.
Tags, targets and spawners #
- An entity has any number of tags (
TagComponent);world.findTaggedandworld.allTaggedread an index instead of scanning. - An
EntityTargetpoints at an entity by name, by tag or at itself, so a camera or trigger in a level can name a player that is spawned later. - A spawn point (
SpawnComponent) names aSpawnerthe game provides;SpawnPoints.spawnAllcalls the right one for every spawn point. - A project's tags, colours and descriptions live in
assets/project/tags.json(TagLibrary).
void spawnPlayers(World world, Entity Function(World, Entity) makePlayer) {
final spawners = [
Spawner(
id: 'player',
label: 'Player',
tags: const {'player'}, // the tags makePlayer gives it, for editors
spawn: makePlayer,
),
];
SpawnPoints.spawnAll(world, spawners);
final player = world.findTagged('player');
player?.addTag('hero');
}
In a Flutter app #
GameWidgethosts the game: FPS counter (showFPS), ECS debugger (showDebug), developer terminal (showTerminal, commands registered onengine.terminal), and anoverlayfor your own widgets.engine.layers(GameLayers) lets a package draw real Flutter widgets over everyGameWidgetshowing the engine, with focus for text fields and menus. Thejust_ui_editorpackage draws its HUDs and menus this way.package:just_game_engine/widgets.darthas ready-made screen widgets: dialogue boxes, subtitles, localized text, camera zoom controls, on-screen touch controls and rebinding lists. Import it only if you use them.
Also included #
- Localization (
LocalizationManager, set asengine.context.localization), with namespaces, fallbacks, plurals and selects. - Dialogue from Yarn Spinner 2.x scripts (
DialogueManager,DialogueRunner,DialogueSystem) and timed subtitles (SubtitleController). - Assets (
engine.assets, anAssetManager) and storage (engine.cache, aCacheManager); the engine keeps its data in a store of its own, apart from the app's. EngineService: things that start and stop with the engine (EngineConfig.services,engine.service<T>()). Plugins (engine.plugins,EnginePlugin) receive every update, for editors and profilers.engine.performanceStatsand per-subsystem stats; see STATS.md.
Ready for 3-D #
The engine's foundations do not assume 2-D: transforms have three axes and Euler rotations, a scene says whether it is 2d or 3d (SceneMode), cameras can use a perspective projection (CameraProjection.perspective) with frustum culling, and a SceneRenderer draws a scene beneath the 2-D world: Graphics3D draws 3-D through one (above).
Web and WebAssembly #
The library has no dart:io outside one conditional import, so a game builds for the web with JavaScript or WebAssembly. On the web, physics is pure Dart, audio waits for the first input, shaders are skipped (above), gamepads work in JavaScript and WebAssembly builds once a button is pressed (browsers hide pads until then), and 3-D scenes load but are not drawn (Flutter GPU is not on the web; the 3-D view says so).
Upgrading from 1.x #
The biggest changes; MIGRATION.md covers every one.
Engine()makes a new engine each call;Engine.instanceis gone.await engine.initialize()before use;dispose()is async and final.- Standard systems are opt-in:
EngineConfig(defaultSystems: DefaultSystems.all). - Achievements, sign-in, leaderboards, currency, inventory and ads moved to the
just_game_servicespackage. - Registries and loaders belong to each engine (
engine.context), not to the process; packages install themselves withEngineConfig.extensions. - Transforms are 3-D (
Vector3position and scale; the 2-D turn isangle), cameras are virtual cameras, and input isjust_inputsactions (engine.inputis anInputService). - UI widgets moved to
package:just_game_engine/widgets.dart, and the re-exports ofjust_inputs,just_dart,just_memoryandjust_audio_engineare limited to what a game uses through the engine. - Removed: the scene graph (
SceneEditor,Scene,SceneNode), the tween animation system, the networking stub. Renamed:AssetBundletoGameAssetBundle,EasingtoEasingFunction.
Related packages #
- just_game_services (pub.dev): achievements, sign-in, leaderboards, currency, inventory and ads as opt-in
EngineServices. just_ui_editor(not published): in-game screens saved as.ui.json, drawn as Flutter widgets, and their editor.just_platformer_kit(not published): platformer components and systems.just_game_engine_editor(not published): the level editor.
The engine is built on just_physics_engine, just_graphics_engine, just_inputs, just_audio_engine, just_tiled, just_deflate, just_game_engine_formats, just_signals, just_memory, just_storage, just_database and just_dart.
Documentation #
- QUICKSTART.md: a first game, step by step.
- MIGRATION.md: upgrading from 1.x.
- ARCHITECTURE.md: how the engine is put together.
- ESP_ARCHITECTURE.md: a suggested app structure (Engine · Signal · Presentation).
- CHANGELOG.md.
- API map: the public API by area (in the repository, not in the package).
- API reference.
Links #
- Website: https://engine.justunknown.com/
- Examples, written for 1.x: https://examples.engine.justunknown.com and https://github.com/just-unknown-dev/just-game-engine-examples
- Discord: https://discord.gg/VXFxVj4Y
- Issues: https://github.com/just-unknown-dev/just-game-engine/issues
License #
BSD-3-Clause; see LICENSE.




