Phaser - Need Rive (animator) support

Could there be a future plugin to integrate Rive (already supported on Defold 2D Game Engine)

4 Likes

Thatā€™s the only thing missing for my company, that keeps us from using phaser

1 Like

really or just kidding? :thinking:

sadly no. When I joined they went with rive and konva. Throwing in a phaser layer here and there, but itā€™s really a pain. Now that I think about itā€¦ one could render the rive offscreen and somehow copy every texture/frame into phaser somehow. but I will also need to map the clicksā€¦ahrg

1 Like


ok, that did not take long!

quick protoype:

this.canvas = document.createElement('canvas');

    this.riveInstance = new rive.Rive({
      // Load a local riv `clean_the_car.riv` or upload your own!
      src: hamster,
      // Be sure to specify the correct state machine (or animation) name
      stateMachines: 'State Machine 1', // Name of the State Machine to play
      canvas: this.canvas,
      layout: layout, // This is optional. Provides additional layout control.
      autoplay: true,
      onLoad: () => {
        // Prevent a blurry canvas by using the device pixel ratio
        this.riveInstance.resizeDrawingSurfaceToCanvas();
      },
    });
    document.body.appendChild(this.canvas);
    this.canvas.width = 200; // Set your desired width
    this.canvas.height = 200; // Set your desired height

    setTimeout(() => {
      // Add the canvas to the Phaser game
      this.texture = this.textures.addCanvas('canvasTexture', this.canvas);

      // Now you can use 'canvasTexture' as any other Phaser texture
      let image = this.add.image(0, 0, 'canvasTexture').setOrigin(0, 0);
      const barrel = image.preFX.addBarrel(1);
      this.add.tween({
        duration: Phaser.Math.Between(400, 500),
        repeatDelay: Phaser.Math.Between(100, 200),
        targets: barrel,
        ease: 'Sine.easeInOut',
        amount: 0.786,
        yoyo: true,
        repeat: -1,
      });
      // Enable input on the image
      image.setInteractive();
      // Get the inputs via the name of the state machine
      const inputs = this.riveInstance.stateMachineInputs('State Machine 1');
      // Add a pointer down event
      image.on('pointerdown', (pointer) => {
        if (!this.riveInstance) return;

        // Get the local y-coordinate of the pointer relative to the image
        let localY = pointer.y - image.y;

        // Calculate the height of each third of the image
        let thirdHeight = image.height / 3;

        let animationName;
        // Check which third was clicked
        if (localY < thirdHeight) {
          console.log('First third clicked');
          animationName = inputs.find((i) => i.name === hamsterEvents.TOUCH_1);
        } else if (localY < 2 * thirdHeight) {
          console.log('Second third clicked');
          animationName = inputs.find((i) => i.name === hamsterEvents.TOUCH_2);
        } else {
          console.log('Third third clicked');
          animationName = inputs.find((i) => i.name === hamsterEvents.TOUCH_3);
        }

        animationName.fire();
      });
    }, 1000);```
3 Likes

And just draw the new frame over it

 update() {
    if (this.texture) {
      const ctx = this.texture.context;
      // Draw the external canvas onto the texture
      ctx.drawImage(this.canvas, 0, 0, this.texture.width, this.texture.height);
      this.texture.refresh();
    }
  }

Any performance hints welcome. Not sure how expensive this is

2 Likes

So I came up with this low level thing now.

  • there is now only ONE request animation frame, leaving the browser more room to breathe between renderings and garbage collections
  • there are no more ā€œin betweenā€ renderings from Rive anymore. so no CPU cycles wasted
  • rive instances now only use CPU during Phaser renders

experimenting hereā€¦

example usage:

createHamster() {
    console.log('create hamster');
    const { width, height } = this.viewport;
    console.log(width, height);
    const hamsterImage = this.add.image(width / 2, height / 4, 'hamster');
    hamsterImage.preFX.addGlow(0xff0000, 5, 0);
    //const shine = hamsterImage.preFX.addShine(0.5, 0, 5, false);
    hamsterImage.preFX.addShine(2, 0.5, 2);
    //shine.reveal = false;
  }

  createLowLevelRive() {
    //
    console.log('create');

    this.rll = new RiveHelperLowLevel(
      'hamster',
      this.textures,
      hamsterRiv,
      'State Machine 1',
      () => {
        console.log('clllback');
        this.createHamster();
      },
      0,
      0,
      400,
      400
    );
  }
// rive wasm, maybe...: https://unpkg.com/@rive-app/canvas-lite@2.19.4/rive.wasm

import RiveCanvas from '@rive-app/canvas-advanced-lite';

// TODO: wasm loading can be done simpler.. see link
// https://codesandbox.io/p/sandbox/rive-volume-knob-draft-xykywk?file=%2Fsrc%2Findex.js%3A2%2C8-2%2C18
// https://codesandbox.io/p/sandbox/rive-canvas-advanced-api-centaur-example-exh2os?file=%2Fsrc%2Findex.ts%3A165%2C3-187%2C4
// https://rive.app/community/doc/low-level-api-usage/doctAfBY6v3P
export default class RiveHelperLowLevel {
  rive;
  renderer;
  isReadyForPhaser = false;
  inputs = [];
  stateMachineName;
  riveFileUrl;

  /**
   * Takes a rive and renders it to a texture.
   * Tells you about it in the callback you passed.
   * The texture you passed acts as a normal texture after the callback.
   *
   * @param {string} key
   * @param {Phaser.Textures.TextureManager} phaserTextures
   * @param {string} riveFileUrl
   * @param {string} stateMachineName
   * @param {Function} initCompletedCallback
   * @param {Number} x
   * @param {Number} y
   * @param {Number} width
   * @param {Number} height
   */
  constructor(
    key,
    phaserTextures,
    riveFileUrl,
    stateMachineName,
    initCompletedCallback = () => {},
    // x,y kinda only needed for debug, as we are doing the positioning in phaser
    x = 0,
    y = 0,
    width = 300,
    height = 300
  ) {
    this.key = key;
    this.debug = false;
    this.phaserTextures = phaserTextures;
    this.stateMachineName = stateMachineName;
    this.riveFileUrl = riveFileUrl;
    this.initCompledtedCallback = initCompletedCallback;
    const loadWasm = async () => {
      this.rive = await RiveCanvas({
        locateFile: () =>
          'https://unpkg.com/@rive-app/canvas-lite@2.19.4/rive.wasm',
      });
      this.createRenderer(x, y, width, height);
    };
    loadWasm();
  }

  createRenderer(x, y, width, height) {
    this.canvas = document.createElement('canvas');
    this.canvas.width = width;
    this.canvas.height = height;

    if (this.debug) {
      // just check if we are riving :-)
      this.canvas.id = 'test-canvas';
      this.canvas.classList.add('absolute');
      this.canvas.style.top = `${y}px`;
      this.canvas.style.left = `${x}px`;
      document.body.appendChild(this.canvas);
    }

    this.renderer = this.rive.makeRenderer(this.canvas);

    this.loadRiveFile();
  }

  loadRiveFile = async () => {
    const bytes = await (
      await fetch(new Request(this.riveFileUrl))
    ).arrayBuffer();
    // import File as a named import from the Rive dependency
    const file = await this.rive.load(new Uint8Array(bytes));
    this.file = file;

    // https://github.com/rive-app/rive-wasm/blob/0ca3dd888343c5730f9ce96425bdc530755ad160/js/src/rive_advanced.mjs.d.ts#L317
    // this.artboard = file.artboardByName('hamster'); // working
    // this.artboard = file.artboardByIndex(0); // working
    this.artboard = file.defaultArtboard(); // working

    this.stateMachine = new this.rive.StateMachineInstance(
      this.artboard.stateMachineByName(this.stateMachineName),
      this.artboard
    );

    // This low level portion of the API requires iterating the inputs
    // to find the one you want.
    for (let i = 0, l = this.stateMachine.inputCount(); i < l; i++) {
      const input = this.stateMachine.input(i);
      // save em' for later
      this.inputs.push(input);

      switch (input.name) {
        case 'dirt':
          // example state machine trigger
          // let dirtInput = input.asTrigger();
          // to fire event:
          // dirtInput.fire()
          break;
        default:
          break;
      }
    }

    this.texture = this.phaserTextures.addCanvas(this.key, this.canvas);

    this.isReadyForPhaser = true;
    this.initCompledtedCallback && this.initCompledtedCallback();
  };

  getInputs() {
    return this.inputs;
  }

  renderLoop(timeMs) {
    const elapsedTimeMs = timeMs;
    const elapsedTimeSec = elapsedTimeMs / 1000;

    this.renderer.clear();
    // if you just need an animation without state machine: DIY
    this.stateMachine.advance(elapsedTimeSec);
    this.artboard.advance(elapsedTimeSec);

    this.renderer.save();
    this.renderer.align(
      // THINK: pass those in, hey or nay?
      this.rive.Fit.cover,
      this.rive.Alignment.center,
      {
        minX: 0,
        minY: 0,
        maxX: this.canvas.width,
        maxY: this.canvas.height,
      },
      this.artboard.bounds
    );
    this.artboard.draw(this.renderer);
    this.renderer.restore();
    // Optionally make the below call if using WebGL // FAKE NEWS: THIS IS NEEDED ALWAYS!!!!!!
    this.renderer.flush();
    // tell rive the render is done
    this.rive.resolveAnimationFrame();
  }

  update(time) {
    if (this.isReadyForPhaser) {
      this.renderLoop(time);
      // update texture for phaser
      if (this.texture) {
        const ctx = this.texture.context;
        ctx.drawImage(this.canvas, 0, 0);
        this.texture.refresh();
      }
    }
  }

  destroy(killCanvas = true) {
    // make sure we clean up all of this stuff, see docs
    // https://rive.app/community/doc/low-level-api-usage/doctAfBY6v3P#cleaning-up-instances
    this.renderer.delete();
    this.file.delete();
    this.artboard.delete();
    this.stateMachine.delete();
    if (killCanvas) {
      // Remove the canvas from the DOM (potentially you could pass in a canvas and paint everything on the same one, but that's for a different day..)
      if (this.debug) this.canvas.remove();
      // gc takeover
      this.canvas = null;
    }
  }
}

using 3 rives in one page (Mac M1 pro)

4 Likes

@RedRoosterMobile

Amazing work! Our team recently came across Rive. I was curious to see if Phaser 3 supports it yet.
Thanks a lot for sharing this

2 Likes

Iā€™m working on an open source game project, itā€™s a squad based TBS ā†’ GitHub - FreezingMoon/AncientBeast: The Turn Based Strategy Game/eSport. Master your beasts! šŸŗ
I was considering going with Blender 3d and our spritesheet rendering plug-in GitHub - FreezingMoon/Spritify: Add-on that converts rendered frames into a sprite sheet once the render is completed, so that you can have a nice animation for games, apps or websites
but this approach has quite a few downsides, too many to list. Weā€™re already using Phaser (the CE version for now, but will migrate), and Iā€™m considering Rive too for unit animations and such.
Itā€™s kinda mind-blowing how the devs havenā€™t been integrating nicely with Rive already, but hopefully it will happen :bear: Nice to see how you guys are doing workarounds for now though.