Tauri 2-alpha and Phaser 3

This is another mid level walk through as I build an application stack to learn about various interesting application technologies, in this case I'm continuing to use Tauri and am going to implement Phaser.js in it. Maybe this is a bit more interesting than the SvelteFlowwalk through with Tauri as this might be the perfect segway for those interested in game development but who only have a web/javascript background to write mobile and multi-platform games. That's enough preamble, let's dive in:

~/code on ☁️  (us-east-1)
❯ sh <(curl https://create.tauri.app/sh) --alpha
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 16122  100 16122    0     0   132k      0 --:--:-- --:--:-- --:--:--  133k
info: downloading create-tauri-app
✔ Project name · phasor-integration-demo
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, bun)
✔ Choose your package manager · npm
✔ Choose your UI template · Svelte - (https://svelte.dev/)
✔ Choose your UI flavor · JavaScript
✔ Would you like to setup the project for mobile as well? · yes

I generally add some Pagni's like a data folder and exclude it from git, add test folders to the project root for end to end, and one each in src and src-tauri, customize the .gitignore a bit, run npm install and you should get this initial folder structure for your initial git commit.

phasor-integration-demo on  main [?] via  v21.1.0 on ☁️  (us-east-1)
❯ tree --depth=2
 .
├──  data
│   └──  README.md
├──  index.html
├──  jsconfig.json
├──  node_modules
│   ├── ... # A whole lot of node_modules
├──  NOTES.md
├──  package-lock.json
├──  package.json
├──  public
│   ├──  svelte.svg
│   ├──  tauri.svg
│   └──  vite.svg
├──  README.md
├──  src
│   ├──  App.svelte
│   ├──  lib
│   ├──  main.js
│   ├──  styles.css
│   └──  vite-env.d.ts
├──  src-tauri
│   ├──  build.rs
│   ├──  Cargo.lock
│   ├──  Cargo.toml
│   ├──  icons
│   ├──  src
│   ├──  target
│   └──  tauri.conf.json
├──  svelte.config.js
├──  tests
└──  vite.config.js

I'm also going to add a justfile as I find that sometimes I just type npm run dev out of habit but Tauri needs to be invoked in the run command, and it's just easier to type just run on the CLI. So here is the justfile for that:

run:
    npm run tauri dev

Just double check that you the app boots up, always a bit of a surprise on alpha and beta project branches.

And let's embed Phaser in a Svelte component and make sure we are handling the component lifecycle correctly. I'll be following the Phaser Javascript guide as I'm just trying to keep this as simple as possible, but a lot of games could benefit from strong typing and a more OOP like set of fundamentals.

NPM seems like the way to go for importing Phaser, so a simple:

❯ npm install phaser@3.70.0

added 2 packages, and audited 70 packages in 1s

12 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Then let's create a new component with the Phaser tutorial code and empty out the default App.svelte file and load the component. So our Phaser component looks like this:

<script>
  import { onDestroy } from "svelte";
  import Phaser from "phaser";

  class Demo extends Phaser.Scene {
    preload() {
      this.load.setBaseURL("http://localhost:1420");
      this.load.image("sky", "assets/sky.png");
      this.load.image("logo", "assets/sprites/phaser3-logo.png");
      this.load.image("red", "assets/particles/red.png");
    }

    create() {
      this.add.image(400, 300, "sky");

      const particles = this.add.particles(0, 0, "red", {
        speed: 100,
        scale: { start: 1, end: 0 },
        blendMode: "ADD",
      });

      const logo = this.physics.add.image(400, 100, "logo");

      logo.setVelocity(100, 200);
      logo.setBounce(1, 1);
      logo.setCollideWorldBounds(true);

      particles.startFollow(logo);
    }
  }

  const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    scene: Demo,
    physics: {
      default: "arcade",
      arcade: {
        gravity: { y: 200 },
      },
    },
  };

  const game = new Phaser.Game(config);
  onDestroy(() => game.destroy(true, true));
</script>

<div class="phasor-container">
  <h1>Cybernetic Squirrel Superhero!</h1>
</div>

<style>
  .phasor-container {
    background-color: #000000d6;
    color: #78b202;
    padding: 1em;
  }
</style>

Then we extend the Phaser.Scene class with some constructor details that include our base URL and three images. Importantly you'll need to add the port designation in the setBaseURL() assignment, https://localhost:1420, for the Tauri backend or you won't be able to load the images. Interestingly this will still work with broken images as I demonstrate below.

The important Svelte parts here is adding the onDestroy() component lifecycle call and aligning it with the Phaser game loop lifecycle by making sure the game loop is destroyed when the component is destroyed. I've intentionally left the images as broken references just because I thought that halfway working version was pretty neat, to see that in action update the App.svelte file to include the Phaser component like this:

<script>
  import Phaser from "./Phaser.svelte";
</script>

<main class="container">
  <Phaser />
</main>

<style>
  .container {
    background-color: #000000d6;
    color: #78b202;
    padding: 1em;
  }
</style>

And even with broken images we should get an interesting animated canvas that looks like this:

Although for me locally it runs very smoothly, the low frame rate gif capture should give you an idea of what is going on here. There is a background image, the logo is bouncing around, and the particles are rendered in a swarm full brightness when they come in then shrink and fade as the logo moves away from them.

I'm going to fix the broken images and add some artwork to the scene so you get an idea of what this could look like with just a little tweaking. The fixes are in the inheritance operation on Phaser.Scene, adding the HTTP port in the base URL and changing the background image to a variable so we can call it's methods to position it centered with background.setPosition().

... 
class Demo extends Phaser.Scene {
    preload() {
      this.load.setBaseURL("http://localhost:1420");
      this.load.image("sky", "assets/title-background-large.png");
      this.load.image("logo", "./assets/cybersquirrel-small.png");
      this.load.image("red", "./assets/mechanical-particles-b.png");
    }

    create() {
      const background = this.add.image(0, 0, "sky");
      background.setPosition(
        this.cameras.main.centerX,
        this.cameras.main.centerY,
      );
...

Consider what we have here, besides a half metal squirrel burning like a sparkler bouncing around an AI generated city-scape that just isn't quite right. We have a multi-platform runtime in Tauri, with Rust/Kotlin/Swift on the back end, a web based front end with all the bells and whistles of a modern UI, a game engine that can work with web assets or texture mapped objects and has almost all of the physics, game logic and special effects to handle anything from simple to moderate complexity 2D games.

Best of all the game can be built with the graceful component system of Svelte which should only enhance component re-usability and give us almost unlimited non-Phaser components we can add for other parts of our game that are better suited for non-flashy/non-physics parts of our game. Another benefit is as long as we link up our Phaser lifecycle to the Svelte component lifecycles we have a natural way of limiting the active context so we can keep the resource use low and support lower end phones(if mobile is our target platform). And Svelete/Tauri gives us a huge level of flexibility for storing and passing data between components. As far as getting our platform audience target as wide as we possibly can, we could even easily convert the Tauri backend calls with web API calls (conditionally make local calls or remote REST API calls based on our hostname value) and host this game as a single page app on almost any web hosting service.

To wrap up this walk through, this was a lot easier than I expected, really just drop the library into our app with NPM and do a tiny bit of code to get things working in Tauri and some minor adjustments for the artwork I generated with DALL-E(edited in GIMP). Probably not the app stack you want if you are targeting AAA quality games, but for graphically simple, 2D games I think this could be a very effective stack.

The code for just this walk through is in the v0.1 tag: https://github.com/gatewaynode/tauri_phaserjs_walkthrough/releases/tag/v0.1

Full code and assets on Github: https://github.com/gatewaynode/tauri_phaserjs_walkthrough/tree/main