2D and 3D¶
good is the kernel, and an engine package sits on it: goo2d for 2D,
goo3d for 3D.
flowchart TD
K["<b>good</b> — the kernel<br/>ECS, scenes, tick loop, memory,<br/>input, assets, coroutines, GameView"]
K --- G2["<b>goo2d</b>"]
K --- G3["<b>goo3d</b>"]
Each renderer sits directly on the kernel and supplies its own drawing: goo2d
paints into a Flutter Canvas, goo3d drives a native surface. Neither depends
on the other, and GameView is the same widget either way.
What is shared¶
These come from the kernel, and are written the same way under either engine:
Game,GameState, the two-isolate model, the fixed tickEntityStruct,Entity,Component,DataPointer, archetypesSceneStruct,Scene, loading and unloadingGameSystem,Query,QueryBuilder, system orderingChild/Parentand the hierarchy — parenting is not a 2D idea- Input: actions, bindings, keys, gamepads
- Assets: the registry,
AssetKey,AssetSource,AssetPack, the CLI pipeline - Coroutines and timelines
- Commands, state channels, buffers
GameView, and the whole Flutter bridge- Networking, when you add it
- The
goodCLI —create,generate,assets,build
What each engine supplies¶
A short list, and it is the whole difference:
| Concern | 2D | 3D |
|---|---|---|
| Transform | Transform2D, WorldTransform2D |
Transform3D, WorldTransform3D |
| Rotation | one angle | a quaternion, four columns |
| Camera | Camera with zoom |
Camera3D with fieldOfView, near, far |
| Drawing | Renderable2D, Sprite, GameRenderer2D |
Renderable3D, MeshAsset, MaterialAsset, Light3D |
| Geometry | Collider2D — circle, box, capsule, polygon |
Collider3D — box, sphere, capsule, hull, mesh |
| Physics | goo2d_physics_box2d (Box2D v3) |
goo3d_physics_box3d (Box3D) |
| Renderer | Flutter Canvas, one drawVertices per frame |
Filament, a draw call per material |
| Up axis | +Y up | +Y up, right-handed |
| Game pair | Game2D/GameState2D |
Game3D/GameState3D |
Which one a project uses¶
One CLI scaffolds either, and everything after the scaffolding step is the same.
2D and 3D do not share a renderer¶
The costs are not shared. A 2D renderer that carried 3D's transform maths, its depth handling and its material model would pay for all of it in every sprite batch — and a 3D renderer built as a special case of a sprite renderer would fight the sprite renderer's assumptions forever.
The split also has a second payoff that has nothing to do with 3D: because the
kernel has no renderer in it, a headless dedicated server can depend on
good plus physics plus good_net and never pull in a rendering package at all.
Writing code that works under both¶
Shared code — an inventory system, a state machine, a networking layer — should
live against the kernel, not against goo2d:
import 'package:good/good.dart'; // not goo2d
class InventorySystem extends GameSystem with FixedTickable {
late final Query holders;
@override
void describeQuery(QueryDescriptor descriptor) {
super.describeQuery(descriptor);
holders = descriptor.query().withAll(Inventory).build();
}
}
Anything that names Transform2D or Sprite is 2D code, and that is the line.
A package of shared gameplay logic that depends on good alone works under
either engine unchanged.
Games do not need to do this
A game picks one engine and depends on it, importing one library. The kernel-only discipline is for shared libraries meant to serve both.