Running games in the browser with SwiftWasm
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:
Two proof-of-concept games running on the browser (Links below)
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 ofFoundation
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
- My engine was using
JSONEncoder
/JSONDecoder
. This was pretty easy to move to a separate module - I was using some geometry functions like
cos
andsin
. Thankfully, Apple has developed a Swift Numerics Package andRealModule
contained what I needed at a much smaller footprint
- My engine was using
- Not including
Foundation
also means no access to types likeDate
andData
. 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:
- The architecture breaks down to
State
,Action
andEnvironment
.State
is your entire game state,Actions
are all the events that cause your game’s state to change andEnvironment
effectively represents the world outside of your game. In this engine, the renderer that draws everything to the screen is considered part ofEnvironment
.Environment
would also be where things like networking is managed. - Your
State
andActions
are the same regardless of what platform you are running on, but theEnvironment
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’sState
based on incomingActions
.Reducers
are composable, which means you can selectively choose the parts of game logic you want to bundle together
- 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-specificReducers
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’sSpriteKit
for rendering on Apple platforms andWebRenderingReducers
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:
I added mobile controls but its much easier with a keyboard
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’supdate
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
-
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.
-
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.
-
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.
More progress - new enemy sprites imported and autopilot AI completing the level on my behalf! pic.twitter.com/h7JD3VbhZO
— Kyle Newsome (@kylnew) August 30, 2021