node-raylib

repository·master·Indexed 43 days ago

https://github.com/robloach/node-raylib

Node.js bindings for raylib 5.5, enabling the creation of games and graphical applications using JavaScript or TypeScript. It provides a CLI for executing scripts and supports DRM mode for ARM devices like Raspberry Pi. Version 0.20.0.

Tokens
41.6K
Snippets
70
Records
204
Agent score
53%

What's inside node-raylib

  1. Understand Pointer Accessible structs

    master
    To optimize performance, certain structs in the Node.js bindings include a hidden property that stores the pointer location of the struct in C++ memory. When calling specific API functions, you can pass this pointer directly to reference the struct, which is more efficient than passing the full object.
  2. How type converters work in the raylib generator

    master

    The node-raylib generator uses ToValue and FromValue patterns to bridge C++ raylib types with Node.js (N-API) objects.

    • ToValue: Converts a C++ struct or primitive into a JavaScript-compatible Napi::Value. For example, a Vector2 is converted into a JavaScript object with x and y properties, while primitives like float and int are converted to Napi::Number.
    • FromValue: Extracts C++ types from Napi::CallbackInfo. These functions typically take a reference to an index to track the current position in the argument list as they consume values from the JavaScript call.
    // Example of converting a C++ struct to a JS Object
    Napi::Value ToValue(napi_env env, Vector2 obj) {
      Napi::Object out = Napi::Object::New(env);
      out.Set("x", ToValue(env, obj.x));
      out.Set("y", ToValue(env, obj.y));
      return out;
    }
    
    // Example of extracting a C++ struct from JS arguments
    Vector2 Vector2FromValue(const Napi::CallbackInfo& info, int& index) {
      return (Vector2) {
        FloatFromValue(info, index += 1),
        FloatFromValue(info, index += 1)
      };
    }
  3. How pass-by-reference functions work in node-raylib

    master

    In the native raylib C++ API, many functions use 'pass-by-reference' (pointers) to modify an object directly instead of returning a new one. For example, ImageDrawPixel() modifies the Image struct passed to it.

    Because JavaScript passes arguments by value to native addons, the C++ side modifies a copy of your object, leaving your original JavaScript object unchanged. To solve this, node-raylib uses a pattern where the native addon returns the modified object, and the JavaScript wrapper synchronizes the properties back to your original object.

    Important: When using these specific functions, the JavaScript wrapper handles the synchronization, but you should be aware that the underlying mechanism relies on returning a modified copy and updating the original reference.

  4. Understand argument flattening in node-raylib

    master

    To achieve high performance, node-raylib uses a technique called argument flattening.

    In standard Node.js native addons, passing a JavaScript object to represent a C++ struct (like Texture2D or Color) is extremely expensive because the C++ layer must parse the object properties at runtime.

    To avoid this, node-raylib flattens these structs into a long list of individual primitive arguments (numbers, booleans, etc.) before they reach the C++ layer. This allows the native side to construct the C++ structs directly from the argument list without expensive object lookups, which can result in significantly higher framerates (up to 6x faster in benchmarks).

    How it affects API usage

    When calling the underlying native raylib object directly, you cannot pass objects. Instead, you must pass the individual properties of those objects in the specific order required by the flattened binding.

    However, for ease of use, the library also provides wrapper functions that allow you to use the standard raylib API (passing objects like Texture or Color) by automatically flattening them for you before calling the native layer.

    // Standard raylib API usage (via wrappers):
    raylib.DrawTexture(myTexture, 10, 10, myTextureColor)
    
    // Underlying native call (flattened):
    raylib.DrawTexture(
        myTexture.id, myTexture.width, myTexture.height, myTexture.mipmaps, myTexture.format,
        10, 10,
        myTextureColor.r, myTextureColor.g, myTextureColor.b, myTextureColor.a
    )
  5. Optimize color performance using integers

    master

    In node-raylib, passing colors as 32-bit integers instead of color objects significantly improves performance. This is because reducing the number of arguments passed between JavaScript and C++ reduces the amount of data that must be copied via the node-addon-api.

    To use this optimization, you can encode a color object {r, g, b, a} into a single integer using bitwise operators in JavaScript, and then pass that integer to the Raylib API functions. Raylib's internal GetColor() function can then decode this integer back into a Color struct on the C++ side.

    let c = {r: 255, g: 128, b: 0, a: 255}
    var cr = c.r & 0xFF;
    var cg = c.g & 0xFF;
    var cb = c.b & 0xFF;
    var ca = c.a & 0xFF;
    var rgba = (cr << 24) + (cg << 16) + (cb << 8) + (ca)
  6. Use raylib structs as plain JavaScript objects

    master

    In Node.js, raylib structs (like Vector2 or Camera2D) are represented as plain JavaScript objects. You can create them by defining an object with the appropriate field names and types. This allows you to pass them directly into raylib API functions.

    let myCamera = {
      offset: {x: 0, y: 0},
      target: {x: 50, y: 50},
      rotation: 0,
      zoom: 1
    }
  7. Use DRM mode on ARM devices

    master

    On certain ARM devices like Raspberry Pi, you can use DRM (Direct Rendering Manager) mode instead of rendering to an X11 window. When installing on ARM, node-raylib includes both the standard and DRM native addons. To use DRM mode, import from raylib/drm instead of the standard raylib package.

    // Using ES Modules
    import * as rl from 'raylib/drm/index.js'
    
    // Using CommonJS
    const rl = require('raylib/drm')
  8. Manage automation event recording and playback

    master

    You can record and play back user input sequences using automation events.

    1. Load/Export: Use LoadAutomationEventList(fileName) to load a list from a file or ExportAutomationEventList(list, fileName) to save a list to a text file.
    2. Setup: Before recording, you must set the list to record into using SetAutomationEventList(list) and optionally set the starting frame with SetAutomationEventBaseFrame(frame).
    3. Recording: Call StartAutomationEventRecording() to begin and StopAutomationEventRecording() to end.
    4. Playback: Use PlayAutomationEvent(event) to play a specific AutomationEvent.
    // Example workflow
    const list = LoadAutomationEventList("my_events.txt");
    SetAutomationEventList(list);
    SetAutomationEventBaseFrame(0);
    
    StartAutomationEventRecording();
    // ... perform actions ...
    StopAutomationEventRecording();
    
    ExportAutomationEventList(list, "recorded_actions.txt");
  9. How pointer-accessible structs work in node-raylib

    master

    To optimize performance, certain raylib structs (like Texture) are cached in C++ memory rather than being copied back and forth between JavaScript and C++ for every function call.

    When you load such a struct, the C++ binding allocates space in a memory pool and returns a JavaScript object containing the struct's data and a hidden pointer property. This pointer is a 64-bit integer representing the memory address in C++.

    Key Concepts:

    • Performance: Passing a single integer (the pointer) to C++ functions is significantly faster than copying large data structures.
    • Scope: Only structs with explicit Load() and Unload() functions in the raylib API (e.g., Texture) use this mechanism. Small, transient structs like Color or Rectangle are passed by value (copied) and do not use pointers.
    • Memory Management: Because these structs live in C++ memory, they are not managed by the JavaScript garbage collector. You must manually unload them to prevent memory leaks.
  10. Manage drawing modes and canvas setup

    master

    Raylib uses a state-based approach for drawing. You must wrap drawing operations in specific Begin and End functions to switch between different rendering contexts like 2D, 3D, textures, or shaders.

    Core Drawing Lifecycle

    • BeginDrawing(): Prepares the canvas (framebuffer) to start a new frame of drawing.
    • EndDrawing(): Finalizes the drawing and performs buffer swapping (double buffering) to display the frame on screen.
    • ClearBackground(color): Clears the entire framebuffer with a specific Color.

    Specialized Drawing Modes

    • 2D Mode: Use BeginMode2D(camera) and EndMode2D() to draw using a Camera2D transform.
    • 3D Mode: Use BeginMode3D(camera) and EndMode3D() to draw using a Camera3D transform. EndMode3D() returns the context to default 2D orthographic mode.
    • Render Textures: Use BeginTextureMode(target) and EndTextureMode() to draw onto a RenderTexture instead of the screen.
    • Shaders: Use BeginShaderMode(shader) and EndShaderMode() to apply a Shader to subsequent drawing commands.
    • Blending: Use BeginBlendMode(mode) and EndBlendMode() to change how colors are blended (e.g., additive, multiplied). EndBlendMode() resets to default alpha blending.
    • Scissor Mode: Use BeginScissorMode(x, y, width, height) and EndScissorMode() to restrict drawing to a specific rectangular area of the screen.
  11. How Node Addon API bindings are generated

    master
    The node-raylib project uses node-addon-api to create bindings between Node.js and the raylib C library. To ensure full coverage of the raylib API, the project automates binding creation by parsing the official raylib API JSON file (provided by the raylib repository). This JSON file contains the metadata required to automatically generate the C++ node-addon-api code that maps each raylib function to a usable Node.js function.