Creating an empty project
Start a canvas without game rules, assets or an FSM.
Now assemble a minimal project: configuration, one scene and its dependencies. Create a folder and install the SDK packages:
mkdir sdk-game
cd sdk-game
npm init -y
npm pkg set type=module
npm pkg set 'scripts.dev=VITE_PLATFORM=web vite' 'scripts.build=VITE_PLATFORM=web vite build' 'scripts.typecheck=tsc --noEmit'
npm install --save-exact @gamedevland/engine
npm install --save-dev --save-exact @gamedevland/vite vite@8.0.16 typescript@5.9.3
mkdir -p game/src
Commands using VITE_PLATFORM=web are shown for macOS/Linux. On Windows, the same variable can be set in a .env file at the project root.
Project files
Configure the build in vite.config.ts. The browser code will live in game, and the build output in dist:
import { createGameViteConfig } from '@gamedevland/vite';
export default ({ mode }: { readonly mode: string }) =>
createGameViteConfig({
mode,
repoRoot: import.meta.dirname,
});
Create game/index.html with a container and entry point. Block Puzzle’s index.html uses the same approach:
<!doctype html>
<html lang="ru">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>SDK Game</title></head>
<body style="margin:0"><div id="app"></div><script type="module" src="/src/main.ts"></script></body>
</html>
Even an empty scene needs a DI definition. There is nothing to register yet, so the methods stay empty. Put the classes in separate files inside game/src.
AppGlobalDi.ts describes the game’s global registrations. Leave them empty for now:
import { GlobalDiDefinition } from '@gamedevland/engine/di';
export class AppGlobalDi extends GlobalDiDefinition {
protected override registerElements(): void {}
protected override registerComponents(): void {}
protected override registerCommands(): void {}
protected override registerServices(): void {}
}
In MainSceneDi.ts, specify the scene name. It has no dependencies yet:
import { SceneDiDefinition } from '@gamedevland/engine/di';
export class MainSceneDi extends SceneDiDefinition {
override getSceneKey(): string { return 'main'; }
protected override registerProviders(): void {}
protected override registerServices(): void {}
}
In AppDi.ts, combine global registrations with scene dependencies:
import { GameDiDefinition } from '@gamedevland/engine/di';
import { AppGlobalDi } from './AppGlobalDi';
import { MainSceneDi } from './MainSceneDi';
export class AppDi extends GameDiDefinition {
protected override createGlobalDefinition(): AppGlobalDi {
return new AppGlobalDi();
}
protected override createSceneDefinitions(): MainSceneDi[] {
return [new MainSceneDi()];
}
}
In main.ts, assemble the configuration and pass it to the engine:
import { Engine } from '@gamedevland/engine';
import { BaseScene } from '@gamedevland/engine/scenes';
import { AppDi } from './AppDi';
import type { GameConfig } from '@gamedevland/engine/core';
const config = {
system: { canvasBackground: '#1d57c8' },
assets: { bundles: [], preload: [] },
scenes: { initial: 'main', map: { main: { key: 'main', useClass: BaseScene } } },
di: new AppDi(),
} satisfies GameConfig;
await Engine.run('#app', config);
Create tsconfig.json at the root:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"lib": ["ES2022", "DOM"],
"types": ["vite/client"]
},
"include": ["game/src/**/*.ts"]
}
Run npm run typecheck, then npm run dev. The expected result is a blue canvas with no errors. preload: [] is explicit: without it, the engine tries to load the default common and layouts bundles, which do not exist here.