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!
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.
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.
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.
- While it is possible to import and use most of the
Foundationlibrary, its footprint on your WASM binary is MASSIVE. As I started to explore importing modules I quickly learned that the use of
Foundationwas 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
- My engine was using
JSONDecoder. This was pretty easy to move to a separate module
- I was using some geometry functions like
sin. Thankfully, Apple has developed a Swift Numerics Package and
RealModulecontained what I needed at a much smaller footprint
- My engine was using
- Not including
Foundationalso means no access to types like
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
Stateis your entire game state,
Actionsare all the events that cause your game’s state to change and
Environmenteffectively represents the world outside of your game. In this engine, the renderer that draws everything to the screen is considered part of
Environmentwould also be where things like networking is managed.
Actionsare the same regardless of what platform you are running on, but the
Environmentis 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.
Reducersare the game logic.
Reducersare the things that actually change your game’s
Statebased on incoming
Reducersare composable, which means you can selectively choose the parts of game logic you want to bundle together
- Therefore, if we write different
Reducersfor 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
Reducersat the very end in their own isolated executables.
- What this means, in practice, is that the engine currently has both
SpriteKitRenderingReducerswhich leverage Apple’s
SpriteKitfor rendering on Apple platforms and
WebRenderingReducerswhich 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
- I am leveraging the browser’s
requestAnimationFrameto 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
updatefunction 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.
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.
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