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.