Could there be a future plugin to integrate Rive (already supported on Defold 2D Game Engine)
Thatās the only thing missing for my company, that keeps us from using phaser
really or just kidding?
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
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);```
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
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)
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
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 Nice to see how you guys are doing workarounds for now though.