Running games in the browser with SwiftWasm

A quick shout-out to Toronto Game Jam 2022 for being the inspiration to push this project forward and help me also find time to blog. It’s my favourite coding event of the year!

Introduction

I’ve been experimenting recently with running some simple games written in Swift in the browser. There’s still a long way to go but results have been promising:

Breakout asteroids

This blog is a summary of progress and learnings so far.

Origin Story

Why Swift?

Over the last several years I’ve been passively developing a SpriteKit game in my free time. While making a game in something like Unity might make more sense if my goal was to finish a game, I’ve been mostly interested in using my language of choice, Swift. It gives me ways to explore the mechanics of Swift that I won’t find as often in my day-to-day.

Why a custom engine?

One of the ways that I’ve been able to explore functional programming is through hobby game development and I’ve been fascinated with the way it might work with an Entity Component System (ECS). My first attempts were mediocre, at best. However, I found new inspiration last year in Point-free’s The Composable Architecture (TCA). So I ended up rewriting my hobby game from the ground up with a brand new ECS inspired by TCA.

One big focus of this new engine was to eliminate use of Apple’s proprietary GameplayKit and isolate the use of SpriteKit library to being a rendering system only. This would open the door to better cross-platform support.

Exploring SwiftWasm

After a highly successful migration to my new engine, the vision for how to support cross-platform became increasingly clear. I was curious to explore the SwiftWasm project and see where it was. It was a pleasant surprise to see there has been some incredible development. Here’s a few thing I found so far:

  • Setup instructions are super easy to follow. The Carton CLI makes creating and developing your Swift web projects really simple. The SwiftWasm team has done an increadible job of making the process to set up and run your code straight forward.
  • The JavascriptKit library makes interacting with the DOM quite intuitive. If you want to “write Javascript in Swift” effectively, you can do this. It’s a bit verbose, and about as “safe” as what writing TypeScript feels like, but it absolutely works!
  • While it is possible to import and use most of the Foundation library, its footprint on your WASM binary is MASSIVE. As I started to explore importing modules I quickly learned that the use of Foundation was preventing me from including more than a small module’s worth of my own code.
  • There were 2 places I needed to rework my code to overcome relying on Foundation
    1. My engine was using JSONEncoder/JSONDecoder. This was pretty easy to move to a separate module
    2. I was using some geometry functions like cos and sin. Thankfully, Apple has developed a Swift Numerics Package and RealModule contained what I needed at a much smaller footprint
  • Not including Foundation also means no access to types like Date and Data. For the timebeing this has not been a requirement for my needs. My general thoughts are that because we are running inside of a browser, the way we interact with dates and data will be different, and require leveraging Javascript Object equivalents for now

Organizing the engine for cross-platform support

I won’t go too deep into the engine’s architecture in this blog post but there’s a few high-level concepts to understand. If you are familiar with Point-free’s TCA this will probably look familiar:

Architecture breakdown

  • The architecture breaks down to State, Action and Environment. State is your entire game state, Actions are all the events that cause your game’s state to change and Environment effectively represents the world outside of your game. In this engine, the renderer that draws everything to the screen is considered part of Environment. Environment would also be where things like networking is managed.
  • Your State and Actions are the same regardless of what platform you are running on, but the Environment is what changes dramatically. How you render game objects will be completely different. How you take in user inputs (a small fraction of all your game actions) will require some straight forward mapping.
  • Reducers are the game logic. Reducers are the things that actually change your game’s State based on incoming Actions.
  • Reducers are composable, which means you can selectively choose the parts of game logic you want to bundle together

Architecture breakdown

  • Therefore, if we write different Reducers for the platform rendering and do a little bit of input mapping, we can keep the rest of our game logic cross-platform. We only bundle in our platform-specific Reducers at the very end in their own isolated executables.
  • What this means, in practice, is that the engine currently has both SpriteKitRenderingReducers which leverage Apple’s SpriteKit for rendering on Apple platforms and WebRenderingReducers which leverage Pixi.js for rendering on web platforms. (In future I could look into writing my own basic rendering libraries, but it’s not a focus at the moment, and I would still need per-platform rendering reducers)

Results and observations

As you can see, it works! You can play the games I showed above here:

Play Wild Breakout here

I added mobile controls but its much easier with a keyboard

Play Asteroids here

This was the first experiment, but Breakout for TOJam2022 helped me push things

Notes

  • I am leveraging the browser’s requestAnimationFrame to run my loop. I feel like I still have a lot to learn about timing. On web but it’s not as elegant or consistent as SpriteKit’s update function and matching the timing between platforms has mostly been trial and error so far.
  • When playing from an iPhone I’ve experienced freezing, but never on my laptop. I haven’t had a chance to deep dive into what is happening and determine if its resource limitations issue or my own code.
  • Debugging on web has been tricky. I found myself swapping to SpriteKit debugging when dealing with runtime errors in order to hit breakpoints and get useful stack traces.

Conclusions

Initial results have been super promising and I only feel encouraged to explore more uses for SwiftWasm in future. Development and deployment was very straight forward. While there might be an atypical bandwidth requirement for using Swift to run your average website, it’s acceptable for an application.

SwiftWasm seems most useful for applications that don’t depend on Foundation. We can’t forget browser limitations; This is not quite like running Swift on Apple or Linux platforms. I’ve learned to question dependence on Foundation in all my code. It comes as an import statement by default in Xcode, but do I really need it? Could other Swift packages out there make themselves more SwiftWasm ready by simply isolating Foundation requirements to a separate module? Apple’s own open source Swift Numerics Library was a great alternative for my needs.

What’s Next

  1. If you think any of this is cool, I suggest sponsoring the SwiftWasm team’s work. It’s incredible how far the project and tooling has come and I think we should show more love and support from the community.

  2. If you are interested in exploring more about my game engine, RedECS, check it out on GitHub. It’s a hobby project for me, progressing at a casual hobby pace, but I’m trying to start versioning my updates. I’m continuing to slowly chip away at achieving parity between Web and SpriteKit. Texture and font rendering are big ones to add soon.

  3. My long term dream is to have my little RPG game running in-browser. I will probably follow my heart on what it takes supports this when prioritizing engine developments.