Skip to main content

· 8 min read
Kevin Glass

Rune is a platform for multiplayer games, so as you can imagine the networking code is core to what we do.

The initial approach that people take to game networking is to send all of the game state across the network every frame. This doesn’t scale, imagine you’re playing an RTS with 1000 units each - the network simply won’t support sending that much data all the time.

Most network strategies only send the inputs from the client across the network, so in our example: send these 50 units to position X. This works by having all the clients maintain their own game state and applying the received inputs to that.

Games make sure everyone starts from the same point. Then we apply the same changes to all the game states and expect them to stay in synchronization. This is called determinism - if we’re at state X and we apply change Y then we end up at state Z.

This sounds like the sort of thing a computer would be really good at, but unfortunately the JavaScript specification leaves some things open to interpretation. This means that different implementations of the JavaScript runtime have slightly different behaviors. These slight differences can result in significant changes to the output.

When it comes to determinism, details count, especially when you’re dealing with a wide variety of devices and runtimes. If the result of math operation on device A is 0.001 different to device B then the resulting direction and final game state can be massively different. Consider if that value is the angle that you’re flying in a game, and then you go on to fly 10^6 units forward. Your positions are now significantly different.

Here at Rune we have this exact problem, JavaScript isn’t consistent across devices and runtimes. We do of course need all of them to react to game state changes in the exact same way. The bright side, thanks to JavaScript, we’re able to patch the parts that aren’t deterministic.

So what needed patching?

Math Functions

The first place to start is all of those functions on the Math object, and the warning sign is right there on MDN page:

Note: Many Math functions have a precision that's implementation-dependent.

This means that different browsers can give a different result. Even the same JavaScript engine on a different OS or architecture can give different results!

Outside of the constants nearly all the functions need patching to ensure that everyone in a game returns the same result. This simply means that all the results need to be rounded to a common precision, namely Single-precision floating-point format. Luckily there is a Math function that does exactly that.

The functions that needed patching for our use case are:

abs, acos, acosh, asin, asinh, atan, atan2, 
atanh, cbrt, ceil, clz32, cos, cosh, exp, expm1,
floor, hypot, log, log10, log1p, log2, max,
min, pow, round, sign, sin, sinh, sqrt, tan, tanh,
trunc

So, does this break anything? No, not really. Results are still accurate enough for games to function normally. There is one exception to all of this that we’ll see next.

Random Numbers

The second piece of the puzzle is random numbers. One of the things that most developers know is there’s no such thing as random numbers in computers, only pseudo random numbers. So you’d think it’d be perfect for determinism - unfortunately not.

The Math.random() specification is JavaScript is deliberately vague to allow runtime implementers to have freedom to build appropriately. Worse than that the specification doesn’t support passing in a seed so there is no way to start the random number generation in a predictable manner.

First stop, we’ll patch Math.random() with a custom seeded random number generator function. There are many well documented out there in the public domain. Brilliant, that gets us deterministic! I've used mulberry32 many times in the past so it was a pleasant surprise when I joined Rune to find us using the same algorithm.

function randomNumberGeneratorFromHash(hash: number) {
return function () {
let t = (hash += 0x6d2b79f5)
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}

In Rune however we use the predict-rollback networking model, which requires us to be able to rollback time and reapply events to our game state. But what if we generated random numbers? A pure seed isn’t enough anymore because we want to generate the same random numbers we generated back when we first ran that time step of logic.

To solve this problem we have to keep track of the seed independently and allow for rolling back to previous seeds. With that approach we now have a fully deterministic random numbers even with rollbacks! We use the hash of each named update loop and xmur3 to generate the seed:

function hashFromString(str: string) {
for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 3432918353)
h = (h << 13) | (h >>> 19)
}
const seed = () => {
h = Math.imul(h ^ (h >>> 16), 2246822507)
h = Math.imul(h ^ (h >>> 13), 3266489909)
return (h ^= h >>> 16) >>> 0
}
return seed()
}

Sorting and Shuffling

A very common action in games programming is to sort an array. Whether that’s for z-sorting in 2D or line of sight checking. What’s more, shuffling an array for game logic is often done using a combination of Math.random() and sorting. JavaScript of course has array sorting built in but unfortunately the exact details of that array sort vary between implementations - especially in regard to strings.

Again MDN has a little hint:

Due to this implementation inconsistency, you are always advised to make your comparator well-formed by following the five constraints.

There’s a decent chance that a developer will accidentally use a comparator that doesn’t quite end up with the same result on different implementations. To remedy this we can patch the Array.prototype.sort function with a default comparator.

Reading around on the topic, this Stack Overflow was the implementation that seemed most appropriate for us, which ended up looking like this:

const defaultCmp = (x: any, y: any) => {
// INFO: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
// ECMA specification: http://www.ecma-international.org/ecma-262/6.0/#sec-sortcompare

if (x === undefined && y === undefined) return 0

if (x === undefined) return 1

if (y === undefined) return -1

const xString = toString(x)
const yString = toString(y)

if (xString < yString) return -1

if (xString > yString) return 1

return 0
}

For details on how this applied see the next section.

How to Patch

Just a quick note on patching the functions above, in case you haven't done it before. JavaScript is flexible, so much so you can override default functions of the core libraries it self. Since functions are properties of objects they can (in most cases) be overridden using Monkey Patching. So in our example of sorting above we do:

// override the sorting operation on Array 
// to use our deterministic function
globalThis.Array.prototype.sort = arraySort

Where arraySort is a sorting function that makes use of a default comparator.

Since we don't want to change the functionality outside of the game logic, we can also take a copy of the original function and reset it after our code has executed:

const originalSort = globalThis.Array.prototype.sort
globalThis.Array.prototype.sort = arraySort

try {
return gameLogic()
} finally {
globalThis.Array.prototype.sort = originalSort
}

Bonus Content!

The other thing we can do is help developers to recognize when their code might produce non-deterministic results. There are many possible causes of this e.g. access to global scope and using locale related functions. In the JavaScript world eslint is a common tool for applying a set of rules to code as the developer is working and at build time. At Rune we provide an eslint plugin that encapsulates all the common errors we've seen so developers are warned right inside their IDEs when something might cause an issue.

As you can see determinism in JavaScript isn’t straight forward but it is obtainable. We now have games running in perfect synchronization.

If you have comments or want to learn more head over to the Discord.

Subscribe to our newsletter for more game dev blog posts
We'll share your email with Substack
Substack's embed form isn't very pretty, so we made our own. But we need to let you know we'll subscribe you on your behalf. Thanks in advance!

· 2 min read
Amani Albrecht

🛠️ App Improvements

  • Brought the settings and profile screens into the redesign, they now match the app’s beautiful new aesthetic 🎨 Also added your most played games onto your profile.
  • Unveiled the Rune drop to our playerbase with a beautiful explainer banner, screen and video 📹
  • 🌒 Updated both our app icon and share room links to rune!
  • 🧹 Cleaned up code in preparation for v2 avatars—get ready for more customization options soon!
  • Final Rune Transition items, including replacing the llama icon with a friends icon for the number of allowed players badge in games 👥 and updated the “Install Rune” image that pops up inside browser rooms!
  • Added our updated Rune socials and emails to the app 🔗

🪲 Bug Fixes

  • Resolved an issue where one failing effect during app booting after tapping a shareLink could leave the app in limbo.
  • 🔧 Fixed our web rooms to properly handle CommonJS imports

💻 SDK Improvements

  • Updated the ESLint plugin version to 1.0.2 ✨
  • Fixed the layout of the dev UI on iOS mobile, which has been displaying incorrectly
Subscribe to our newsletter for more game dev blog posts
We'll share your email with Substack
Substack's embed form isn't very pretty, so we made our own. But we need to let you know we'll subscribe you on your behalf. Thanks in advance!

· 2 min read
Kevin Glass

Have you ever thought about writing a multiplayer game, but the task seems too daunting? What if networking was already done and you could just focus on a small fun game? Rune makes it easy to build multiplayer games quickly with a huge player base across regional servers.

To celebrate our move to Rune we're going to run a mini game jam. We've loved what everyone has brought to the table over previous jams and really want more of it!

WHAT: In this jam you can use any JavaScript based technology, from React to Pixi to Pure JS and everything in between. Here's how it will run:

  • Theme will be announced on the first day of the jam
  • Games should be started and submitted in the jam timeframe.
  • Any art or assets can be used - existing or created for the jam.

📆 WHEN: The jam will start at 00:01 UTC on August 1st and run until 23:59 UTC on August 11th. You'll have 11 days to build and release a multiplayer game on Rune.

📩 SUBMITTING: To submit simply release the game to Rune by the final date and let us know on Discord that its for the jam.

🎖️ WINNERS: Three winners will be selected based on play time of the game in the week starting 18th August. These lucky folks will be receiving a selection of sick Rune merch.

Remember this is all about creating something from end to end in the time period. Scope carefully and focus on the fun!

As always we can't wait to see what you build and if we're here to help! More information on how to build and publish a game is available in the documentation.

Come join us on the Discord to get involved!

Subscribe to our newsletter for more game dev blog posts
We'll share your email with Substack
Substack's embed form isn't very pretty, so we made our own. But we need to let you know we'll subscribe you on your behalf. Thanks in advance!

· 7 min read
Kevin Glass

Rune is a platform to get your games played by 1000s of players a day. When you’re rolling out games for this large of a player base, rather than a small focused target group, it’s really important to understand who you’re writing games for.

The community of developers around Rune are already getting a handle on what works for the players. Going with your gut is always a good start but more data is better, right? Let’s look at some of the statistics from the player base who are actively playing multiplayer games.

We should remember the data below is specific to the Rune platform but is also representative of a large number of players (millions in fact) in the real world.

Age Group

Player’s age affects many aspects of targeting games from attention span to what’s appropriate to show. While games used to be the domain of teens there are now gamers from all age groups, which means you either need to satisfy both ends of the spectrum or accept that you’re targeting your game at a specific group without knowing if you’re reaching them.

More specifically to the Rune platform however we know that players are largely in the 13-21 age bracket. They’re young adults who have played lots of online games but appreciate the chill atmosphere of Rune - hanging out with their friends on voice chat and playing a variety of session based mini-games.

This means that games need to be fast to get into, fast to understand, fast to win and play again. The enjoyment and reward is in sharing the experience with their friends so any feature targeted at bringing more friends in is a bonus!

Top Languages (other than English)

A really interesting factor in the Rune player landscape is the primary language players speak. As you can see the top 3 primary languages (other than English) are Russian, Spanish and Portuguese. Now of course many of these players also speak English because they’ve played so many games before, but appealing to these groups can be key to game success.

But that means internationalization, and that’s a lot of work! Well yes and no. Players on the Rune platform generally don’t want to read so avoiding text all together is a better design than translating it all. If you absolutely must have text, keep it short and simple so that either the players know enough English to understand or it can be translated very simply.

Interests

As mentioned above, the average player on Rune is a teenager and you can see that in their interests. No surprise gaming is #1 in the activities chart but I found it particularly interesting to see the mix after that, Drawing - Football - Math. How about a game where you draw footballs and calculate their volumes?! Ok, maybe not, but it does show quite how wide ranging the games can be.

Looking at the types of games they like we’re seeming pretty mainstream releases, so again helpful while you’re thinking about ideas.

In terms of theming you can see a heavy cartoon/illustrated style to the TV shows and movies that they like. Visually it doesn’t have to be something AAA game style, it can be that more abstract interesting style that indie game devs do so well!

Time Spent Gaming and Peak Times

On the Rune platform games are played within a room. The average room length is about 25 minutes and players have 2 or more of these a day. So another part of the picture becomes clear. These players are playing for about an hour a day - probably just enough time to catch up with their friends and have a few games.

We also see peak loads of players around the end of day, when players have come back from school or work, and they’re catching up with their friends. In fact that’s one of the reasons for the name Rune, that's when our players are playing!

Types of Game That Work

Finally lets talk about what games we’ve already seen working. It’s early days so there’s plenty of room for game developers to push out brand new ideas and have overnight hits. Games that have done well so far:

  • Strategy games like Cathead Defense where players build towers and defend the world from invaders together.
  • Turn based games like Dungeons of Glee where players go on adventure together in a paced chill environment.
  • Physics Games like Melancia where players have a shared experience dropping, bouncing and matching fruit.
  • Arcade Games like Please Hold where.. well, I think you'd have to play this one, it defies describing!
  • Traditional Games like Connect Four where friends play a familiar game in the context of their voice chat.
  • Idle Games like Tap the Button where players get bonuses for being together with friends.

There a few things to remember that might not be obvious when you’re designing a game to be successful on the platform:

  • Players will be hanging out and chatting with each other while they’re playing, games need to give space for them to chat.
  • Sessions with outcomes should be short, let players play a full game in the short amount of time they have (or are willing to commit to it)
  • Fun comes in lots of sizes, don’t assume that what isn’t fun elsewhere isn’t fantastic when you’re playing together with voice chat.
  • Assume players will play regularly, so progression and save points are recommended.
  • Super casual games work. The top games on Rune at the moment are super casual chill experiences.
  • A good place to start is a novel twist on something existing. Adding multiplayer is often a catalyst to new game mechanics.
  • Many players will try the game solo before recommending it to play with friends, try to make the solo experience as endearing as the real thing.
  • Don't forget to check out our Best Practices

Getting Your Game Seen

We're working hard to make visibility of games better on the Rune platform right now. As always though theres a lot of value in getting your game seen externally. Twitch and YouTube are your friends either by making your only videos or finding areas of your game that appeal to streamers.

Things that streamers/social-media likes:

  • Parts of games that cause players to scream/laugh/emote with each other.
  • Funny side effects of game logic - crazy physics is always a win!
  • Big effects for actions in games

Top 10 Games on Rune Right Now

At the time of writing the top 10 games on Rune are:

1. Melancia (jallen)
2. Scorched Turf (cokeandcode)
3. Tap the Button (jallen)
4. Tic Tac Toe (Helios1138, oblador, Shane Helm)
5. Quizcraft (cokeandcode)
6. Cathead Defense (propcat)
7. Dungeon of Glee (cokeandcode)
8. Pinpoint (Helio1138, Lynda)
9. Flick Footie (cokeandcode)
10. Jumper Race (Tonai)

You can see a wide variety of games work but they all have space for players to chat while playing.

Summary

Players of games generally range across many different demographics but with luck using the information in this article, our developers stand a better chance of writing the next great game on Rune!

If you’d like more information or just to comment on the article just drop into our Discord.

Subscribe to our newsletter for more game dev blog posts
We'll share your email with Substack
Substack's embed form isn't very pretty, so we made our own. But we need to let you know we'll subscribe you on your behalf. Thanks in advance!

· 12 min read
Kevin Glass

At Rune we’re building a place where developers can share their multiplayer web games with the world. This of course means we care a lot about game networking code and how it performs in the real internet at scale.

Networking in games isn’t easy, you’re dealing with a conflict between the speed of light, the distances between people playing on the internet and player’s expectations. Worse still a player’s emotional state, i.e. whether they win or lose, can often be dependent on the networking model that’s been used and whether it seems “fair”.

There are several types of game networking models used in modern games. Normally when you’re planning out a network game you’ve got to consider:

  • Latency for the Player - or “how does it feel?”. Will players still feel attached to their characters or units? If there’s noticeable latency between controls and action, can it be explained away in some game fitting way?
  • Available Bandwidth and Battery - some network models can have high network usage and can be expensive on battery (as well as bandwidth). Can your expected player base absorb that?
  • Is it difficult to implement? - We often try to aim for the most simple solution, assuming that the differences between the average and optimal solutions won’t matter. That’s not the case in network code. Is a simple model good enough?
  • What happens when the network goes wrong? The internet isn’t consistent. Across all the different connections you’ll see a lot of variance in latency and constraints. More annoyingly, you may see a large variance in a single connection. A great connection may still have lag spikes at 5x the average latency. What happens in your network model when there is a lag spike?

That fourth one is really painful. It’s extremely difficult to test properly meaning you’ll most likely only find out there are issues once the code is out in the wild. Game networking is a lot like video conferencing - if there’s a problem your users will just know. They’ll detect even tiny corner cases and it’ll distract from their experience.

The internet has constraints that make it hard to predict what it’ll look like when you try to play a game:

  • Packet Size - We’re all very used to being able to download large files. Libraries often abstract developers from the underlying transport and allow them to send whatever they want. However, the actual packets that run across the network have a fixed size (about 1500 bytes - MTU) so if you care about latency you need to be thinking in the packet model.
  • Physical Constraints - The internet is fast, but its not that fast. Let’s assume there was one fiber line around the world. A packet, even at light speed through fiber would still take ~200ms to travel around the world. Now add in routers and switches on that path, and remember that most of the internet isn’t fiber. Don’t forget the last hop to the end user device. This tells us it’s important to think about regional servers (outside of any network model).
  • End User Devices - There are a great variety of devices on the internet. Mobile games (where Rune targets) can have a wide spectrum of device power and connection availability especially in emerging markets. The game of course still needs to feel fair.
  • Congestion and Loss - The internet is a shared resource. Most of the time this doesn’t matter, most routes and backbones are hugely under subscribed. When congestion does become an issue the internet has one solution - drop low priority packets (that means yours).

It’s pretty clear it’s a hard nut to crack but of course we do see many successful internet playable games. Clearly there are network models that work. Let’s look at a (non-exhaustive) list.

Turn Based

Some games don’t need real time network or for that matter low latency controls. If the game is either actually turn based - or can be treated as turn based - we can use a simple authoritative server and message passing.

Player 1 takes their turn. They send a message describing their actions to all the other players via the server (or peer to peer). Each receiving player (and the original player) plays out those actions in their game state. The next player then takes their turn and so on. If needed the server can also take a turn to move the computer controlled elements. Games like X-COM did this with great success. With additional rules like overwatch, the game still felt dynamic and fun but the networking didn’t need to have any pressure at all.

There’s a couple of caveats here.

  1. The players must start from the same game state.
  2. The application of the actions must be deterministic, that is playing out the actions must result in the same state on all instances of the game logic.
  3. If a player joins mid-game you still need to be able to serialize the complete game state and pass to them.
  4. If a player lags then all the other players get to wait too.

Huge pro on this one, it’s really easy to implement!

Brute Force

Some of the early real time shooters out there did networking but they didn’t obsess over the details of making it “fair”. If your players can cope with accepting what they saw was wrong and they died anyway then you can skip trying to be clever.

The approach was simply to spam the network with packets describing where you said you were and what you said you were doing. The server would accept what you said as fact and try to resolve any interactions within a set of rules. e.g. if A says he shot B and they are close enough then he must have, tell B to take the damage and die.

Surprisingly the games were still fun and players weren’t screaming about it being unfair. They simply didn’t know anything better and just accepted lag deaths. In most cases the rules on the server would be good enough to make things seem ok and players enjoyed the games - until the event of unofficial modding of clients.

As soon as you’re trusting clients you’re in a dangerous place where a nefarious player can modify their games to let them cheat by just keeping within the bounds of the server rules. Games makers went back and forth with the cheaters trying to implement more and more complex rules to prevent the modified clients from working. Of course there were only a few devs and 1000s of players so it was a losing battle.

Lock-Step

In lock step all the clients in the game are peers of each other. They all run their own copy of the game logic. Each frame the client sends the player’s current inputs to all the other clients. Clients only move the game forward once they’ve received the inputs from all players for any given frame - since it’s only then they know they have enough information to work out what needs to be shown.

This worked very well on local networks but became more painful on the internet. In the lock step model if one player is lagging - all of the players lag since they need to wait for a step from that player.

Again, since the clients were being trusted, games were open to cheating. This didn’t stop some games being very successful using the model. Traditional RTS games (like Starcraft) used the model along with some older shooters like Doom 1.

In the RTS/Click-To-Move games the lock-step model works particularly well, since players expect a certain delay between telling a unit to move or act. This delay period can be used as a buffer for any lag between peers. Diablo 1 used this approach, and found a really interesting bit of information out about players. Players didn’t hate the delay as long as it was the same delay every time. Diablo actively managed the delay between the player’s click and response by moving the delay window until it could remain constant.

Delayed Action

Another common network model is delayed action. In this model each player runs a copy of the game logic / state. When a player takes an action it’s sent to the server for scheduling. The server game logic is ticking along, when it receives an action from a player it schedules it in its game logic and notifies the clients (including the client it receives it from). Each of the clients schedules the action at the time the server has determined. The client and servers copies of the logic execute the action and then the results get played back.

It feels a bit like turn based, but the server time step is always moving forward. Two players can take actions at the same time and the server will schedule them deterministically on all clients.

Sounds great, but there are few issues:

  • Client game states can’t move forward any faster than the server, so the client is waiting on a tick message from the server. If there's a lag spike client’s feel it immediately. Clients generally run along behind the server for a few frames to give a buffer should a spike happen.
  • There’s a latency between when the player pressed the button to move the unit and it actually moving since the message has to get from client to server and back again before being executed. Since clients are generally running a bit behind this latency can be larger than just the network delay.
  • If the client gets a lag spike it might well get behind by a significant amount since it’s waiting on ticks from the server. A client will then get a whole series of packets in one go causing it to have to speed up. Games do this very subtly in the hope players don’t notice.
  • Game logic has to be deterministic such that when actions are applied on all clients they end up with the same state.

Predict-Rollback

Many modern games use the Predict-Rollback model which tries to balance low latency client controls with authoritative servers that prevent cheating.

In this model clients and a central server keep a copy of the game state. Each client is running ahead at its own rate trying to stay somewhere close to server time. When a player takes an action the client schedules locally at a time based on its local measurement of the delay to the server.

It then sends the action to the server which schedules it at the same time the client decides - assuming that it's not in the server’s past. The server passes the action out to all clients with the scheduled time. Since we’ve accounted for server delay in the scheduling in many cases all the clients and the server apply the action at the same point in game logic and everything stays in sync.

The client is predicting what the outcome of its action will be and letting the player continue on the assumption it’s correct.

So where does the rollback happen? There are a couple of cases:

  1. If the time that the client wanted to schedule the action at was in the past for the server, then the action needs to be rescheduled at a different time in the server’s future. All clients will be told the new time (including the original sender)
  2. If the client that’s predicting the outcome of the local actions receives an action from another client that’s in its past. This may make its game state prediction incorrect.

When these cases happen the clients have to rollback their game state to point before the incorrect or new action happened. Then play forward again the actions until the current time - thereby getting to the correctly synchronized game state. The clients then continue from that point predicting the outcomes of player actions.

Some implementations have rollback functions that are “anti-actions” - in that they undo whatever the action would have done. Others keep a copy of the last known valid server state on the client by maintaining two copies of the game state.

Predict-Rollback is great because for many cases the prediction is accurate and the player gets very responsive controls. It also supports an authoritative server without stopping clients that are doing the right thing from moving forward. Lag is limited to the player with the bad connection.

Tricky bits with this model:

  • It’s hard to implement. It’s a complex model that requires engineering to get right.
  • When there’s a conflict and a rollback occurs there’s a spike in CPU that needs to be managed carefully.
  • Rollbacks that cause significant change need to be managed by the game rendering - hiding issues and making things seem natural.

All that being said, the pros of quick input response time, low bandwidth and conflict resolution mean this is becoming more and more the standard approach to game networking.

Summary

As you can see there are different ways to make game networking work, and as you can tell from the games mentioned people have been trying to solve it for decades.

At Rune we implement predict-rollback, it’s complicated to implement but the bright side is you don’t have to, it’s just part of the platform. If you want to learn more about how to implement a multiplayer game on Rune, check out our examples, tech demos and documentation - or join our Discord.

Subscribe to our newsletter for more game dev blog posts
We'll share your email with Substack
Substack's embed form isn't very pretty, so we made our own. But we need to let you know we'll subscribe you on your behalf. Thanks in advance!

· 11 min read
Kevin Glass

Here at Rune we provide a multiplayer Javascript games SDK and platform for everyone to get their creations out to 1000s of players. We provide technical demos to illustrate how you can use the platform to implement various game types.

I’ve been putting together a new tech demo that shows how to implement the basic synchronization of movement between clients connected in a Rune room for a top down game. You can try it out here. Starting here gives us a super simple example of how to wire everything up without worrying about gravity/physics or complex collision detection.

Let’s start by looking at the architecture of a Rune game and the separation between rendering and logic.

It’s often considered good practice to separate your data model and logic from the rendering, i.e. the MVC pattern. However, when it comes to multiplayer this isn’t just best practice, it’s absolutely required to let us run copies of the game logic on both the server and client.

The logic should only contain the data that is required to make the decisions about how the game updates and how winning/losing can be evaluated - i.e. the game state. We want to try and keep it as simple and fast as possible since in the predict-rollback network model (that Rune uses) we will be running multiple copies of the logic. The logic has to be implemented with some restrictions that allow it to be executed both on the browser and server.

The renderer, or client, is the code that actually converts the game state to something that the player can view and interact with. The client can be implemented using any library or framework that can run in the browser.

Let’s get to the code, I’m going to assume you already know how to create a game project and just jump straight into the logic. In this demo we’re going to have a map, players and trees. So first let’s declare some types to describe those:

// types of entities we'll display in the world
export type EntityType = "PLAYER" | "TREE"

// an entity is anything that is displayed in the world
// outside of the base tile map. These will be
// z-sorted to give top down style depth
export type Entity = {
x: number
y: number
sprite: number
type: EntityType
playerId?: PlayerId
}

// the extra data for the player
export type Player = {
// the state of the controls for this player - this
// is the bit thats actually sent regularly across
// the network
controls: Controls
animation: Animation
vx: number
vy: number
// true if the player is facing left instead of right
// as the sprites are designed
flipped: boolean
} & Entity

// the controls that we're applying to the game state
// based on which inputs the player is currently pressing
export type Controls = {
left: boolean
right: boolean
up: boolean
down: boolean
}

Next we’ll need to describe the game state we want to synchronize, in Rune that’s as easy as this:

// this is the core of what we're trying to keep
// in sync across the network. It'll be held on clients
// and server and the Rune platform will keep it
// in sync by applying deterministic actions
export interface GameState {
entities: Entity[]
players: Player[]
}

We need to setup an initial state for the game which all clients will start from before applying changes they receive from clients:

Rune.initLogic({
setup: (allPlayerIds) => {
const initialState: GameState = {
entities: [],
// for each of the players Rune says are in the game
// create a new player entity. We'll initialize their
// location to place them in the world
players: allPlayerIds.map((p, index) => {
return {
x: (index + 1) * 64,
y: (index + 1) * 64,
playerId: p,
type: "PLAYER",
sprite: index % 4,
animation: Animation.IDLE,
controls: {
left: false,
right: false,
up: false,
down: false,
},
flipped: false,
vx: 0,
vy: 0,
}
}),
}

// add the tree entities
for (const tree of trees) {
initialState.entities.push({
type: "TREE",
x: tree[0],
y: tree[1],
sprite: 4,
})
}

return initialState
},

In the game logic we need to declare what the clients can do and how the game should update each frame. In Rune the game update is defined as part of setting up the Rune SDK like so:

update: ({ game }) => {
// go through all the players and update them
for (const entity of game.entities.filter((e) =>
e.type === "PLAYER")) {
const player = entity as Player
// assume the player is doing nothing to start with
player.animation = Animation.IDLE

// for each control that the player has pressed attempt to move them
// in the appropriate direction. Only move if the player isn't blocked
// by whatever is in the tile map.
if (player.controls.left) {
player.vx = Math.max(-MOVE_SPEED, player.vx - MOVE_ACCEL)
player.flipped = true
} else if (player.controls.right) {
player.vx = Math.min(MOVE_SPEED, player.vx + MOVE_ACCEL)
player.flipped = false
} else {
...

The game logic is configured to run at 30 updates a second and on each update we’re going to move the players based on what their controls are - i.e. are they pushing up/down/left/right. There’s a simple tile map collision check in there to stop the player running off the side of the world - but that’s it.

So how does this synchronize the clients?

The Rune platform runs this logic on the server and each of the clients. When a change is made to the game state is first applied locally - so latency in controls is very low - and then sent to the server and subsequently to all other clients. This is all timed so that the local client isn’t applying the changes too early and gives the server time to schedule the change at the right time.

So everyone playing and the server have a copy of the game logic which they’re keeping up to date based on the changes they receive. This relies on the game logic being fully deterministic but from a developer point of view means you don’t really have to think about how the sync is happening. As long as you keep your updating code in the game logic, the clients will stay in sync.

The client will run a copy of this logic and update() loop so will immediately update is run. The server will also run a copy of this logic and update() loop but slightly behind the client to allow for any action conflict resolution, e.g. two players try to take the same item. When the server has resolved the conflict the client will rollback its changes if needed and apply the new actions from the authoritative server putting the client back in the correct state.

The final bit of the game logic is how the “changes” to the game state can be indicated by players, what Rune calls actions.

// actions are the way clients can modify game state. Rune manages
// how and when these actions are applied to maintain a consistent
// game state between all clients.
actions: {
// Action applied from the client to setup the controls the
// player is currently pressing. We simple record the controls
// and let the update() loop actually apply the changes
controls: (controls, { game, playerId }) => {
const entity = game.players.find((p) => p.playerId === playerId)

if (entity && entity.type === "PLAYER") {
(entity as Player).controls = { ...controls }
}
},
},

The actions block defines the set of calls the renderer can make to translate player input into changes to the game state. In this case we simply take whatever the client has said the controls from the player are and store them in the player entity. As mentioned above, because the client is running its own copy of logic these changes are quickly applied.

You can see in this case we’re sending the controls rather than explicit positions, which at first might seem a little strange. This makes sense when you consider one more factor, conflict resolution.

If two players both make actions on their local copy of logic that conflict in some game specific way then the clients have to rollback their game state, apply the actions in the correct order and recalculate game state. Let’s say they both try to take an item at the same time, because their logic is running locally they’ll both think they took it. Once the actions reach either end it becomes clear that one player took the item first and the Rune SDK calculates the state to match the correct situation.

Now, if we sent explicit positions this conflict resolution would result in significant jumps - where a player’s actions were completely disregarded because they were in complete conflict. If we send the controls then the resolution is much smoother, the player still pressed the controls and had them applied, just the resulting game state is a little different. A lot of the time this can be hidden altogether in the renderer.

Now we have the game logic, the players can update controls and they’ll move thanks to our update loop. The final part is to get something on the screen and let our players play! The tech demo uses a very simple renderer without a library or framework. It just draws images (and parts of images) to a HTML canvas and uses DOM events for input. Check out graphics.ts and input.ts if you want to see the details.

First we need to register a callback with Rune so that it can tell us about changes to game state:

// Start the Rune SDK on the client rendering side. 
// This tells the Rune app that we're ready for players
// to see the game. It's also the hook
// that lets the Rune SDK update us on
// changes to game state
Rune.initClient({
// notification from Rune that there is a new game state
onChange: ({ game, yourPlayerId }) => {
// record the ID of our local player so we can
// center the camera on that player.
myPlayerId = yourPlayerId

// record the current game state for rendering in
// out core loop
gameState = game
},
})

The rendering itself is purely taking the game state that it’s been given and drawing entities to the canvas:

// if the Rune SDK has given us a game state then
// render all the entities in the game
if (gameState) {
// render all the entities based on the current game state
const allEntities = [...gameState.entities, ...gameState.players]

;allEntities
.sort((a, b) => a.y - b.y)
.forEach((entity) => {
if (entity.type === "PLAYER") {
// players need to be rendering using animation
// and flipping
const player = entity as Player
drawTile(
player.x - playerFootPosition[0],
player.y - playerFootPosition[1],
entitySprites[player.sprite],
player.animation + frame,
player.flipped
)
...

The only other thing the renderer needs to do is convert player inputs into that action we defined in game logic:

// we're only allowed to update the controls 10 times a second, so
// only send if its been 1/10 of a second since we sent the last one
// and the controls have changed
if (
Date.now() - lastActionTime > 100 &&
(gameInputs.left !== lastSentControls.left ||
gameInputs.right !== lastSentControls.right ||
gameInputs.up !== lastSentControls.up ||
gameInputs.down !== lastSentControls.down)
) {
lastSentControls = { ...gameInputs }
lastActionTime = Date.now()
Rune.actions.controls(lastSentControls)
}

There’s a couple of gates put on sending actions. We don’t want to send unchanged controls into the game logic, it won’t change anything and wastes bandwidth. The Rune SDK also limits us to 10 actions per second from any client so we make sure we’re not breaking that.

That’s pretty much it, we have a game logic that will keep the client’s game state in sync and a renderer that will let our players play. Of course this is a simple tech demo but with a little more work, collision between players, fighting and item collection could easily be added.

If you have any questions or comments on the tech demo or Rune in general, be sure to join our Discord.

Assets from Pixel Frog.

Subscribe to our newsletter for more game dev blog posts
We'll share your email with Substack
Substack's embed form isn't very pretty, so we made our own. But we need to let you know we'll subscribe you on your behalf. Thanks in advance!

· 5 min read
Kevin Glass

Rune is our platform to get your games seen by 1000s of players every day. This is one of things that particularly attracted me to the platform. The most common meme I see on my X/Twitter feed is this:

Why? It’s not that indie game devs don’t market their games. They actually spend a lot of time reading and trying to understand how to optimize their store pages, trying to stand out in the crowd, working out how to post to Reddit for maximum exposure, using unfamiliar social media like TikTok and Instagram, and how to get the journalists to see it.

So why do indie games always seem to struggle with getting out there. Well because it’s hard, like really hard.

  • Posting your game and getting lots of retweets on Twitter is marketing to game devs. They do love games but they’re also busy writing them. Unlikely to be players.
  • Reddit posting is extremely hard to get right. Your post might just get lost in the stream of the people doing the same thing. Your post might get flagged as an advert and you get banned forever. Your post might get absolutely trashed because someone is having a bad day, hurting your motivation but sometimes increasing your exposure. Your post, if you’re very lucky might get some great up votes and huge traction. Even then you could be all the wrong types of eyes on your games.
  • Stores are saturated with indie games. Steam, Epic, App Store and Google Play all have an enormous amount of games being released every day. It’s really difficult to stand out and then of course you’ve got commercial games being pushed out with huge marketing and brand names to compete with.

TikTok and Instagram are ideal places to market games, you’re reaching the players not game devs, or journalists - but the people who might actually play your game. The tricky part is the type of content people want here and how to convert them into actually playing.

Video viewers are looking for short clips, you’re not going to get a lot of time to show how cool and interesting your game is. They also want humor - which in many games doesn’t really happen in a 10 second viewing. Finally they want style and polish. If your game is the next great 3D adventure this can work well, but if you’re building a web game with a stylistic look (pixel art, geometry based, etc) it can easily fall flat.

Time Magazine: Most people now have a shorter attention span than a goldfish

Finally even if your videos are great, you get a great content creator to provide a narrative and you’re seeing huge view numbers - how many of these people are then moving on to play your game? Unfortunately not too many - they’ve done all the work they're willing to do, opened their app and viewed your video. Clicking a link and installing something, that's a big ask.

All of this sounds pretty dire but it is possible. Many of my game dev friends have had huge success stories or even minor hits through consistent and quality marketing strategies. It’s just hard.

Back when the App Store was new there was a gold rush, an opportunity for indie game developers to be visible. Same with Steam and Google Play. There are moments in time in every game distribution platform where there’s room to shine - the only problem is often the player bases aren’t huge at that time. Of course the App Store, Steam and Google Play had huge potential player bases and space to fill.

What we all need to look out for (and what attracted me to Rune) is a place that:

  • Has space to be seen
  • Has enough players to make it worth the time investment
  • Has a way for me to make a dollar in doing so

Unfortunately I missed the boat on App Store, Google Play and even itch.io, so it’s always worth looking into new ones as they arrive.

As I mentioned above the viewing audience is getting less and less willing to take action to go and find the thing they’ve seen. An interesting trend in distribution platforms is mixing the social aspects of the application (your TikTok / Instagram / Twitter features) with the actual playing of the games. This simply means the barrier to go from seeing something to playing something is lowering - the shared content is the actual game so by the time you’re seeing it - you’re playing it too! You could of course argue that Steam tried to retrofit this into their platform but unfortunately ended up with a largely toxic environment.

The newer generation of players have a lower attention span, they just want it all right now. Platforms that give these players the games in the context of their social media are where we should be looking. Lets bypass traditional marketing altogether and just put the games in front of the players in a familiar social wrapper.

If you’re interested in this topic or want to hear more about how Rune is doing this by all means hit me up on Discord.

Subscribe to our newsletter for more game dev blog posts
We'll share your email with Substack
Substack's embed form isn't very pretty, so we made our own. But we need to let you know we'll subscribe you on your behalf. Thanks in advance!

· 9 min read
Kevin Glass

Rune is a platform for publishing web games to millions of players, and we love every game that a developer builds on it. One of the more difficult parts about distributing web games is the number of types of devices the game will end up running on. If you are building web games for the desktop then performance is often not a factor. Web games tend towards being more simple visually than traditional desktop or console games and as such the super powered machines that most desktop players have don’t struggle with even non-optimized code. This is great for artistic and creative approaches to building games, developers can simply focus on making the game exactly the way they want it, in the easiest way for them.

However, it’s different when you start considering mobile devices. Here at Rune we have a large player base which also means a huge variation in end user devices. If you’re trying to be inclusive and accessible in your game dev, which I know most of us are, you need to be making it possible for older devices to play your games (think developing nations and young people). Here’s some stats on the version of devices we see in our player base.

What we see above fits with what I’ve seen in other environments too, iOS devices tend to keep up with versions - probably because of the Apple policy of pushing people hard to upgrade. Android however has a huge range of devices due to both the nature of the users wanting to keep their devices for a long time and the number of low budget models out there. The other thing to consider is that iOS devices on a later version nearly always have up to date powerful hardware as well. Apple doesn’t keep old operating systems running on old devices for too long. Android however has many low spec devices released that are still using the modern OS versions.

Of course when you’re building games you’re always trying to balance the time it takes to optimize code against the number of potential players you’ll get. It’s all about time management. The data above and my own experience shows me that when you’re building web games and targeting mobile platforms the main pain point on performance is going to be Android.

When it comes to multiplayer games the logic code at least will likely be running multiple times - attempting to rationalize the differences between the authoritative server state and local client state - so performance impact is compounded.

So, how do we address performance of games on a wide range of Android devices?

Minimize Garbage Collection

The javascript runtime has come a long way, and specifically garbage collection (GC) is really really fast compared to some other languages (I’m looking at you Java). It has let us web devs get very used to creating temporary objects and replacing complete instances rather than updating fields in them - generally this leads to nicer code at least in my opinion.

However, regular GC in a game loop is the touch of death on low end Android devices. First, their runtimes might not be quite as far along as we’d like (see below). Second, efficient GC relies on having a fair amount of memory to play with - this is often not the case on lesser Android handsets.

Consider where you can reuse objects instead of creating new ones.

Old School Hackz

Back in the bad old days we had to optimize things by hand regularly. People did (and some still do) take great pride in spending a lot of time getting the tightest game loop possible. Today the profiler is your friend to find those bottlenecks, you can never assume anything, but some of the less well known from past are things are still issues on slow Android devices today.

Example 1: Collision Checks

Many games perform a lot of simple collision checks between a large number of bodies. Often this is as simple as checking the distance between two points:

let dx = p1.x - p2.x;
let dy = p1.y - p2.y;
let totalRadius = p1.radius + p2.radius;

if (Math.sqrt((dx*dx)+(dy*dy)) < totalRadius) {
// Collision
}

That Math.sqrt when used multiple times can really add up. So consider that we change the check to this instead:

let dx = p1.x - p2.x;
let dy = p1.y - p2.y;
let totatlRadius = p1.radius + p2.radius;

if ((dx*dx)+(dy*dy) < totalRadius * totalRadius) {
// Collision
}

Same result, no Math.sqrt.

Example 2: Angle Calculations

Math.cos/sin/tan/atan2 are often used to work out movement and directions of entities in the game. Again, while these operations are so much faster than they used to be they can still end up being a bottleneck if used in the main game loop. One common gotcha is using an infinitely accurate value for the angle, so we end up computing Math.cos(1.1) and Math.cos(1.10001) preventing any sort of lookup optimization.

So two things to consider

  • Clamp your angles! Make sure your angles are only accurate to a fixed number of decimal places. Even better if you can get it down to 1 degree accuracy.
  • Use a lookup table. Calculate all the trigonometry results you’ll need at startup and just index into an array to pick them up. If it was good enough for Wolfenstein 3D, it’s good enough for us!

Use WebGL (sometimes)

If you’re using a library then the chances are it's gone the WebGL route and implemented an efficient renderer for you. If you’re not and you’re going to write your own, be very careful. Writing a WebGL renderer that works really well on desktop and modern devices but fails dismally on older devices is all too easy.

  1. Make sure you know which extensions you’re using and why, and what will happen if they’re not available.
  2. Keep your shaders short. Android devices with old or poor chipsets will struggle otherwise.

Another option is to look at HTML Canvas for rendering. Canvas used to be really slow and we all have scars, but it's been accelerated everywhere for quite some time. If you have the option and it makes sense for your game consider using Canvas instead.

Anecdotally one of my games with a pretty well optimized WebGL renderer was outpaced by far by Firefox’s Canvas renderer on older Android devices!

Adapt Framerate

The obsession with 60fps or even 120fps is so important in desktop gaming, it’s an absolute expectation of players. As you move into web games on the mobile web it is easy to assume the same. Here’s a dirty secret, most mobile games don’t run at 60fps. Even if their refresh rate to the screen is 60, the actual redraws are much less.

There are two ways to approach the framerate optimization:

  • Just always run at 30fps - make the game feel right there and accept that on higher end devices you’re just saving them a lot of battery!
  • Detect when the game is running at less than your target FPS and step it down. This is slightly harder to do because you don’t want tiny peaks and troughs of performance to impact the stepping.

One nice thing about the model Rune uses is that since the logic is always at a fixed framerate independent of rendering, changing the framerate is often very easy.

Collision Detection and Physics

Collision Detection and Collision Resolution (physics) can be extremely complicated pieces of code. Optimally determining which entities need to be checked, whether they actually collide and what the resulting change is heavy on the CPU.

If your game isn’t a pure physics simulator, e.g say a platformer or a shoot 'em up, consider writing a simple custom handler instead of using a full physics engine.

CPU and Network Throttling

A feature that isn’t as obvious in Chrome Dev Tools is the CPU and network throttling. When you’re working on your game even on your desktop you can easily simulate a much slower processor and network connection using these options:

Throttling down CPU in particular is a really good way to see how it’s going to feel on a old Android device. I generally work at 6x slower but 20x is more of match to the really low spec device.

Android System WebView

Most of the time any web view in an application, like Rune, is going to make use of the Android System WebView. On modern versions of Android this is essentially Chrome and performs really well.

There are many versions of the Android operating system and particular devices where the Android System WebView can’t be upgraded so leaves your game running on something slower and more buggy. It's such a severe problem that in the Rune application we enforce a minimum web view version so developers don't have to worry about it.

If you’re hearing about performance issues on your web game and it’s on Android, get them to check the System WebView version.

Testing

Final note. There is nothing like real device testing. The emulator gets you so far but if you can acquire an old device to test on its really worth experiencing how very different it can be. Ebay is a great place to start. I use a Samsung J2 which is definitely the worst device I’ve ever seen but it’s been invaluable in testing performance tuning.

Hopefully this blog is helpful to some web game developers out there. If you’d like to discuss more don’t hesitate to hit me up on Discord.

Subscribe to our newsletter for more game dev blog posts
We'll share your email with Substack
Substack's embed form isn't very pretty, so we made our own. But we need to let you know we'll subscribe you on your behalf. Thanks in advance!

· 6 min read
Kevin Glass

Here at Rune we're building a platform to help developers make multiplayer mobile games and get them out to plenty of players. The technology available to make games has made it easier over the last 30 years. When I started, getting sprites on the screen was the hard bit. That was back in (muffled noises). Then it was getting 60fps. Then it was cross platform. Then it was 3D, and then it was mobile cross platform games that were the challenge. At each stage the bar gets raised and we can do more with less.

The next on the list for indie game developers like me is internet multiplayer - to many of us it's that scary and complicated piece that we don't think we can scale to. I've tried and failed multiple times to build something truly internet playable. It's not impossible, it's just hard. There's so much going on in the modern internet it's really hard to predict and design for all the network conditions you might encounter.

Tarn Adams (Dwarf Fortress): "Multiplayer development is like juggling chainsaws. Each element is complex on its own, but once you start adding other players into the mix, the complexity increases exponentially."

Different devices, CPU throttling, WIFI vs cell connection and different approaches to network all make the code itself hard to write. After that you need to think about optimizing latency with regional infrastructure. Even after you've got past the code and infrastructure complexity, you get to start working out how you're going to build a community big enough that there will be people online to play.

Rami Ismail (Vlambeer): "Making a multiplayer game is not just about coding the gameplay. It's about creating an infrastructure that can handle a community, and that’s an entirely different beast."

So, yes I work at Rune now and of course I'm going to be slightly biased, but 5 months ago I didn't. I was quite happily leading an enterprise oriented company using every spare minute I had to think, work on and talk about games. Your typical hobbyist/indie developer, building games for the love of it and always having the next project in mind. I'd recently built both a moderately successful MMORPG and an online Terraria clone, so suffered the pains of writing my own networking. At this point I'm thinking about mini-games as a relief and the link to Rune was passed to me by a friend.

The stack is Javascript/Typescript which happens to be where my game development had taken me (via BASIC/ASM/C/C++/Java/Unity) so it felt like a good fit. Having written a few games in the first month I was pleased with the outcome, playing them with my friends on the app was nice and it felt like a smooth process to get things working.

Did it solve everything for me with a click of its magical fingers? Of course not! You still have to think about your code and data structure, design your game for multi-player and consider the myriad of corner cases that multiplayer throws your way - you simply can't get away from that.

What it did do, was give me a free predict/rollback networking model (something I'll write about later), an infrastructure that meant players weren't always getting huge pings and framework in which I could stop thinking about how the networking was going to work and focus on the bits I enjoy - rendering, feel and game design.

So how do we actually build the game? You have a two pieces to build:

  • The Renderer - here you can use whatever libraries or frameworks you like. There are games built with React, ThreeJS, PixiJS and many others already. The SDK is javascript based so it's easiest if you start there but I'm pretty sure some of the engine experts will find a way to integrate pretty quickly. For me I used plain Canvas and the other direct browser APIs to write my first game.
  • The Logic - this code is going to run on the devices and on the server so there's one authoritative point. As such it's a bit more sensitive to what you can and can't use. It's still vanilla javascript and there are eslint plugins to guide your development here.

Most of us have this split in our game code even in single player games so it should feel pretty familiar to most game devs. The logic is about the raw data. The renderer is about translating that to and from the user. It's important of course, given the games are going to be running on mobile devices, that the code is pretty performant. Not just for player experience but for battery life.

In my case, for my first game, I decided to port a game I had just started working on over to Rune to try it out. Making a multiplayer version of a traditional rogue/nethack game felt like a good first step. Since I'd already developed the game with a separate logic and rendering layers it was pretty quick to port over - and with the help of the amazing doodle rogue tileset it's looked pretty good too. I do like a ridiculous name, so that game became "Dungeons of Glee" (say in a deep voice!)

It was a surprisingly quick process. I think two evenings of game dev time took me from old prototype to deployable game. There was a quick round of review where the feedback was helpful and the game was out and ready to play!

As an expectant indie game dev, I waited with excitement to receive my first player stats email and see how wonderfully my game had done. Not well it turns out. One thing you have to get your head round is when you have this large potential player base, is doing some research on what type of players they are - really matters. Previously my game success had been based on reasonably small numbers, so I basically knew the type of player I was hitting.

In this environment I'd missed the mark considerably but I was lucky enough to get in contact with the people behind Rune, they helped me to understand the player base and happily I now receive weekly emails with 1000s of hours of gameplay every week.

I liked the platform so much when I got the opportunity to join the company it really was a no-brainer for me, and here we are.

Am I still building games? Of course I am! Hopefully this was an interesting read, if you want to discuss anything here or otherwise just hit me up on Discord

Subscribe to our newsletter for more game dev blog posts
We'll share your email with Substack
Substack's embed form isn't very pretty, so we made our own. But we need to let you know we'll subscribe you on your behalf. Thanks in advance!

· One min read
Bjarke Felbo

Today we're stoked to announce that we're launching a $100k grant for open-source web games! 🥳

Indie game developers and web devs can now receive a grant to make an open-source multiplayer game using JavaScript / TypeScript. With these grants, we hope to support indie devs wanting to make awesome games and dramatically boost the open-source web game ecosystem!

Rune will award two kinds of grants to indie game devs who make an open-source multiplayer web game:

  • Spark grants of $500 awarded to promising indie game devs
  • Ignite grants of $5000 awarded to devs proven themselves with a Spark grant

Awardees will also get featured on this blog, in the Rune app and on social media. We think it's a win-win for everyone:

  • New exciting multiplayer games on Rune
  • Interesting open-source code that others can learn from
  • Free money and recognition for talented indie devs

Check out the grants page for eligibility, application process, and all the details! We're so excited to see all the amazing open-source games that you will make!

Subscribe to our newsletter for more game dev blog posts
We'll share your email with Substack
Substack's embed form isn't very pretty, so we made our own. But we need to let you know we'll subscribe you on your behalf. Thanks in advance!