A type-safe bridge for bidirectional communication between Unity WebGL/WebGPU and Angular.
This project has two parts:
| Package | Distribution | Contents |
|---|---|---|
ngx-unity |
Angular library (npm) | Reusable viewport component, IUnityInstance type, mock utilities |
| Unity C# scripts | Copy to Assets/ |
Attributes, TypeScript code generators, editor settings |
- Angular → Unity: Call Unity methods from Angular via auto-generated
unity-client.ts - Unity → Angular: Receive Unity events in Angular via auto-generated signals (no RxJS required)
- Typed JSON payloads: Declare a
[Serializable]DTO viaJsonTypeand get a generated TypeScript interface plus automaticJSON.stringify/JSON.parse - Multi-instance routing: Unity → Angular events carry the originating canvas id; observe a single instance via
forInstance(canvasId) - Callback Support: Request-response and event registration patterns between C# and JavaScript
- TSDoc Generation: C# XML documentation automatically appears in generated TypeScript
- Custom Attributes:
[AngularExposed]and[JSLibExport]for clean, declarative setup - Configurable Output: Control where generated files are placed via Editor settings
<ngx-unity-viewport>: Ready-to-use component that loads Unity WebGL/WebGPU with automatic mock fallback
- Unity 2021.2+ (for
makeDynCallcallback support) - Angular 22+ (the library and generated code use
input(),output(),viewChild.required()and signals) - Node 24+ to build and test the example workspace
-
Copy all
.csfiles from this repository into your Unity project (e.g.,Assets/Scripts/UnityAngularBridge/). -
Configure the output paths (optional):
Tools > UnityAngularBridge > SettingsSet whereunity-client.tsandunity-jslib-exported.service.tsare generated. By default,unity-client.tsgoes to your Documents folder. (Upgrading? The client file was renamed fromUnityClient.tstounity-client.ts— delete the old file.) -
Enable callback support (if using callbacks):
Tools > UnityAngularBridge > Enable Callback SupportThis sets the required Emscripten arg (-s ALLOW_TABLE_GROWTH). -
Recompile or click Play in Unity Editor to regenerate all TypeScript files.
-
Copy the generated TypeScript files into your Angular project (e.g.,
src/app/generated/). -
Register
UnityClientin your app config:import { UnityClient } from './generated/unity-client'; export const appConfig: ApplicationConfig = { providers: [UnityClient], };
-
Use the
<ngx-unity-viewport>component to embed Unity:import { NgxUnityViewport, type IUnityInstance } from 'ngx-unity'; @Component({ imports: [NgxUnityViewport], template: ` <ngx-unity-viewport buildPath="unity" height="500px" (instanceReady)="onUnityReady($event)" /> `, }) export class MyComponent { onUnityReady(instance: IUnityInstance): void { // Wire up your bridge service } }
-
Inject
UnityJSLibExportedServicewherever you need Unity events:import { UnityJSLibExportedService } from './generated/unity-jslib-exported.service'; const jsLib = inject(UnityJSLibExportedService); // Access signals directly const selectedObject = jsLib.sendSelectedObject; // Signal<string | null>
Mark Unity methods with [AngularExposed]:
/// <summary>
/// Load an object by its ID. Called from Angular.
/// </summary>
[AngularExposed(gameObjectName: "SceneManager")]
public void LoadObject(string objectId)
{
// Your logic here
}This generates UnityClient.ts with typed methods and TSDoc:
export class UnityClient {
/** Load an object by its ID. Called from Angular. */
public sceneManager_LoadObject(unityInstance: IUnityInstance, objectId: string): void {
unityInstance?.SendMessage("SceneManager", "LoadObject", objectId);
}
}- Only
stringandnumberparameter types are supported (Unity WebGL limitation) - Maximum 1 parameter per method
- The
gameObjectNameidentifies which Unity GameObject receives theSendMessagecall - Optional
Documentationproperty overrides the TSDoc output - For complex objects, use
JsonType(see Typed JSON Payloads)
Note: Generation happens at compile time / Play mode, not at runtime. A default GameObject name is used unless overridden in [AngularExposed].
Recompile or click on 'Play' in Unity editor to trigger AngularExposedExport.cs.
This will automatically generate unity-client.ts to the configured output path.
Files are only rewritten when their content actually changes, so the Angular dev
server is not triggered by every Unity domain reload.
Declare [DllImport("__Internal")] methods with the optional [JSLibExport] attribute:
/// <summary>
/// Sends the selected object ID to Angular.
/// </summary>
[DllImport("__Internal")]
[JSLibExport(Category = "Selection")]
private static extern void SendSelectedObject(string objectId);
/// <summary>
/// Sends a pipe-separated list as a string array.
/// </summary>
[DllImport("__Internal")]
[JSLibExport(IsStringArray = true, Category = "Objects")]
private static extern void SendObjectsList(string objectIds);
/// <summary>
/// Notifies Angular (no data, event-only).
/// </summary>
[DllImport("__Internal")]
[JSLibExport(Category = "Lifecycle")]
private static extern void SendSceneReady();Call from Unity:
#if PLATFORM_WEBGL && !UNITY_EDITOR
SendSelectedObject(objectId);
SendObjectsList(string.Join("|", objectIds));
SendSceneReady();
#endifThis generates an Angular service with pure signals (no RxJS):
@Injectable({ providedIn: "root" })
export class UnityJSLibExportedService {
/** Sends the selected object ID to Angular. */
readonly sendSelectedObject: Signal<string | null>;
/** Sends a pipe-separated list as a string array. */
readonly sendObjectsList: Signal<string[]>;
/** Notifies Angular (no data, event-only). Increments on each event. */
readonly sendSceneReady: Signal<number>;
}| Property | Type | Default | Description |
|---|---|---|---|
IsStringArray |
bool |
false |
Split pipe-delimited string into string[] |
JsonType |
Type |
null |
[Serializable] DTO type — payload is JSON.parsed into a typed signal |
Category |
string |
"" |
Organize methods (for documentation) |
Documentation |
string |
"" |
Override TSDoc (falls back to XML docs) |
IsCallbackRegistration |
bool |
false |
Mark as callback registration point |
Note:
IsStringArrayvalues are pipe-delimited, so individual values must not contain the|character. UseJsonTypewhen values can contain arbitrary text.
Recompile or click on 'Play' in Unity editor to trigger JSLibExport.cs.
This will automatically generate:
BrowserInteractions.jslib— placed inAssets/Plugins/unity-jslib-exported.service.ts— placed at the configured output path
Both directions support complex objects via JSON. Declare a [Serializable] DTO
class with the JsonType attribute property; the generators emit a matching
TypeScript interface and handle serialization automatically.
The C# method keeps a single string parameter and deserializes it itself
(SendMessage can only carry strings); the generated TypeScript wrapper is fully typed:
[Serializable]
public class SpawnRequest
{
public string objectId;
public float x;
public float y;
public float z;
public string colorHex;
}
[AngularExposed(gameObjectName: "SceneManager", JsonType = typeof(SpawnRequest))]
public void SpawnFromJson(string request)
{
SpawnRequest spawnRequest = JsonUtility.FromJson<SpawnRequest>(request);
// Your logic here
}Generated TypeScript:
export interface SpawnRequest {
objectId: string;
x: number;
y: number;
z: number;
colorHex: string;
}
public sceneManager_SpawnFromJson(unityInstance: IUnityInstance, request: SpawnRequest): void {
unityInstance?.SendMessage("SceneManager", "SpawnFromJson", JSON.stringify(request));
}Pass JsonUtility.ToJson(obj) on the C# side; Angular receives a typed signal:
[Serializable]
public class SceneState
{
public string selectedObjectId;
public int objectCount;
public bool visible;
}
[DllImport("__Internal")]
[JSLibExport(JsonType = typeof(SceneState), Category = "State")]
private static extern void SendSceneState(string json);
// Usage:
#if PLATFORM_WEBGL && !UNITY_EDITOR
SendSceneState(JsonUtility.ToJson(state));
#endifGenerated Angular service:
export interface SceneState {
selectedObjectId: string;
objectCount: number;
visible: boolean;
}
readonly sendSceneState: Signal<SceneState | null>;Mirroring JsonUtility's rules, DTOs may contain public instance fields of:
string, int, long, float, double, bool, arrays / List<T> of those,
and nested [Serializable] classes. Properties, dictionaries, and polymorphism
are not supported. Malformed JSON from Unity is logged to the console and the
signal keeps its previous value.
Based on jmschrack.dev/posts/UnityWebGL.
Unity sends a request to Angular with a callback. Angular processes and responds:
Unity (C#):
[DllImport("__Internal")]
[JSLibExport(Category = "Data")]
private static extern void RequestDataFromWeb(string query, Action<string> onResult);
[MonoPInvokeCallback(typeof(Action<string>))]
private static void OnDataReceived(string data)
{
Debug.Log($"Received: {data}");
}
// Usage:
#if PLATFORM_WEBGL && !UNITY_EDITOR
RequestDataFromWeb("my-query", OnDataReceived);
#endifAngular (TypeScript):
const jsLib = inject(UnityJSLibExportedService);
// Register a handler that responds to Unity's requests
jsLib.registerRequestDataFromWebHandler((query, respond) => {
const result = processQuery(query);
respond(result); // Sends result back to C# callback
});Unity registers a callback that Angular can invoke later:
Unity (C#):
[DllImport("__Internal")]
[JSLibExport(IsCallbackRegistration = true, Category = "Navigation")]
private static extern void RegisterOnNavigationChanged(Action<string> handler);
[MonoPInvokeCallback(typeof(Action<string>))]
private static void OnNavigationChanged(string route)
{
Debug.Log($"Navigation: {route}");
}
// Register once on start:
#if PLATFORM_WEBGL && !UNITY_EDITOR
RegisterOnNavigationChanged(OnNavigationChanged);
#endifAngular (TypeScript):
const jsLib = inject(UnityJSLibExportedService);
// Later, notify Unity of a navigation change:
jsLib.notifyOnNavigationChanged("/new-route");- Callback methods must be
staticand marked with[MonoPInvokeCallback] - For registration callbacks, enable Emscripten support via
Tools > UnityAngularBridge > Enable Callback Support - Callbacks support
Action(void) andAction<string>(string parameter)
Open Tools > UnityAngularBridge > Settings to configure:
| Setting | Default | Description |
|---|---|---|
| unity-client.ts path | MyDocuments | Where the Angular-to-Unity client is generated |
| Service .ts path | Assets/Plugins | Where the Unity-to-Angular service is generated |
| IUnityInstance import path | ngx-unity |
Module the generated client imports IUnityInstance from; leave empty to emit an inline interface (for consumers not using ngx-unity) |
Paths can be absolute or relative to the Unity project folder.
Generated TypeScript includes JSDoc comments from either:
- C# XML documentation comments (
/// <summary>) — requires XML docs enabled in Unity - Attribute
Documentationproperty (fallback/override)
The ngx-unity Angular library (in example/angular-unity-example/projects/ngx-unity/) provides reusable building blocks:
A drop-in component that handles Unity WebGL/WebGPU loading with automatic mock fallback:
<ngx-unity-viewport
buildPath="unity"
height="400px"
[mockFactory]="myMockFactory"
(instanceReady)="onReady($event)" />| Input | Type | Default | Description |
|---|---|---|---|
buildPath |
string |
'unity' |
Path to Unity WebGL/WebGPU build (relative to public/) |
height |
string |
'400px' |
CSS height of the canvas |
canvasId |
string |
auto-generated | DOM id for the canvas; keys forInstance() routing |
mockFactory |
() => IUnityInstance |
built-in mock | Custom mock factory for development |
fallbackToMock |
boolean |
true |
Fall back to a mock when a real build fails to load; set false to surface the failure instead |
| Output | Type | Description |
|---|---|---|
instanceReady |
IUnityInstance |
Emitted when Unity (or mock) is ready |
instanceCreated |
{ instance, canvasId } |
Like instanceReady, plus the canvas id for per-instance routing |
loadError |
Error |
Emitted when a build was found but failed to load (even when falling back to a mock) |
A missing build always falls back to a mock (intended for development without
a Unity build). fallbackToMock only governs what happens when a build exists
but fails to load; with fallbackToMock=false the loadFailed signal is set and
an error overlay is shown.
A testing utility that creates a basic mock IUnityInstance:
import { createMockUnityInstance } from 'ngx-unity';
const mock = createMockUnityInstance({
onSendMessage: (obj, method, data) => {
// Simulate project-specific Unity responses
},
});Multiple <ngx-unity-viewport> components can coexist on the same page.
Each viewport creates its own <canvas> with a unique DOM ID, and the Unity
loader script is loaded only once even when viewports share the same buildPath.
The generated jslib automatically tags every Unity → Angular call with the
originating canvas id (Module.canvas.id), so the generated service can route
events per instance — no handshake or extra Unity code required:
<!-- Two viewports side by side using the same Unity build -->
<ngx-unity-viewport
buildPath="unity"
height="300px"
(instanceCreated)="onReady($event)" />
<ngx-unity-viewport
buildPath="unity"
height="300px"
(instanceCreated)="onReady($event)" />private readonly jsLib = inject(UnityJSLibExportedService);
onReady(event: { instance: IUnityInstance; canvasId: string }): void {
// Per-instance signals, isolated from the other viewport:
const channel = this.jsLib.forInstance(event.canvasId);
const selected = channel.sendSelectedObject; // Signal<string | null>
// Per-instance callbacks:
channel.registerRequestDataFromWebHandler((query, respond) => respond('...'));
channel.notifyOnNavigationChanged('/route'); // targets only this instance
}Semantics:
- The flat signals on the service (e.g.
jsLib.sendSelectedObject) keep their original behavior: they reflect the most recent event from any instance — single-viewport apps need no changes. forInstance(canvasId)returns a channel with the same signal set, isolated per instance.- Request-response calls prefer a channel handler registered via
forInstance(id).register...Handler, falling back to the flat handler. - The flat
notify...()methods broadcast to all instances; useforInstance(id).notify...()to target one. - Events from a jslib built before this feature carry no canvas id and are
routed to the
"default"channel — regenerate the.jsliband the service together to keep them in sync. - If the canvas has no DOM id (custom templates, OffscreenCanvas), events fall
back to the
"default"channel, matching the old single-instance behavior.
ngx-unity/
├── *.cs ← Unity C# source files
├── README.md
├── example/
│ ├── angular-unity-example/
│ │ ├── projects/
│ │ │ └── ngx-unity/ ← Angular library (publishable to npm)
│ │ │ └── src/lib/
│ │ │ ├── components/ ← NgxUnityViewport
│ │ │ ├── models/ ← IUnityInstance
│ │ │ └── testing/ ← createMockUnityInstance
│ │ └── src/ ← Example app
│ │ └── app/
│ │ ├── generated/ ← Unity-generated TS files
│ │ ├── services/ ← Project-specific bridge
│ │ └── components/ ← Demo UI
│ └── unity-project/ ← Example Unity project
The example workspace contains a vitest suite covering the library component, mock utilities, and the generated bridge code contract:
cd example/angular-unity-example && npm ci && npm run build:ngx-unity && npm testThe CI workflow runs the same build + test sequence on every push and pull request.
- Distribute Unity scripts as a UPM package (git URL)