Skip to main content

· 4 min read
Kevin Glass

At the core of Rune, we have games being enjoyed by millions of players. I’m glad to take the time to celebrate one of them, Melancia, that has been doing very well on the platform. This wonderful game was made by jallen.

Play Melancia Now

What’s the Game?

Deceptively simple, the game features a Tetris-style well in which the player drops brightly colored fruits. When two fruits of the same type collide, they combine to make the next bigger fruit. The aim, as the name of the game may give away, is to make melons by combining all the other fruits. Other players are also playing in their own wells, trying to get to the melons before you.

It’s a clean concept implemented with especially satisfying physics and sounds that evidently keep players coming back for more. There’s something very delightful about the pop as you make your next fruit combinations.

What’s Great About It?

Things that seem to work about Melancia on Rune (good tips for other devs):

  • Simple, understandable mechanics with no text to read
  • Bright and obvious graphical style
  • Physics—especially shared physics—make for a pleasing experience
  • Easy controls for mobile

How Does It Work on Rune?

The game was implemented on the Rune SDK without having to change anything or take any special measures. If you’d like to see for yourself, the source is open:

https://github.com/jallen-dev/dusk-games/tree/main/games/melancia-game

The game was built over the course of a week, which, while short for game development, is pretty common amongst Rune games. It doesn’t take months of work to put out something to millions of players!

Developer Interview

Jallen was kind enough to answer a few questions for us on the game and the development process.

How long have you building games?

About a year. I'm originally a web dev who has been easing into gamedev by making games in React, pixi.js and three.js.

What gave you the idea for the game?

It's heavily inspired by the game "Suika Game" which went viral last year. The main way Melancia Game differs is it's multiplayer and a race against the clock. It also puts less emphasis on the puzzle aspect, since there is no penalty for your fruit spilling out of the top of the container.

How long did the game take to build?

1 week. I started it on March 12 and it went live on March 19.

What was the most fun bit of the game to develop?

Figuring out how to get the physics library to play nicely with Rune. It was a fun challenge to solve, and I tested a few different solutions. What I ended up going with is having the client send the position/rotation/velocity of every fruit each time it drops a new fruit, so that the other clients can sync. Since each player has their own separate container of fruit, it's not really a problem to let each client be the source of truth for that player's fruit state.

Did you expect the game to be successful?

I had a feeling it would do well, since it's based on a hit game. I think my changes to the gameplay also helped it be a better fit for Rune's audience. It's faster-paced and requires less deliberation, which is ideal for a casual game over voice chat. Still, I was surprised by just how well it has done.

What would you different next time?

Use propel-js to do the physics logic side. My solution with syncing state is a bit of a hack. Plus having the physics in logic would enable new features, like players sharing the same container (i.e. a co-op mode).

How did you find Rune to work with?

It's fantastic. Small API surface, not opinionated about what you use to build your game's client. I think it's great for most types of web games.

Anything else you'd like to say?

If you liked this game you might like another one of my games that shared the same inspiration: https://coinjargame.com/

What Do the Players Think?

One player comments:

The game is just cool

Another player says:

The game is simply top

I think they like it! Thanks so much to jallen for building the game and giving joy to so many players around the world.

If you’d like to talk about the game, learn how it was built, or build your own, drop in at 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!

· 7 min read
Kevin Glass

I’ve built a few game servers over my career, including session-based mini games and an MMORPG. At Rune, that experience comes in handy as we’re building a game architecture that needs to support 10 million players. Getting the architecture right is key to having something scale in the long term.

There are lots of resources around the web on game server architecture, but it’s also worth noting that the requirements for gaming are very similar to voice/video conferencing (maybe that’s why WebRTC fits so well?) - so it’s worth checking out the architectural approaches there too.

The following are some of the issues and requirements to consider when building your own scalable multiplayer game architecture.

Matchmaker vs. Real-Time

Most games have some form of matchmaking, or working out the best pairing of players. Some games also have social and configuration type activities that don’t require player-to-player interaction. These are normally categorized as “matchmaking.” The requirements on latency in these scenarios are reasonably low. If a player is choosing a map or finding another set of players to compete with, a second or more response time is acceptable.

Once the players actually start playing the game, they immediately have a different set of requirements. In the “real-time” phase, player latency changes the game experience dramatically. It’s important that we’re optimizing for low latency on the network and high throughput on the server activities.

Since there are two different sets of requirements, it’s good practice to split these two scenarios into different components in the architecture. In the diagram above, we have the split between the central server - responsible for our matching type activities, and the real-time regional servers used for relaying in-game messages.

Regional Servers

As mentioned in Modern Game Networking Models, one of the first things to think about in any networked game is making sure that the connection between client and server is the best it can be.

Even if you assume everyone is on a great connection (something that isn’t true!), in the best case, network packets travel at the speed of light. If players are connecting from anywhere in the world, then the distances between a central server and the clients add up to significant latency.

To solve this, it’s ideal to introduce regional servers, that is, servers that are closer to clients. This limits the distance the packets have to travel and hence lowers the latency.

The downsides here are that running lots of servers is costly, and of course, if players want to play together from very distant locations, you have to choose one region for them to play on that might not be optimal for them.

Where players are a long distance from each other you have essentially two options:

  1. Pick a regional server that’s equally distant from both. This ensures they both get a similar network experience - although not optimal for either.
  2. Rely on backbone trunking. Connecting across the public internet can be slow for lots of reasons (e.g., poor cell/wifi, lots of network hops). The backbone fabric that your servers run on is often much faster. We can choose to have the players access a local regional server as an access point and then connect these regions via the faster backbone.

Load Balancing

Any scalable system needs to be able to add capacity through adding servers and this means load balancing. The split between matchmaking and real-time also changes how we do load balancing.

On the central server, the load balancing can act largely like a web application, that is, load balancing can be stateless and we rely on the database as the synchronization layer. If a player applies a change to their skin, the application can make a change, store it in the database and on the next invocation read it back from the database. There’s no running state that needs to be maintained between invocations.

However, on the real-time server, there will be several pieces of state:

  • The connections from the players themselves. As mentioned in WebRTC vs WebSocket, for high performance we want to establish and maintain a connection between clients and the real-time server. The connection must not be dropped between interactions with the server.
  • The server is running the authoritative part of the game, making sure that the players see the same state and don’t cheat. The server may also be running part of the game logic such as computer-controlled actors and in-game events. This needs to continue to run whether players are taking actions or not.

Since the real-time server is stateful, the load balancing needs to connect to the same server for all players in the same session. In an MMORPG this means the zones the players are allocated to a server and all players in those zones connect to that server. In Rune, this means that the room/game combination is allocated to a server in the same way.

More generically in load balancing, this is known as “sticky sessions.” When a connection is made to the load balancer an attribute/parameter of the connection is used to determine where the connection needs to go. This of course makes the load balancer that bit more complicated and often leads to custom load balancing solutions.

Database

For most online games, databases are key to maintaining the long running state and storing player profiles, levels etc. The central server uses the database to maintain state between invocations so it’s heavily used and operations often rely on results from the database. This behavior is common in web application architectures too and means being very careful with your database performance.

On the other hand, the real-time server use of a database cannot block operations. Real-time exchanges are measured in millisecond latency and any blocking based on a database is likely to degrade player experience. On the real-time server it’s generally preferred to avoid any database read access in the core network flows - instead pulling the data required at the start of the session and holding it in memory.

There are of course cases where actions in the real-time server where the database needs to be updated to reflect changes the player has made. Whenever possible keeping these interactions asynchronous is the best approach.

The matchmaking and real-time servers should have a separate database (or at least a different set of tables/schema) that they act on. This allows us to have different rules of engagement to the database in each case and to be able to tune the database in each scenario for its specific expected use.

The final point of database interaction is where the updates on the real-time server need to be passed back to the central server, or vice versa. Again, whenever possible this needs to be an asynchronous process so that it doesn’t interfere with the real-time server run-time operations.

Metrics

Final, but important, point: don’t forget metrics/observability. When scaling any system it’s key to be able to understand how the system is operating and even more how any change that you make affects the performance and stability.

Applying metrics after the fact is actually pretty hard, trying to instrument or interpret database everything once it’s all in place and the implementation has passed is time consuming and error prone.

When building a scalable system design and add the metrics to the features as they’re implemented. Having well-thought-out and intentional metrics is the only way to really tune and improve an architecture.

If you found this article interesting or have questions, drop by 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

  • More profile improvements like adding explainer text for the "Top Developer" badge and in-room profiles! 👀
  • 🌐 Added the ability to change your language and country directly in the new and improved profile screen!
  • Enhanced our expo update logic on Android, now it’ll download automatically without user feedback and wait to apply until the next app restart 🚀
  • Removed the Preview button from the avatar editor, as avatars now update lightning-fast when you choose new cosmetic options.
  • Upgraded scrolling performance in the profile interest picker and enable bouncing scroll everywhere so the app is more responsive 📜

🪲Bug Fixes

  • Fixed a visual bug where games with long names were overlapping the # of players section on the game details screen 👥
  • Rectified a visual issue where avatar customization options appeared square instead of round on Android 7.
  • Sorted out an unexpected audio switch from speaker to earpiece when a new roommate joins your room on iOS devices.
  • Busted two bugs where the splash screen was flashing white briefly beforehand and showing after they exited a room if they booted the app into an incoming call.
  • Lots of perfecting how the avatar background colors and profile screen look inside the app 🎨

💻 SDK Improvements

  • 🚀 Added ESLint and Vite plugins to the SDK package, simplifying code setup!
  • Spruced up our example game dev UI so it is more responsive to layout changes ✨
  • Introduced checks for newer SDK versions on dev/build commands, ensuring you all have the most up-to-date tools! ⚒️
  • Integrated updated avatar assets into the SDK, so many options for customization! 🛠
  • Updated the dev UI for better screen fit across all phones, especially in landscape mode.
  • Improved dev UI logic so now you can’t remove players from the UI while it’s loading.
  • Refurbished the SDK network detection logic to better handle CSS!
  • Fixed a possible state desync that was happening on player leaving.
  • Upgraded our error messages in the SDK to be clearer and introduced logging of error stack traces to enhance troubleshooting and debugging 📊
  • 🎯 Rebuilt the interpolators to independently maintain speed and targets for each axis, enhancing precision and control!
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

When building games as a coder, there’s one issue that prevents us from being motivated for a game more than anything else - art. Whether it’s conscious or not, if you're working on a project that you have to look at hundreds of times a day, it’s very hard to stay excited when it has programmer art. At Rune, I’m still building lots of games and tech demos, and each one is far easier to absorb and understand when the art is “good enough”.

The ideal case is finding an artist to work with who’s just as enamored with the game concept as you are. However, often artists have many options and the game needs to get to a certain level before they’ll engage.

If there’s one thing I tell anyone who asks about building hobby/indie games, it’s to get consistent, good enough art in place early. It’s motivating, it makes demos look better and it’s actually not that hard.

General Tips for Assets

Look for asset sets not one offs. It’s easy to find one sprite and just think that’s the one for your game. However, more important than great art is consistent art. If you can find a set that’s big enough to keep you fed for a while, that's better than the greatest single piece of art.

  • Look for simple art styles that you think you could extend. I’m a terrible artist but given a pattern to follow I can sometimes extend sets to cover what I need for my games. If you can understand how the art was built (lighting, colors etc) you’re in a better place to extend.
  • Check the license! First, be sure you understand what the license means to your project:
    • Can you continue using this art commercially?
    • Are you even allowed to redistribute the art in an open source repository?
    • Is extending the art allowed?
  • Check the ownership! Unfortunately the game asset world is littered with people stealing art and reselling it as their own. The best bet is to try and find the vendor’s actual website and evaluate it from there rather than on one of the market sites where everyone looks the same.

Here’s some of my favorite sources/approaches to free/low-cost art to bootstrap your project and keep you motivated. (I should note I’m in no way affiliated with any of these sites)

Graphics

Stylised / Geometry

The most common approach that developers take is using “stylised” graphics - pure geometry with some nice shader or shadows. Trust me this is one of the hardest things to make look good and requires a really good artist with understanding of color and dynamics. You might get lucky and just hit something that looks great but there are better options.

Kenney.nl

Kenney has been producing free game art for over a decade, and frankly he’s a bit of a hero of mine. He produces 2D, 3D and UI art and it’s all insanely good quality. What’s more it’s all released as CC0 - meaning you can use it however you choose even commercially.

https://kenney.nl/

Oryx Design Lab

Oryx (who actually works for Bungie) started out by building a very simple sprite set for a game jam which resulted in the creation of Realm of the Mad God. It’s all 2D art and pretty low cost for what you’re getting. There are also sound effects available.

https://www.oryxdesignlab.com/

Kay Lousberg

Kay is a more recent arrival on the game asset scene but has definitely been taking a page out of the book of Kenney. The art is higher polycount but it pays off if you can stand it in your engine. The characters are nothing short of amazing and again low cost or patreon based.

https://kaylousberg.com/game-assets/

Quaternius

Quaternius was a new find for me thanks to the Rune game Cooking Frenzy. Really great low-poly 3D art and game ready.

https://quaternius.com/

Lost Garden

Daniel Cook has been working to produce game art and amazing games like Triple Town for a long time. Lost Garden is one of the first places I found game art that really worked for me.

https://lostgarden.com/tag/art/

itch.io

By now everyone knows about itch.io for both games and games assets. To say the amount of content is large is an understatement. Itch however can have several issues:

There are a lot of content authors who have just taken someone else’s assets and claimed them as their own. As mentioned above, Check the License! There are lots of individual resources but not many complete sets. There are some great authors producing large consistent sets but you need to find them. The quality is wide ranging, some of the art looks good but when trying to include the content it’s simply not game ready.

Some of my favorites from itch are Minifantasy (built an MMORPG from this one), Pixel Frog and Doodle Rogue.

CraftPix

CraftPix is a great collection of free and low cost art. The real wins here is the licensing is very clear and the assets are grouped together into consistent art styles. Well worth a look.

https://craftpix.net/

Open Game Art

Open Game Art collates a massive collection of old, retired and one off game graphics and sounds. The quality can be quite low but there are some gems hidden if you search carefully. Be careful with the license and ownership here, there are a lot of art resources added that don’t have permission.

https://opengameart.org/

Graphics River

Graphics River is a general art site for designers but has a section for game assets which is full of great resources. The quality can be extremely high but the price also reflects this making it the more expensive option of resources I choose. There are complete game kits that contain all the art you might need for specific types of games.

https://graphicriver.net/game-assets

On AI

An option becoming ever more popular is using AI to generate game assets. For me at least I find it great for generating single images, for instance I use it for social previews for blog articles. However when it comes to creating solid, consistent sets of arts across a whole game it doesn’t really work out. AI is great, but what you need is a real artist.

Sounds

Free Sound

Free Sound is the original open source sound effects library. It’s great for finding real atmospheric sounds but not so great for a set of consistent sound effects. Worth a look.

https://freesound.org/

Sound Snap

Sound Snap is a great source for free and low cost sound effects. The pricing model is slightly hard to work with but the quality of the resources makes it worth your time.

https://www.soundsnap.com/

Pixabay

Pixabay is another great source for free sound effects with clear licensing and a great search engine. This is my go-to site for game sound effects.

https://pixabay.com/sound-effects/

SFXR

If you’re more in the mood for creating your own sound effects you can’t go far wrong with SXFR. It’s a simple tool with dials and settings you can play with (or randomize) to get interesting new sounds for your games. Be warned, it’s a lot of fun and can be a time sink!

https://sfxr.me/

Eleven Labs

It’s really nice to have natural sounding voices in your game, and Eleven Labs is the low-cost way of doing this. The output is shockingly realistic and the tuning parameters let you create just the voice for your giant bandit frog.

https://elevenlabs.io/

Browser voice

If you want voices but you either can’t afford a commercial solution or you don’t want to ship huge sound files for all the audio in your game there is always the option of Web API Speech Synthesis.

The voices available are platform dependent and there's normally only a couple that actually sound good enough. However, for some games that's enough.

https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis

Do you know more sources or have experience with finding great art/sounds? Let us know 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!

· 2 min read
Kevin Glass

Hey everyone! The moment we've all been impatiently waiting for is finally here! Rune’s 1st Jam has officially started and will run from today, August 1st 00:00 UTC, until August 11th 23:59 UTC.

The theme we’ve chosen for the occasion is... Creativity! 🧑‍🎨

This summer we want to help players unleash all of their imagination with the theme of "Creativity". Be it through playing in unique scenarios, experimenting with unconventional mechanics, giving life to mythical creatures, or interacting through drawing and images.

Focus on the aesthetics of the visuals and build a world of colors, shapes, and original designs: players should breathe creativity as soon as they step into the game 🎨 🏞️ Ensure that the game-play involves building, crafting, molding, to trigger the players’ creativity and stimulate their inventiveness 🏗️ 🪵 🧲 🚧

Seeking inspiration? Exploring these games might help getting you started:

Look at the above titles as simple suggestions to make your mind take off, rather than constraints for your ideas: this time we chose a fairly open theme because we want your creation to embody the spirit of artistic freedom and inventive game-play 💭

No matter your level of experience: your participation and your unique ideas are what is going to take to make Rune's 1st Jam unforgettable 🥳

Tech-demos and other open-source games can be used as the starting point for your jam submission! 🙂

We wish all of you the best luck ad we cannot wait to see the amazing games you'll come up with! Happy Jamming everyone! 😊 🙏

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!

· 6 min read
Kevin Glass

At Rune, we’ve got a platform where millions of users are now playing casual multiplayer games. As I blogged about previously, we care a lot about making the multiplayer networking excellent. No matter which model you’re using, making the best use of the available internet transport is key.

When it comes to real-time games, the majority of modern releases use UDP rather than TCP. The web, however, for a long time only had access to TCP (via HTTP) and developers have found novel ways to make use of the reliable stream to build real-time games. However, the reason most real-time games outside of the web use UDP is its unreliable nature.

Why is unreliable good?

Real-time games rely on prompt message delivery, or your ‘ping’ from a gamer’s point of view. With TCP and reliable delivery, if one packet gets delayed so does everything sent afterward. With UDP and unreliable delivery, packets aren’t dependent on each other so delays don’t compound. This does leave the developer working out which packets/data need to be reliable and how to ensure it, but the finer level of control gives options for a better game experience.

As I said above, the web used to be TCP only, but with the advent of WebRTC, and a few years after its initial release, we were given RTC Data Channel which, while not pure UDP, can act like it.

What are WebSockets?

WebSockets have been with us since about 2008. They were the first game development-usable two-way communication available in the browser. Before this, we had server-side pushes and some polling technologies, but the response time was high.

When a browser retrieves pages, it connects out to the server, requests the page, and gets a response. Browsers generally reuse their connections, so the TCP connection was staying open to the server for subsequent requests. The server, however, can’t send anything with HTTP unless a request has been received that it's going to respond to.

Enter WebSockets and the 101 Upgrade / Switching Protocols messages. Since the TCP connection is staying up anyway, can’t we just leave it there but let the server and client exchange data (or frames) as and when they want?

Now we have two-way communication between the client and server, and this works well for certain types of games. However, the connection is still TCP, which isn’t great as I’ll describe in more detail below.

The great thing about WebSockets is how easy it is to implement on both client and server. There are a wide range of libraries easing the development of WebSockets, most notably Socket.io, and the amazing availability of cheap hosting for them.

What is WebRTC and Data Channel?

WebRTC is a technology to allow voice and video communications directly in the browser. It arrived around 2011, got formalized, and was widely supported by 2015. As part of the specification, the RTC Data Channel was added. Data Channel provides a real-time method to send raw data between browsers and servers, or “peers” in WebRTC language.

Since WebRTC originated from the voice/video world, when they added data channels, they used a telecoms-based protocol called SCTP. Luckily, SCTP is based on UDP and it can be configured to act like raw UDP, i.e., send a packet once when I tell you to.

WebRTC is considerably more complicated to work with than WebSockets. The initial setup of the data channel requires the peers to exchange signaling information (SDP, a text payload). This means you’ll need some other transport (normally WebSocket) to pass the signaling before you can even get a channel set up.

Since the final connection is using UDP, network traversal can also be more complicated. Again, WebRTC used the telecom standards for determining the best path through a network to connect peers using STUN, ICE, and TURN.

  • STUN to determine your own address
  • ICE to describe possible ways to connect
  • TURN as a relay when other connection methods can’t work.

The infrastructure you need to run WebRTC is more intricate although there are now free and inexpensive cloud services available.

Library support is also limited with respect to WebRTC, with the majority of the few libraries available focusing on the voice/video side of the API rather than the data channel that we’d like for game use. There are some great examples like node-datachannel that are becoming mature enough to use in production services along with great cloud services like livekit.io.

Head of Line Blocking

So, given that WebRTC is harder to use and develop with, why would we use it?

As mentioned above, there’s a significant difference between TCP and UDP that matters for real-time games, Head of Line Blocking.

TCP attempts to fully manage the connection between endpoints for you. This includes resending packets for reliability if the far end has not acknowledged them, windowing to make sure we’re not attempting to send “too much” and congestion control to detect when the network can’t handle what's waiting. This is great for general connections - the internet is a difficult place to work in and managing the connection like this takes a lot of work off the developer.

However, for real-time games, every millisecond counts and having TCP ensuring order and delivery for every packet often gets in the way. As the Gaffer article above describes, if one packet on a TCP connection doesn’t get through then every other packet is now waiting for the first’s delivery and the developer can’t access any of the data (that may have already arrived) until that first packet completes its journey.

In short, if you’re wanting to do real-time communications, you need UDP, and in the current web browser, that means WebRTC.

Is it worth the work? Yes.

The Future

Of course, with all things web, there’s a new standard coming that might shake things up again, WebTransport. This new standard looks like it’s going to give us game developers everything we might want.

Unfortunately, at the time of writing, the standard is still changing, browsers aren’t quite there yet, and the server-side support is very limited.

I’m still holding out hope that WebTransport is the answer, but what do you think?

If you’re interested in this topic or the Rune platform, drop by our Discord and let us know.

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!

· 10 min read
Kevin Glass

In the second tech-demo, we look at making a platform game multiplayer using the Rune SDK. For anyone following along the base code is the same as the last technical demo, but we'll cover most of it in this article.

You can give it a go on the tech demos page.

First a re-cap of the architecture of a Rune game. We separate the rendering and logic like so:

It’s good practice to separate your data model and logic from the rendering, i.e. the MVC pattern. With 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 contain the data that is required to update the game and how winning/losing can be evaluated - i.e. the game state. We want to try and keep it fast since in the predict-rollback network model (that Rune uses) we will be running multiple copies of the logic. The logic is implemented to maintain determinism and to allow it to be executed both on the browser and server.

The renderer, or client, is the code the renders to the game for the player and accept their input. The client can be implemented using any library or framework that can run in the browser.

Let's get to the code. If you need directions on creating a game project they're in the docs. In this demo we’re going to have a map and some players. So first let's declare some types to describe those:

// the extra data for the player
export type Player = {
x: number
y: number
sprite: string
playerId?: PlayerId
// 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
}

// 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
jump: boolean
}

For Rune to synchronize the clients we'll need to define the shared data, 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 {
players: Player[]
}

Next we can initialize the logic for the game which all clients will start from before applying changes they receive from clients:

Rune.initLogic({
setup: (allPlayerIds) => {
const initialState: GameState = {
// 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: 20 + (index + 1) * 32,
y: 260,
playerId: p,
type: "PLAYER",
sprite: PLAYER_TYPES[index % PLAYER_TYPES.length],
animation: Animation.IDLE,
controls: {
left: false,
right: false,
jump: false,
},
flipped: false,
vx: 0,
vy: 0,
}
}),
}

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 player of game.players) {
player.animation = Animation.IDLE

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 {
if (player.vx < 0) {
player.vx = Math.max(0, player.vx + MOVE_ACCEL)
} else if (player.vx > 0) {
player.vx = Math.min(0, player.vx - MOVE_ACCEL)
}
}

player.vy += GRAVITY
...

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 left/right.jump. We're going to have two sets of collision, one against a tile map for the level and another between players. This lets players use each other as platforms!

The collision code is brute force, look for tiles that we might be colliding with and then check rectangle/rectangle collision for the players. You can see that in isValidPosition.

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.

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 player = game.players.find((p) => p.playerId === playerId)

if (player) {
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 an 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 the 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
// our 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 the game state
for (const player of gameState.players) {
const frames =
player.animation === Animation.JUMP
? playerArt[player.sprite].jump
: player.animation === Animation.WALK
? playerArt[player.sprite].run
: playerArt[player.sprite].idle

drawTile(
player.x - 16,
player.y - 16,
frames,
Math.floor(Date.now() / 50) % frames.tilesAcross,
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.jump !== lastSentControls.jump)
) {
lastSentControls = { ...gameInputs }
lastActionTime = Date.now()
Rune.actions.controls(lastSentControls)
}

There’s a couple of conditions put on sending actions. We don’t want to send unchanged controls into the game logic, it won’t change anything. The Rune SDK also ensures we send a maximum of 10 actions per second from any client to prevent swamping the network.

That’s 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.

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!

· 6 min read
Kevin Glass

I’ve written a few multiplayer games and seen patterns in the things I got wrong across projects. As part of my work here at Rune I’m getting to see more and more developers making network games and there’s definitely some common themes in the problems they face.

When you write a lot of single player games it’s easy to get into habits that make game development faster. For instance, knowing that your data model is as fast to update and as your rendering is a real bonus when trying to move quickly from concept to MVP. Some of these habits can make multiplayer game development harder, so here’s a few things to think about when you’re building your next hit game.

Players Leaving and Joining

In a session/room based game players can leave and join at will. This might be due to active choices, connection issues or being removed from the game. For each player we’re normally holding state - that could be their position, the items they're holding or their score.

What happens when a player leaves the game? The state could just be deleted and the other players carry on without them. There’s room for interesting game design here though, where a leaving player gives their state/progress to other players. Maybe the player needs to leave a marker where they were. Even more, what if a leaving player has a detrimental effect on others - encouraging people to keep playing? Do we need to adjust the difficulty of the game based on the number of players?

What happens when a player joins the game mid-way through? They could become a spectator of the game, they could jump in with the other players at their current point. Is there any bonus for players joining? Again, consider if we need to scale the difficulty of the game based on the new player count.

What happens if a player leaves and then re-joins quickly? This might mean they just lost connection, so consider a grace period. Rejoining players often get their state reset, check if this can be used as an exploit. If there is a way to gain through rejoining, players will find it.

Determinism

As described last week, determinism in multiplayer games is often key to the network model. It’s quite easy to make a deterministic data model that ends up being non-deterministic on the rendering side. Pay special attention to how the renderer converts the inputs given from the network data model - a common issue is running animations independently from the synchronized data model resulting in different player interpretations of whats happened.

Be intentional when making effects take place on the client side that aren’t driven by shared data.

Player Interactions and Rollbacks

Interactions between players are always hard in networked games, of course it’s where the fun is too. Thinking about interactions up front is critical to making a game “feel” right. These interactions generally fall into the following categories from easiest to hardest to get right:

  1. Data exchange - like swapping or sharing objects and consumables. In these cases there’s nothing real time visual to see (other than stats or inventories updating) so delay doesn’t feel bad. These are an easy win to make a game feel multiplayer.
  2. Out of band effects - like power-ups that affect other players. We see these in popular carting games, you collect an item, use it and all the other players get their screen blacked out or become mini versions etc. The nice thing about these is that they can be delayed slightly from the player input without it seeming wrong - meaning that latency can be absorbed.
  3. Ranged Interactions - like shooting an opponent. These are still latency and rollback sensitive but visually the rollback won’t look so bad because it’s so far away. If you appear to hit but actually miss the player doesn’t feel quite so cheated.
  4. Close Interactions - like two players pushing each other around. These are by far the hardest to get right, which is why so many multiplayer games ignore them. The latency really hurts here. e.g. the remote player stops moving and you get a rollback causing a sudden movement in the shared object. They can be mitigated with slow moving objects and by using acceleration in the player.

Categorizing your interactions early on can help you both choose a networking model and de-scope features that will make your game either too hard to implement or unsatisfying as an experience.

What to Synchronize

In contrast to Determinism above, sometimes the first stop of a developer working on a multiplayer game is to synchronize everything. I know I fall into this trap regularly. At the least it means you’re sending more across the wire than you need to but at worst you can be causing rollbacks for things that don’t matter.

I repeat, be intentional when making effects take place on the client side that aren’t driven by shared data.

Player Base Size

One of the hardest things as an indie game developer is getting from your early access build to something popular enough to have regular players. This means when you’re designing your multiplayer experience for large groups you need to keep in mind in the early days it’ll likely be much fewer, or even solo play.

Even if it's temporary, having a design that's flexible enough to make it fun for solo play and then grow into multiplayer is great. Once your game is off and running you can throw the solo aspects away if needed.

Equally knowing the upper end of the players supported by your design is important. Designing for small groups brings different complexities than designing for massively multiplayer numbers. Of course depending on the game you might end up with some aspects for each.

Real Life Internet

As mentioned in game networking models, the real internet isn’t always smooth sailing. High latency is pretty common as is varying latency on a single connection. Try to think about how the game will feel with players on significantly different network connections. Accessibility-wise you want to get as many players on board as possible, so this may well mean that not all connections are equal.

I’ve seen a couple of games out there that have a lag bonus - if your connection was poor or unstable you get an easier ride in the game. With the right tuning this feels like it could be a good solution.

There are probably many other gotchas and areas to think about when designing multiplayer games, these are just a few that have come up for me. If you have other areas you think should be mentioned or want to discuss any of the above be sure to visit 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!

· 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!