Adding New Games to the Monorepo
This monorepo supports multiple games. Each game has a frontend plugin, an entry in games.config.json, and (for Unity games) a Unity project folder at the repo root. The example-unity-project/ serves as a reference template.
Checklist
1. Add Entry to games.config.json
Add a new object to the games array at root games.config.json:
{
"id": "my-game",
"type": "unity",
"enabled": true,
"label": "MY GAME",
"pageTitle": "My Game",
"projectPath": "./my-game-project",
"bridgePath": "./frontend/src/my-game/bridge",
"generatedCsPath": "./my-game-project/Assets/Scripts/Bridge/Generated",
"lintExcludes": ["Assets/SomeVendoredPackage/**"],
"iconSrc": "/my-game-project/images/game-icon.png",
"faviconHref": "/my-game-project/images/fav-icon.png"
}
| Field | Required | Notes |
|---|---|---|
id |
Yes | Must match the frontend feature folder name (frontend/src/<id>/) |
type |
Yes | "unity" or "web" |
enabled |
Yes | false hides from selection and excludes from tooling |
label |
Yes | Display name in game selection menu (uppercase) |
pageTitle |
Yes | Browser tab title |
projectPath |
Unity only | Relative path to Unity project root from repo root |
bridgePath |
No | Relative path to game-specific bridge TS schemas (enables TS → C# codegen) |
generatedCsPath |
No | Relative path for generated C# output (defaults to <projectPath>/Assets/Scripts/Bridge/Generated/) |
lintExcludes |
No | Glob patterns for paths to exclude from C# linting (e.g., vendored third-party code) |
iconSrc |
No | Path to icon served from frontend/public/ |
faviconHref |
No | Path to favicon served from frontend/public/ |
2. Create Frontend Feature Directory
Create a feature folder at frontend/src/<id>/ following Feature Sliced Design:
frontend/src/<id>/
├── <id>.plugin.ts
├── ui/
│ └── <id>-game-layer.tsx
└── home/
└── ui/
└── <id>-home-overlay.tsx
3. Create the Game Plugin
The plugin implements GamePlugin from frontend/src/game-selection/models/game-plugin.model.ts:
import type { GamePlugin } from '../game-selection/models/game-plugin.model';
import { MyGameLayer } from './ui/my-game-layer';
export const myGamePlugin: GamePlugin = {
id: 'my-game',
register() {},
unregister() {},
GameLayer: MyGameLayer,
};
idmust match theidingames.config.jsonregister()/unregister()wire up Unity message handlers, analytics providers, etc.GameLayeris the root React component rendered when this game is active
4. Register the Plugin
Add the plugin to frontend/src/game-selection/services/game-plugin.registry.ts:
import { myGamePlugin } from '../../my-game/my-game.plugin';
const GAME_PLUGINS: Record<string, GamePlugin> = {
// ... existing plugins
[myGamePlugin.id]: myGamePlugin,
};
In DEV mode, the registry warns if an enabled game has no matching plugin entry.
5. Add Game Icons
For the game selection menu, place icon assets in:
frontend/public/<projectPath-basename>/images/game-icon.png
frontend/public/<projectPath-basename>/images/fav-icon.png
These paths must match the iconSrc / faviconHref values in games.config.json.
Unity Project Setup
For type: "unity" games, a Unity project folder must exist at the projectPath location.
Required Structure
Use example-unity-project/ as a template:
<project-folder>/
├── Assets/
│ ├── link.xml
│ └── Scripts/
│ └── Runtime/
│ ├── GameEntryPoint.cs
│ └── <ProjectName>.Runtime.asmdef
├── Packages/
│ └── manifest.json
├── ProjectSettings/
│ └── (standard Unity project settings)
└── <project-folder>.sln.DotSettings
.sln.DotSettings (Required)
Every Unity project must have a <project-folder>.sln.DotSettings file at its root. This suppresses the CheckNamespace inspection in JetBrains IDEs (Rider), which is necessary because our namespaces intentionally diverge from folder paths.
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">DO_NOT_SHOW</s:String>
</wpf:ResourceDictionary>
The filename must be <project-folder>.sln.DotSettings (e.g., snake-wars-game.sln.DotSettings).
.sln File
The .sln file is generated by Unity Editor when the project is first opened. The C# linting tooling (pnpm check:unity) requires it to exist — it will throw an error if no .sln is found. Open the project in Unity Editor at least once to generate it.
Shared Packages via manifest.json
All Unity games must reference the shared C# packages via relative file: paths in Packages/manifest.json:
{
"dependencies": {
"com.games.shared": "file:../../shared/cs/com.games.shared",
"com.games.shared.api": "file:../../shared/cs/com.games.shared.api",
"com.games.shared.bridge": "file:../../shared/cs/com.games.shared.bridge",
"com.games.shared.core": "file:../../shared/cs/com.games.shared.core",
"com.games.shared.fluencysdk": "file:../../shared/cs/com.games.shared.fluencysdk",
"com.games.shared.game": "file:../../shared/cs/com.games.shared.game",
"com.vuplex.webview": "file:../../shared/cs/ThirdParty/com.vuplex.webview"
}
}
Assembly Definition (.asmdef)
Create a runtime assembly definition at Assets/Scripts/Runtime/<ProjectName>.Runtime.asmdef referencing at minimum:
{
"name": "<ProjectName>.Runtime",
"references": ["SharedCore.Runtime", "Games.Shared.Game", "Games.Shared.Api"]
}
Game Entry Point
Every Unity game needs a static entry point using [RuntimeInitializeOnLoadMethod] that calls SharedCoreEntryPoint.RegisterServices(...) and connects the communication bridge. See example-unity-project/Assets/Scripts/Runtime/GameEntryPoint.cs as a reference.
React → Unity bridge:
SharedCoreEntryPoint.RegisterServices()automatically creates theReactBridgeManagerGameObject (loaded fromResources/ReactBridgeManagerprefab incom.games.shared.core). This is required for React to send messages to Unity viaunityInstance.SendMessage. Games that already have the prefab in a scene will reuse the existing instance.
link.xml (Required)
Every Unity project must have an Assets/link.xml file to prevent IL2CPP from stripping assemblies that are accessed via reflection at runtime (e.g., Newtonsoft.Json deserialization, shared bridge services). Without it, production WebGL/IL2CPP builds will crash with missing type errors.
Copy the baseline from example-unity-project/Assets/link.xml and add your game-specific assemblies:
<linker>
<assembly fullname="Games.Shared.Api" preserve="all" />
<assembly fullname="Games.Shared.Bridge" preserve="all" />
<assembly fullname="Games.Shared.Game" preserve="all" />
<assembly fullname="SharedCore.Runtime" preserve="all" />
<assembly fullname="FluencySDK.Runtime" preserve="all" />
<!-- Replace with your game's assembly name(s) from .asmdef files -->
<assembly fullname="MyGame.Runtime" preserve="all" />
</linker>
Rules:
- Always preserve the shared baseline assemblies (
Games.Shared.*,SharedCore.Runtime,FluencySDK.Runtime) - Add every game-specific
.asmdefassembly (checkAssets/Scripts/Runtime/*.asmdefandAssets/Scripts/Bridge/*.asmdef) - Add any UnityEngine types your game accesses via reflection or animation
- See the Unity docs on managed code stripping for more details
C# Linting Excludes
If the game includes third-party code (vendored assets, store packages), add glob patterns to the lintExcludes array in the game's games.config.json entry:
{
"id": "my-game",
"lintExcludes": ["Assets/VendoredPackage/**", "Assets/StoreAssets/**"]
}
These are merged with the global base excludes (**/Plugins/**, **/TextMesh Pro/**, **/ThirdParty/**) at lint time.
Game-Specific Bridge Models (TS → C# Codegen)
For game-specific TypeScript models that need C# equivalents (distinct from the shared shared/ts/bridge/ models), the generator picks them up via the bridgePath field in games.config.json.
How It Works
pnpm script generate-bridge-models automatically:
- Reads
games.config.json - For each enabled Unity game with both
projectPathandbridgePathset, checks if thebridgePathdirectory exists - If found, generates C# into
generatedCsPath(or<projectPath>/Assets/Scripts/Bridge/Generated/by default) - Namespace:
PascalCase(<projectPath-basename>).Generated(e.g.,SnakeWarsGame.Generated)
Setup
- Create the bridge directory (e.g.,
frontend/src/<id>/bridge/) - Set
bridgePathingames.config.jsonto point to it (e.g.,"./frontend/src/<id>/bridge") - Add Zod schemas (
.tsfiles) following the same patterns asshared/ts/bridge/ - Run
pnpm script generate-bridge-models— it processes shared models AND all discovered game targets
Convention Summary
| Component | Path |
|---|---|
| TS source schemas | Wherever bridgePath points (typically frontend/src/<game-id>/bridge/) |
| Generated C# output | generatedCsPath or <projectPath>/Assets/Scripts/Bridge/Generated/ |
| C# namespace | PascalCase(<projectPath-basename>).Generated |
Systems That Auto-Discover Games from games.config.json
These systems read games.config.json and operate on all enabled games automatically:
| System | What it does | Filter |
|---|---|---|
pnpm check:unity / fix:unity |
C# linting via JetBrains inspections (uses lintExcludes) |
type === 'unity' and enabled !== false |
pnpm script generate-bridge-models |
TS → C# codegen for game-specific bridge models | type === 'unity', enabled, has projectPath + bridgePath |
pnpm script sync-prettier-ignore |
Updates .prettierignore with Unity project paths |
type === 'unity' with projectPath |
pnpm fix:format / check:format |
Runs sync-prettier-ignore automatically before prettier | (chained) |
game-plugin.registry.ts |
DEV warning for missing plugins | All enabled games |
games.constant.ts |
Frontend game selection menu | All games (filtered by enabled at runtime) |
