OneJS

repository·main·Indexed 43 days ago

https://github.com/singtaa/onejs

A JavaScript runtime for Unity UI Toolkit that allows developers to build UIs using React 19 and TypeScript, rendered natively through Unity's GPU-accelerated UI system without a browser or webview. Version 3.4.3 includes a native QuickJS engine with C# interop, a JSRunner for project management, a TypeGenerator for C# to TypeScript declarations, and tools for inline TSX execution via JSPad.

Tokens
29.4K
Snippets
55
Records
119
Agent score
56%

What's inside OneJS

  1. Overview of OneJS

    main

    OneJS is a framework for building Unity UIs using React 19 and TypeScript, rendered natively through Unity's UI Toolkit. It operates without a browser or webview, using a purpose-built QuickJS core.

    Key features include:

    • React 19 + TypeScript with esbuild bundling.
    • Hot reload support in both Edit mode and Play mode.
    • Styling options: USS, CSS Modules, and a built-in Tailwind-style JIT generator.
    • Full C# interop: Access C# types directly from JS, including zero-alloc fast paths for per-frame data.
    • Cross-platform: Runs via QuickJS on Desktop, Mobile (iOS/Android), and via the browser's JIT engine on WebGL.
  2. Platform Support for OneJS Runtime

    main

    OneJS Runtime provides QuickJS integration for Unity across multiple platforms with varying engine implementations:

    • Editor/Standalone: Uses native QuickJS via quickjs_unity.dylib, .dll, or .so.
    • iOS: Uses native QuickJS, statically linked as __Internal.
    • WebGL: Uses browser-based JavaScript via OneJSWebGL.jslib, which supports JIT execution.

    For specific implementation details regarding WebGL, refer to ../Plugins/WebGL/OVERVIEW.md.

  3. Access C# types via the CS Proxy

    main

    The OneJS runtime provides a CS global object that allows you to access any C# type directly from JavaScript. This enables seamless interop with Unity and custom C# classes.

    Generic Types

    To create generic types (like List<T> or Dictionary<K, V>), call the type as a function, passing the type arguments as parameters.

    Indexer Support

    Numeric indexers (e.g., List<T> or arrays) can be accessed using standard bracket notation [].

    Note: String-based indexers (like Dictionary<string, T>) do not support bracket notation in the proxy and require explicit method calls: get_Item(key) and set_Item(key, value).

    // Accessing standard Unity types
    CS.UnityEngine.Debug.Log("Hello");
    var go = new CS.UnityEngine.GameObject("MyObject");
    
    // Creating generic types
    var IntList = CS.System.Collections.Generic.List(CS.System.Int32);
    var list = new IntList();
    list.Add(42);
    
    var Dict = CS.System.Collections.Generic.Dictionary(CS.System.String, CS.System.Int32);
    var dict = new Dict();
    // String indexers require explicit method calls
    dict.set_Item("key", 100);
    
    // Using numeric indexers
    var first = list[0];
    list[0] = 100;
  4. Choose between JSRunner and JSPad

    main

    OneJS provides two different MonoBehaviour approaches for running JavaScript in Unity. Choosing the right one depends on whether you are building a production application or prototyping.

    JSRunner (Production Framework)

    Use JSRunner for complete React/JavaScript applications intended for production (desktop, mobile, web, or console). It is designed for professional workflows.

    • Workflow: Uses external files and a full IDE workflow.
    • Collaboration: Files are version-controllable (Git/SVN).
    • Iteration: Supports automatic live reload on file save.
    • Management: You manage npm and the build process (e.g., via package.json).
    • Output: Produces an app.js.txt in the instance folder.

    JSPad (Rapid Prototyping Tool)

    Use JSPad for quick experimentation, testing features, or prototyping UI ideas without setting up a full project structure.

    • Workflow: Write TSX directly in an inline editor within the Unity Inspector.
    • Collaboration: Self-contained within the Unity scene.
    • Iteration: Requires manual Build & Run to see changes.
    • Management: The editor handles npm and esbuild automatically.
    • Output: Builds to a temporary directory (Temp/OneJSPad/).
  5. Edit-mode preview in OneJS

    main

    OneJS allows you to preview your UI in the Unity Game view without entering Play mode. This is driven by the JSRunner component using [ExecuteAlways] logic.

    • Requirements: A valid PanelSettings asset and a successfully built entry file.
    • Behavior: The preview runs at a 30Hz tick via EditorApplication.update. Live reload also works in edit mode.
    • API: Check IsEditModePreviewActive to see if the preview is currently running.
    • Note on UIDocument: OneJS uses UIDocument rather than the newer PanelRenderer (Unity 6.5+) because UIDocument supports eager, authoring-time rendering required for this preview mode.
  6. Use Zero-Allocation Interop for hot paths

    main

    The runtime provides two tiers of API for C# interop. While the generic Bind<T>() API is convenient for prototyping, it causes boxing (approx. 80B per call) because it uses generics with value types.

    For per-frame operations (e.g., GPU compute shader calls or high-frequency updates), use the Specialized Bindings (BindGpu*) which use direct primitive types and involve zero allocations.

    Available Specialized Bindings

    MethodSignaturePurpose
    BindGpuSetFloatById(int, int, float) → voidSet shader float by property ID
    BindGpuSetIntById(int, int, int) → voidSet shader int by property ID
    BindGpuSetVectorById(int, int, float, float, float, float) → voidSet shader vector by property ID
    BindGpuSetTextureById(int, int, int, int) → voidSet shader texture by property ID
    BindGpuDispatch(int, int, int, int, int) → voidDispatch compute shader
    BindGpuGetScreenWidth() → intGet screen width
    BindGpuGetScreenHeight() → intGet screen height

    Best Practice: Property ID Caching

    To avoid string allocations during per-frame calls, cache Unity's Shader.PropertyToID() results in a JavaScript Map during initialization.

    // JavaScript side - cache property IDs once at init
    const _propertyIdCache = new Map<string, number>()
    
    function getPropertyId(name: string): number {
        let id = _propertyIdCache.get(name)
        if (id === undefined) {
            id = CS.OneJS.GPU.GPUBridge.PropertyToID(name)  // One-time allocation
            _propertyIdCache.set(name, id)
        }
        return id
    }
    
    // Per-frame calls use cached ID - zero allocations
    const timeId = getPropertyId("_Time")
    setFloatById(shaderHandle, timeId, performance.now() / 1000)
  7. Four Rules for High-Performance OneJS Development

    main

    To maintain performance and portability, follow these four architectural rules when designing systems that cross the JavaScript/C# boundary:

    1. Config crosses once: Describe a system using a single value (e.g., a versioned JSON document) that crosses the boundary once, rather than a sequence of individual setter calls.
    2. Steady state scales with gameplay, not world size: A running system should not cost JavaScript anything unless an event occurs. Cost should grow with the amount of gameplay (e.g., player interaction), not the total number of entities in the world.
    3. Bulk data moves as one buffer: When JavaScript needs many values, use flat typed arrays instead of reading properties one by one to avoid the overhead of multiple crossings and object boxing.
    4. Commands batch into one crossing: Combine multiple operations into a single call (e.g., StyleBridge.ApplyStyles) rather than making one call per property.
  8. How JSRunner Auto-Watch works

    main

    The JSRunnerAutoWatch system manages the lifecycle of file watchers and project readiness automatically during Play mode transitions.

    Play Mode Lifecycle

    1. Exiting Edit Mode: The system ensures PanelSettings are created, scaffolds project files via EnsureProjectSetup(), and prepares the session tracking.
    2. Entering Play Mode: The system finds all active JSRunner components with valid working directories. It automatically runs npm install and npm run build if necessary, then starts the file watcher.
    3. Exiting Play Mode: The system calls NodeWatcherManager.StopAll() to stop all running watchers, ensuring folders are unlocked for moving or renaming in Edit mode.

    Watcher Status Indicators

    • Running: Watcher is active and auto-rebuilding on file changes.
    • Starting...: npm install or watcher startup is in progress.
    • Idle (enter Play Mode to run): Not in Play mode; will start automatically upon entering Play mode.
  9. Use the Seam Pattern for Cross-Host Capabilities

    main

    When implementing a capability that requires different implementations depending on the host (e.g., different input backends or filesystem providers), use the Seam Pattern. Instead of using if statements to branch inside your logic, inject the implementation.

    // Instead of branching inside the caller, inject the backend
    setInputBackend(backend)
  10. How JSPad handles standalone builds

    main

    Even though JSPad uses a temporary directory during development, it supports standalone builds (without Node.js/npm) through automatic serialization.

    How it works

    When you enter Play Mode, a [InitializeOnLoad] handler builds the JSPad instances. The resulting JavaScript bundle and source map are automatically saved into hidden serialized fields on the JSPad component (_builtBundle and _builtSourceMap). The scene is then auto-saved to ensure these bundles are persisted.

    Benefits

    • No Runtime Dependencies: In a standalone build, JSPad loads the serialized bundle directly instead of looking for files in a temp folder.
    • Accurate Error Messages: Because the source map is serialized along with the bundle, stack traces in standalone builds will show original TypeScript line numbers instead of compiled JavaScript locations.
  11. OneJS System Requirements and Architecture

    main

    OneJS allows you to build user interfaces using React 19 and TypeScript that run directly inside Unity via UI Toolkit.

    Core Concepts:

    • No DOM/Webview: OneJS does not use a web browser or a DOM. It uses QuickJS (or the browser's engine on WebGL) to execute JavaScript and renders via Unity's UI Toolkit.
    • Compilation: TSX source files are compiled into a single bundle using esbuild.
    • Hot Reloading: Changes to .tsx files trigger a rebuild and immediate reload in the Unity Game view.

    Requirements:

    • Unity: 6000.3 or newer.
    • Node.js: Version 18 or newer must be installed on your development machine and available on your PATH (required for npm commands used during build/install).
    • Render Pipelines: Supports Built-in, URP, and HDRP.

    Important Limitations:

    • OneJS only renders through UI Toolkit. It is not compatible with uGUI (Canvas) or IMGUI (OnGUI).
  12. Understand the OneJS WebGL Architecture

    main

    In WebGL builds, OneJS bypasses the standard QuickJS WASM interpreter and runs JavaScript directly in the browser's engine (e.g., V8 or SpiderMonkey). This allows for JIT (Just-In-Time) optimization, providing significantly higher performance than the native QuickJS path used in other platforms.

    The Execution Flow:

    1. Unity WASM Module executes C# code (compiled via IL2CPP).
    2. C# Code calls into the browser via [DllImport("__Internal")] targeting OneJSWebGL.jslib.
    3. OneJSWebGL.jslib (an Emscripten library) acts as the bridge, routing calls to the Browser JavaScript engine.
    4. QuickJSBootstrap.js runs within the browser context to manage the OneJS environment.