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,
};
  • id must match the id in games.config.json
  • register() / unregister() wire up Unity message handlers, analytics providers, etc.
  • GameLayer is 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 the ReactBridgeManager GameObject (loaded from Resources/ReactBridgeManager prefab in com.games.shared.core). This is required for React to send messages to Unity via unityInstance.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 .asmdef assembly (check Assets/Scripts/Runtime/*.asmdef and Assets/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:

  1. Reads games.config.json
  2. For each enabled Unity game with both projectPath and bridgePath set, checks if the bridgePath directory exists
  3. If found, generates C# into generatedCsPath (or <projectPath>/Assets/Scripts/Bridge/Generated/ by default)
  4. Namespace: PascalCase(<projectPath-basename>).Generated (e.g., SnakeWarsGame.Generated)

Setup

  1. Create the bridge directory (e.g., frontend/src/<id>/bridge/)
  2. Set bridgePath in games.config.json to point to it (e.g., "./frontend/src/<id>/bridge")
  3. Add Zod schemas (.ts files) following the same patterns as shared/ts/bridge/
  4. 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)