Problem
Building a game with JGengine currently requires too much engine-architecture work before the developer can focus on the game itself.
The project author has to understand and manually coordinate several separate concepts:
game.config.ts / defineGame
loop.ts lifecycle hooks
- feature flags and subsystem registration
- content, world, input, UI and presentation wiring
- per-frame, per-player and scheduled work
- save, reset, replication and cleanup behavior
This creates friction even though the engine already contains most of the necessary capabilities.
Current flaws
1. Game definition and runtime behavior are split
A game's static resources are declared in one file while initialization, player joining, event handling and ticking live elsewhere.
To understand a game, a developer may need to jump between:
game.config.ts
loop.ts
world.ts
content.ts
setup.ts
main.tsx
The separation is based on JGengine internals rather than how game developers think about their game.
2. Capabilities are activated twice
Games often enable a capability with a feature flag and then manually register its content and event bindings.
For example, quest support may require both:
features: { quest: true }
and later:
ctx.game.quest.register(quests);
ctx.game.quest.bind("entity.died");
Supplying a quest system should be enough to install the quest capability and its infrastructure.
3. The central loop becomes a manual fan-out list
Large games manually call every ticking subsystem:
tickWorldBoss(ctx);
tickMobs(ctx, dt);
tickAuras(ctx);
tickHero(ctx, playerId, dt);
tickPets(ctx, playerId, dt);
tickDelve(ctx, playerId, dt);
tickMail(ctx, playerId);
tickAuction(ctx, playerId);
This makes the game entry point know too much about every subsystem. It also makes timing policy repetitive and inconsistent across games.
4. Alternative abstractions create separate authoring paths
Cartridges reduce wiring for games that match their shape, but become a second public game-definition model. Games that outgrow the abstraction must return to raw defineGame and hand wiring.
That creates a rewrite cliff:
high-level cartridge
↓ game becomes unusual
raw defineGame + manual wiring
There should be one game-authoring path.
5. A giant universal config would lock games into another schema
Replacing the current API with an increasingly large game.config.ts schema would only move the problem. Every unusual game would require new fields, escape hatches or another alternative path.
6. Hidden feature registration is also undesirable
Moving everything into setupWorld(game), setupCombat(game), etc. creates a wall of setup calls and hides lifecycle ordering.
Likewise, scattering invisible update subscriptions across arbitrary files can make runtime order difficult to reason about.
Proposed direction
Keep one public game-authoring model: defineGame.
export const game = defineGame({
name: "My Game",
world,
player: player({
health: 100,
controller: thirdPersonController(),
}),
systems: [
runFlow(),
enemyDirector(),
combat(),
progression(),
],
ui: standardHud(),
});
The definition should be the clear, readable composition root for the game.
Systems should represent meaningful game capabilities—not tiny tick functions.
Good:
systems: [
combat(),
quests(),
crafting(),
dropInCoop(),
]
Too granular:
systems: [
projectileTick(),
cooldownTick(),
auraTick(),
damageTick(),
]
The smaller operations remain internal to combat().
Per-system timing
Each system should declare how it runs. The engine compiles all system schedules into the actual game loop.
Fixed simulation
export const combat = defineSystem({
id: "combat",
tick: {
type: "fixed",
rate: 60,
stage: "combat",
},
update(ctx, dt) {
updateProjectiles(ctx, dt);
updateDamage(ctx, dt);
resolveDeaths(ctx);
},
});
Interval work
export const enemyDirector = defineSystem({
id: "enemy-director",
tick: {
type: "interval",
every: 1,
},
update(ctx) {
updateEncounterPressure(ctx);
spawnEnemies(ctx);
},
});
Frame/presentation work
export const presentation = defineSystem({
id: "presentation",
tick: {
type: "frame",
},
update(ctx, frame) {
updateCamera(ctx, frame);
updateEffects(ctx, frame);
},
});
Event-only systems
export const progression = defineSystem({
id: "progression",
events: {
"enemy.defeated"(ctx, event) {
grantXp(ctx, event.playerId, event.xp);
},
"quest.completed"(ctx, event) {
grantRewards(ctx, event);
},
},
});
A possible timing contract:
type SystemTick =
| { type: "fixed"; rate?: number; stage?: string }
| { type: "frame"; stage?: string }
| { type: "interval"; every: number }
| { type: "manual" };
Ordering
Runtime order must remain deterministic.
The game or engine can define broad stages:
fixed:
input
movement
combat
ai
activities
cleanup
frame:
animation
camera
effects
Systems select a stage and may optionally declare local ordering constraints:
combat({
tick: {
type: "fixed",
stage: "combat",
after: "movement",
before: "death-resolution",
},
});
Most systems should only need a stage. Explicit before/after constraints should be reserved for real dependencies.
System ownership
A system should own its complete engine contract where applicable:
defineSystem({
id: "quests",
create(ctx) {},
start(ctx) {},
update(ctx, dt) {},
events: {},
save: {},
replicate: {},
reset(ctx) {},
dispose(ctx) {},
});
This prevents capabilities from being added to gameplay while accidentally being omitted from saving, replication, reset or cleanup.
Player configuration
player(...) should remain a first-class part of the game definition because the player is a major game concept, but it should avoid excessive nested constructors and engine jargon.
The API should support concise common cases while allowing custom implementations:
player: player({
health: 100,
controller: thirdPersonController(),
})
A custom game can replace or extend any part without changing authoring models.
Non-goals
- Do not create cartridges or genre-specific game-definition formats.
- Do not replace
defineGame with a universal giant schema for every possible genre.
- Do not require a separate
main.tsx, game.ts and loop.ts puzzle just to launch a game.
- Do not expose every low-level tick function in the composition root.
- Do not make runtime ordering accidental or dependent on import order.
- Do not require feature flags when installing a system already proves the capability is used.
Desired outcome
A developer should be able to open one definition and understand the important shape of the game:
export const game = defineGame({
name: "World of ClaudeCraft",
world,
player,
systems: [
combat(),
mobs(),
quests(),
professions(),
economy(),
activities(),
social(),
],
ui,
});
The engine should then:
- build the runtime context
- install each capability once
- validate dependencies and conflicts
- compile fixed/frame/interval/event execution
- preserve deterministic ordering
- run per-player work where declared
- include system-owned save, reset, replication and cleanup behavior
The developer should focus on game behavior and system composition rather than designing the engine assembly process for every project.
Acceptance criteria
Problem
Building a game with JGengine currently requires too much engine-architecture work before the developer can focus on the game itself.
The project author has to understand and manually coordinate several separate concepts:
game.config.ts/defineGameloop.tslifecycle hooksThis creates friction even though the engine already contains most of the necessary capabilities.
Current flaws
1. Game definition and runtime behavior are split
A game's static resources are declared in one file while initialization, player joining, event handling and ticking live elsewhere.
To understand a game, a developer may need to jump between:
The separation is based on JGengine internals rather than how game developers think about their game.
2. Capabilities are activated twice
Games often enable a capability with a feature flag and then manually register its content and event bindings.
For example, quest support may require both:
and later:
Supplying a quest system should be enough to install the quest capability and its infrastructure.
3. The central loop becomes a manual fan-out list
Large games manually call every ticking subsystem:
This makes the game entry point know too much about every subsystem. It also makes timing policy repetitive and inconsistent across games.
4. Alternative abstractions create separate authoring paths
Cartridges reduce wiring for games that match their shape, but become a second public game-definition model. Games that outgrow the abstraction must return to raw
defineGameand hand wiring.That creates a rewrite cliff:
There should be one game-authoring path.
5. A giant universal config would lock games into another schema
Replacing the current API with an increasingly large
game.config.tsschema would only move the problem. Every unusual game would require new fields, escape hatches or another alternative path.6. Hidden feature registration is also undesirable
Moving everything into
setupWorld(game),setupCombat(game), etc. creates a wall of setup calls and hides lifecycle ordering.Likewise, scattering invisible update subscriptions across arbitrary files can make runtime order difficult to reason about.
Proposed direction
Keep one public game-authoring model:
defineGame.The definition should be the clear, readable composition root for the game.
Systems should represent meaningful game capabilities—not tiny tick functions.
Good:
Too granular:
The smaller operations remain internal to
combat().Per-system timing
Each system should declare how it runs. The engine compiles all system schedules into the actual game loop.
Fixed simulation
Interval work
Frame/presentation work
Event-only systems
A possible timing contract:
Ordering
Runtime order must remain deterministic.
The game or engine can define broad stages:
Systems select a stage and may optionally declare local ordering constraints:
Most systems should only need a stage. Explicit
before/afterconstraints should be reserved for real dependencies.System ownership
A system should own its complete engine contract where applicable:
This prevents capabilities from being added to gameplay while accidentally being omitted from saving, replication, reset or cleanup.
Player configuration
player(...)should remain a first-class part of the game definition because the player is a major game concept, but it should avoid excessive nested constructors and engine jargon.The API should support concise common cases while allowing custom implementations:
A custom game can replace or extend any part without changing authoring models.
Non-goals
defineGamewith a universal giant schema for every possible genre.main.tsx,game.tsandloop.tspuzzle just to launch a game.Desired outcome
A developer should be able to open one definition and understand the important shape of the game:
The engine should then:
The developer should focus on game behavior and system composition rather than designing the engine assembly process for every project.
Acceptance criteria
defineGameis the single public game-authoring path.