Skip to main content

19 posts tagged with "Game Development"

View All Tags

· 9 min read
Kevin Glass

At Rune, we want you to be able to use game development tools that you love with our platform. With this in mind, we’ve adapted the tutorial game from the popular framework Phaser to be multiplayer on Rune.

Approach

Phaser is wonderfully powerful as a game library, and one of its key concepts is putting everything into the scene graph. This is fantastic for a single player game since the physics/collision can happen on the client side where the scene graph lives. However, when you approach multiplayer (with any framework) the game needs to be able to run its physics both on clients and validating server. With this in mind in this tech demo we’ll move the physics into the logic of the game and use a separate library to manage it.

Outside of this the Phaser framework can be used as normal.

Client Side

To anyone who's used Phaser before this will look pretty familiar. For those who haven't this is setting up a Phaser runtime and renderer and loading the assets that will be used to render the game:

export default class TutorialGame extends Phaser.Scene {
preload() {
// preload our assets with phaser
this.load.image("sky", "assets/sky.png")
this.load.image("ground", "assets/platform.png")
this.load.image("star", "assets/star.png")
this.load.image("bomb", "assets/bomb.png")
this.load.spritesheet("dude", "assets/dude.png", {
frameWidth: 32,
frameHeight: 48,
})
}
}

const config = {
type: Phaser.AUTO,
width: window.innerWidth,
height: window.innerHeight,
scene: TutorialGame,
scale: {
mode: Phaser.Scale.ScaleModes.FIT,
},
}

new Phaser.Game(config)

Here's the first difference to a normal Phaser application. Since we're going to be using Phaser for the rendering only (the physics will be happening in the game logic) we're going to add a mapping table that will convert physics object on the server to the client side scene graph elements:

  physicsToPhaser: Record<number, Phaser.GameObjects.Sprite> = {}
lastSentControls: Controls = {
left: false,
right: false,
up: false,
}

You can also see lastSentControls above. Since Phaser is providing the input from the player and we need to send that to the logic, we'll record the controls we sent last time. We want to avoid sending the controls more often than needed to avoid wasted networking communications by making sure we only send the inputs when they change.

Next up we have the Rune integration. We initialize the Rune SDK with a call back function that tells us when game state is changing. In this case this means when our physics objects have been created, updated or deleted. When we get this notification, we're going to scan through the state and update the Phaser rendering to match. First, we locate each physics body in the phaser world:

 // for all the bodies in the game, make sure the visual representation
// exists and is synchornized with the physics running in the game logic
for (const body of physics.allBodies(game.world)) {
const rect = body.shapes[0] as physics.Rectangle

const x = Math.ceil(
(body.center.x / PHYSICS_WIDTH) * window.innerWidth
)
const y = Math.ceil(
(body.center.y / PHYSICS_HEIGHT) * window.innerHeight
)
const width = Math.ceil(
(rect.width / PHYSICS_WIDTH) * window.innerWidth
)
const height = Math.ceil(
(rect.height / PHYSICS_HEIGHT) * window.innerHeight
)

let sprite = this.physicsToPhaser[body.id]

If we don't have a sprite for the body yet, we create the right one based on the type of body we've been given:

// if a sprite isn't already created, create one based on the type
// of body
if (!sprite) {
if (body.data && body.data.star) {
const size = Math.ceil(
(rect.bounds / PHYSICS_WIDTH) * window.innerWidth
)
sprite = this.physicsToPhaser[body.id] = this.add
.sprite(x, y, "star")
.setDisplaySize(size * 2, size * 2)
} else if (body.data && body.data.player) {
// create the player and associated animations
sprite = this.physicsToPhaser[body.id] = this.add
.sprite(x, y, "dude")
.setDisplaySize(width, height)

this.anims.create({
key: "left",
frames: this.anims.generateFrameNumbers("dude", {
start: 0,
end: 3,
}),
frameRate: 10,
repeat: -1,
})

this.anims.create({
key: "turn",
frames: [{ key: "dude", frame: 4 }],
frameRate: 20,
})

this.anims.create({
key: "right",
frames: this.anims.generateFrameNumbers("dude", {
start: 5,
end: 8,
}),
frameRate: 10,
repeat: -1,
})
} else {
sprite = this.physicsToPhaser[body.id] = this.add
.sprite(x, y, "ground")
.setDisplaySize(width, height)
}
}

Finally, once the sprite is definitely in the world we update it to match the body position based on what the logic has given us:

// update the sprites position and if its a player the animation
sprite.x = x
sprite.y = y
if (body.data?.player) {
const controls = game.controls[body.data?.playerId ?? ""]
if (controls) {
if (controls.left) {
sprite.anims.play("left", true)
} else if (controls.right) {
sprite.anims.play("right", true)
} else {
sprite.anims.play("turn", true)
}
}
}

The final step is pass the input from the phaser side into the logic so we can update the physics model. First we record the input, we have on screen controls which we can listen to:

const left = document.getElementById("left") as HTMLImageElement
const right = document.getElementById("right") as HTMLImageElement
const jump = document.getElementById("jump") as HTMLImageElement

left.addEventListener("touchstart", () => {
gameInputs.left = true
})
right.addEventListener("touchstart", () => {
gameInputs.right = true
})
left.addEventListener("touchend", () => {
gameInputs.left = false
})
right.addEventListener("touchend", () => {
gameInputs.right = false
})
jump.addEventListener("touchstart", () => {
gameInputs.up = true
})
jump.addEventListener("touchend", () => {
gameInputs.up = false
})

Then in the Phaser update if the inputs have changed, we pass them to our logic through a Rune action:

update() {
// As with the physics we don't want the controls to be processed directly in the
// the client code. Instead we want to schedule an action immediately that will update
// the game logic (and in turn the physics engine) with the new state of the player's
// controls.
const stateLeft = gameInputs.left
const stateRight = gameInputs.right
const stateUp = gameInputs.up

if (
this.lastSentControls.left !== stateLeft ||
this.lastSentControls.right !== stateRight ||
this.lastSentControls.up !== stateUp
) {
this.lastSentControls = {
left: stateLeft,
right: stateRight,
up: stateUp,
}
Rune.actions.controls(this.lastSentControls)
}
}

And that's our client done!

Logic Side

On the logic side, we're going to maintain a propel-js physics models that represents our world in the game state. We'll update this each loop and that state will be passed back to the Phaser client to render.

First, we'll setup some game state containing the physical world and state of each players controls, essentially what we need to update the world.

export const PHYSICS_WIDTH = 480
export const PHYSICS_HEIGHT = 800

export interface GameState {
world: physics.World
controls: Record<PlayerId, Controls>
}

export type Controls = {
left: boolean
right: boolean
up: boolean
}

type GameActions = {
controls: (controls: Controls) => void
}

declare global {
const Rune: RuneClient<GameState, GameActions>
}

Next we'll initialize the Rune SDK and configure the world to have our players, platforms and stars:

Rune.initLogic({
minPlayers: 1,
maxPlayers: 4,
setup: (allPlayerIds) => {
const initialState: GameState = {
world: physics.createWorld({ x: 0, y: 800 }),
controls: {},
}

// phasers setup world but in propel-js physics
physics.addBody(
initialState.world,
physics.createRectangle(
initialState.world,
{ x: 0 * PHYSICS_WIDTH, y: 0.2 * PHYSICS_HEIGHT },
0.5 * PHYSICS_WIDTH,
0.05 * PHYSICS_HEIGHT,
0,
1,
1
)
)
physics.addBody(
initialState.world,
physics.createRectangle(
initialState.world,
{ x: 0.75 * PHYSICS_WIDTH, y: 0.4 * PHYSICS_HEIGHT },
0.5 * PHYSICS_WIDTH,
0.05 * PHYSICS_HEIGHT,
0,
1,
1
)
)
physics.addBody(
initialState.world,
physics.createRectangle(
initialState.world,
{ x: 0.5 * PHYSICS_WIDTH, y: 0.6 * PHYSICS_HEIGHT },
0.5 * PHYSICS_WIDTH,
0.05 * PHYSICS_HEIGHT,
0,
1,
1
)
)
physics.addBody(
initialState.world,
physics.createRectangle(
initialState.world,
{ x: 0.5 * PHYSICS_WIDTH, y: 0.9 * PHYSICS_HEIGHT },
1 * PHYSICS_WIDTH,
0.3 * PHYSICS_HEIGHT,
0,
1,
1
)
)

// create a player body for each player in the game
for (const playerId of allPlayerIds) {
const rect = physics.createRectangleShape(
initialState.world,
{ x: 0.5 * PHYSICS_WIDTH, y: 0.5 * PHYSICS_HEIGHT },
0.1 * PHYSICS_WIDTH,
0.1 * PHYSICS_HEIGHT
)
const footSensor = physics.createRectangleShape(
initialState.world,
{ x: 0.5 * PHYSICS_WIDTH, y: 0.55 * PHYSICS_HEIGHT },
0.05 * PHYSICS_WIDTH,
0.005 * PHYSICS_HEIGHT,
0,
true
)
const player = physics.createRigidBody(
initialState.world,
{ x: 0.5 * PHYSICS_WIDTH, y: 0.5 * PHYSICS_HEIGHT },
1,
0,
0,
[rect, footSensor]
) as physics.DynamicRigidBody
player.fixedRotation = true
player.data = { player: true, playerId }
physics.addBody(initialState.world, player)

initialState.controls[playerId] = {
left: false,
right: false,
up: false,
}
}

// create a few stars to play with
for (let i = 0; i < 5; i++) {
const rect = physics.createCircleShape(
initialState.world,
{ x: i * 0.2 * PHYSICS_WIDTH, y: 0.15 * PHYSICS_HEIGHT },
0.04 * PHYSICS_WIDTH
)
const star = physics.createRigidBody(
initialState.world,
{ x: i * 0.2 * PHYSICS_WIDTH, y: 0.15 * PHYSICS_HEIGHT },
10,
1,
1,
[rect],
{ star: true }
) as physics.DynamicRigidBody
physics.addBody(initialState.world, star)
}

return initialState
}

As seen above, for each body, we set user data indicating the type of body it should be rendered as. This game state will immediately be sent back to our client, which will create sprites in the Phaser scene graph and position them accordingly..

Next, we need to process the input action we provided from the client. This is as simple updating our game state to know which controls a player is pressing:

actions: {
controls: (controls, { game, playerId }) => {
game.controls[playerId] = controls
},
},

The final step of our update loop is update the physics model based on the controls provided from the player clients:

update: ({ game, allPlayerIds }) => {
// each loop process the player inputs and adjust velocities of bodies accordingly
for (const playerId of allPlayerIds) {
const body = game.world.dynamicBodies.find(
(b) => b.data?.playerId === playerId
)
if (body) {
if (game.controls[playerId].left && !game.controls[playerId].right) {
body.velocity.x = -100
} else if (
game.controls[playerId].right &&
!game.controls[playerId].left
) {
body.velocity.x = 100
} else {
body.velocity.x = 0
}

// check if we're on the ground
if (body.shapes[1].sensorColliding) {
if (game.controls[playerId].up) {
body.velocity.y = -600
}
}
} else {
console.log("Body not found")
}
}

// propel-js likes a 60fps game loop since it keeps the iterations high so run it
// twice since the game logic is configured to run at 30fps
physics.worldStep(60, game.world)
physics.worldStep(60, game.world)
}

Above we can see that we apply velocities directly to the bodies in propel-js based on the controls the player have provided. We're also using a foot sensor to determine if the player is on the ground and hence if they can jump. One other note here is a nuance of propel-js, our game logic is running at 30fps but the physics model works best at 60fps so we simply run two updates.

There you have it, a multiplayer version of the Phaser sample with the Rune SDK. It takes a little bit of rethinking of the model but we can make use of a lot of power of Phaser!

Want to know more? Why not drop by the Discord and have a chat?

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!

· 3 min read
Kevin Glass

When you’re building any client software, it’s useful to know what types of hardware your end users have – doubly so with experience-centric software like games. For the types of games I like to build, even the lowest-end desktop hardware has been more than enough for many years. However, for mobile game platforms like Rune, the types and power of devices that end users have can dramatically affect the player experience.

In this article, we’ll look at the types of devices and their capabilities found in the Rune user base. We’ve taken the top 30 most popular mobile devices on Rune (accounting for about 2 million users) and broken the data down by processor, GPU, screen size, memory, and release date.. As you can see below, in mobile game development, there’s still a huge range of capabilities to account for.

Screen Size

The graph above shows the screen resolutions in device-independent pixels. There’s a huge variety of screen sizes in use, going all the way down to significant numbers (40k) of users with screens as low as 375x667. Likewise, the top end has over 50k users with 2399x4973 screens. Responsive design is key.

Memory

The spread of onboard memory is also wide, going as low as a single GB. The top end is quite low compared to very modern devices, maxing out at 8 GB. This, of course, is only in the top 30 devices in a much bigger user base, but it gives you an idea of what the games need to run on.

Processor and Graphics

The following charts show the spread of CPUs and GPUs on the devices playing your games today.

Processor

Graphics

Analyzing the graphs above, we can see there are essentially two types of devices being used:

  • Octa-core CPU, Mali/PowerVR GPU class devices. These are reasonably powerful and will cope with most Rune games very well.
  • Quad-core CPU, Adreno GPU class devices. These are the budget devices that we see so many in Gen Z having due to the lower cost. These are the ones that you need to target to get maximum playtime for your games.

Release Year

One final piece of information: the release year of the devices in the top 30.

This explains the other data: the majority of devices in the top 30 are 4–5 years old.

Hopefully, the data here will help focus and tailor your game development efforts to get the maximum playtime from our player base.

Does this align with what you’ve seen? Want to know more? Why not drop by the Discord and have a chat?

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!

· 4 min read
Kevin Glass

At Rune, the majority of the games on the platform are multiplayer. This is largely because we provide an SDK that enables JavaScript developers to build multiplayer experiences very easily, and our player base has come to expect it. Of course, as mentioned in Modern Game Networking Models, this means we focus on making the backend networking something special.

There are a lot of ways of making games multiplayer, from hot seat to shared screen and of course networking itself. Even in networking, there are multiple models to choose from each of which is suitable for a different type of game or programming complexity.

If you’re building a network layer for a single game or a bunch of very similar games then choosing the network model that’s the easiest and satisfies those game constraints is the best move.

However, at Rune, we’re pretty opinionated about a single model that works for all cases, predict-rollback. We need to provide a single common framework for all the games on Rune and so we focus on one networking model that supports the massive variety of games on the platform.

Predict-Rollback

In Modern Game Networking Models we talked in a bit of detail about how predict-rollback works. In summary, we essentially let all clients continue moving forward predicting the current game state based on the inputs they know about. If another client provides a new input (via the authoritative server) that occurs before the game time the current client is at, we roll-back the game state, apply the input, and then re-predict the current state.

So why do we think predict-rollback is the future of networking games and the best fit for a generic networking framework?

  • Some great games have used it to provide excellent multiplayer experiences, like Rocket League and Street Fighter. They also do an amazing job of hiding the rollback/changes when they occur.
  • It works for all cases, whether it's turn-based, RTS, or faster-paced twitch games; predict-rollback provides a stable, consistent approach. Even in turn-based games, where there should be no rollbacks, the simple simulation modified by inputs approach still fits the bill.
  • There’s growing library and platform support. Unity, Godot, and even Valve’s Source engines all have plugins that support this model.

What’s so great about the model then?

  • Low bandwidth—you only need to send the initial state and changes to that state. That’s pretty powerful right there. The variance in networks especially with the emerging nations becoming a huge consumer of games means this is super important.
  • Best player experience—in many cases, it means that clients can run forward without latency between player input and response. Of course, you need to deal with conflicts when they occur, but this seems to be much easier than the alternatives.
  • Most consistent implementation—once you’ve got determinism handled, it’s the most consistent approach across platforms and devices.. Every device acts the same and gets the same results.

What are the downsides? The process of rolling back and re-calculating the game state can be CPU heavy. Depending on your approach you may have to calculate many frames of change quickly based on the new input. However, this is why it’s now the right choice. Devices have reached a point where CPUs are extremely overpowered for what they’re trying to achieve in games - so there’s room to have a smart and utility based network model.

Of course, if you’re building a network model for a specific game, there are many tricks and game-specific approaches you could take.

If, however, you're building a library/framework that supports many types of games in many different environments and on different devices, predict-rollback is the right choice for now and the future.

Want to learn more about our approach or simply want to discuss the content of this article? Stop by our Discord and let’s chat!

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

Hey everyone! 🎉

The energy around our very first Rune Jam has been so incredibly positive, welcoming and and uplifting, and we’re thrilled to finally announce the winners of this creative extravaganza!

For 10 whole days, you devs crafted away, the theme of "Creativity," and the results were nothing short of spectacular. Now, let's celebrate the winners who captivated gamers the most:

🥇 First Place: Tonai with Scribble - 14,630 minutes

  • Our top game is writing home with a Rune merch bundle featuring a stylish t-shirt, a cool hat, and a handy water bottle.

🥈 Second Place: PixelPincher with Chillville - 10,372 minutes

  • A fantastic hangout game deserving of a Rune t-shirt and hat.

🥉 Third Place: JumpArtifact with Triangle Artistry - 1,730 minutes

  • Rounding out our top three 📐 with a well-earned Rune t-shirt.

We cannot thank everyone enough for making Rune's 1st Jam an unforgettable event. Any game that didn't make the top 3 leader board still will receive some awesome Rune stickers as a token of our appreciate for your efforts and creativity.

While we celebrate the winning games, we truly believe that the Rune gamers were the real winners here. Every single one of these games were a ton of fun and the playtime hours are definitely a testament to that!

Stay tuned for more events like this, and keep pushing the boundaries of creativity! Happy developing, everyone! 🌟

Want to find out more? Why not stop by our Discord and let’s chat!

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

At Rune, we’ve got a platform that lets developers get their games out to millions of players on mobile devices across the world. With that number of players, you can imagine the device range is also wide. While Rune abstracts a lot of this complexity away, there are still a few things developers need to consider.

Cross-Platform Design

The most obvious area is the design of the visuals and fitting it to the various screen sizes out there. The range of screen sizes is shown below along with the different approaches you can take to deal with them.

Tiny UI

Probably the easiest approach is to simply make your UI tiny or limited in such a way that it’ll fit on any screen. This is often the most effective game visual design approach since it also encourages you to limit what's going on in terms of user interface - which leads to less reading, which is something the audience wants.

If you design for 360x600, you’re likely to be fine in 99% of cases. However, this does limit your creativity to this reasonably confined space. It also means players with larger screens won’t feel the benefit.

Responsive

With responsive design, the developer creates a UI that responds to the screen size available. This is very common in web and app design, but not so much in game design. A responsive design takes some thinking about; elements of the UI and game interface are described in terms of their proportion to each other. So, maybe the logo is 25% of the height of the screen and the start button is 10% – no matter the size of the screen, those elements will fill 30% of it.

For games, this can be hard since the elements of a game UI are often extremely richly styled – themed to fit the game. Unlike traditional web and mobile applications where the UI is reasonably simple, in an RPG the UI might be built out of intricate gold edging with scrolls filling in the background with complex textures. Making this scale up and down for different screen sizes automatically can be difficult.

Custom

For those developers with the time and inclination, we have the custom approach, and surprisingly, where many commercial games end up. The developer creates a code base that has different layouts and assets for different scenarios. Most common is the tablet/mobile split, where depending on the device, the UI is significantly different. However, this same approach can be applied to pure phone sizes by categorizing them:

  • nHD - around 640x360 pixels,
  • qHD - around 960x540 pixels
  • HD - around 1280x720 pixels
  • HD+ - around 1600x900 pixels

This gives us fixed targets for the custom code to work against. Pick the lowest one that’s less than or equal to the actual resolution and use that layout code.

Performance Characteristics

Here are some of the top devices and their specifications taken from over 10 million recorded devices on the Rune platform.

VendorModelCPUGPU
RedmiM2006C3LGOcta-core (4x2.0 GHz Cortex-A53 & 4x1.5 GHz Cortex-A53)PowerVR GE8320
SamsungSM-A107MOcta-core 2.0 GHz Cortex-A53PowerVR GE8320
RedmiM2004J19COcta-core (2x2.0 GHz Cortex-A75 & 6x1.8 GHz Cortex-A55)Mali-G52 MC2
RedmiM2006C3MNGOcta-core (4x2.3 GHz Cortex-A53 & 4x1.8 GHz Cortex-A53)PowerVR GE8320
SamsungSM-G532MQuad-core 1.4 GHz Cortex-A53Mali-T720MP2
AppleIPhone 7Quad-core 2.34 GHz (2x Hurricane + 2x Zephyr)PowerVR Series7XT Plus (six-core graphics)
SamsungSM-G610MOcta-core 1.6 GHz Cortex-A53Mali-T830 MP1
RedmiM2003J15SCOcta-core (2x2.0 GHz Cortex-A75 & 6x1.8 GHz Cortex-A55)Mali-G52 MC2
SamsungSM-A015MOcta-core (4x1.95 GHz Cortex-A53 & 4x1.45 GHz Cortex A53)Adreno 505
AppleIPhone 11Hexa-core (2x2.65 GHz Lightning + 4x1.8 GHz Thunder)Apple GPU (4-core graphics)

Even just looking at the top 10 or so, we can see a reasonably wide range of available hardware.

CPU

Mobile CPUs are getting faster all the time, but there are still plenty of low-specification devices out there. You also have to consider that the device will be running other applications at the same time as your game and if you’re using Rune, it’ll be used for a voice call as well.

It’s best to avoid CPU intensive loops making, sure your code does this in small sections over multiple rendering frames rather than attempting to process a lot of data in one go.

The pauses caused by CPU operations being locked up with a tight loop are the number one cause of player abandonment. That crazy button mashing when your phone seems to have stopped responding, resulting in the app/game eventually coming back but immediately closing. Users don’t like the feeling of their phone not working and so rarely open the application a second time to give it another chance.

GPU

Graphics chip usage is one of the most common causes of a very good game failing to spread across the mobile universe. Mobile graphics performance varies a great deal even from devices over the last 5 years. Your beautiful 3D game isn’t going to be so interesting for a player who is seeing it at one frame every three seconds while holding a phone that’s melting in their hand.

Keep it simple, especially 3D. Low-poly models don’t have to mean ugly. Multi-pass rendering should be used sparingly – a shadow pass is normally enough to make it feel real.

Also, it’s worth keeping in mind that the final game is going to be played on a 6.5-inch device; what you see on your monitor where it’s scaled up isn’t what will be visible to someone looking down at a tiny device. Avoid obsessing over the tiny detail you can see that no player ever will.

Physical Properties

One aspect that’s often overlooked with cross device games design is the physical aspects of the different devices.

First, we have the physical size of the design, especially when you’re thinking of two-handed controls on portrait games. What feels nice proportionally on a large device (8 inches or more) will often be uncomfortable to hold on a smaller device (around 6 inches). It’s worth finding items of the right size to test the feel of the controls (I use cardboard, cut and measured).

Second, “notches” – oh, how we hate them! Ever since the iPhone introduced the camera notch, web and game designers have despaired. Different devices now have different notches and notch sizes, meaning developers need to consider what’s called the “safe area.” As a game designer, of course, you want to fill the screen with the assets, so you both have to account for the notches but also avoid putting anything important there.

Luckily, if you’re writing games on Rune, it handles the safe area/notches for you leaving you with a clean rectangular area in which to put your game!

Making your game work well cross-platform and cross-device increases the number of potential players you have access to. In multiplayer games, it’s also key to make sure the experience is as similar as possible across devices to keep the game feeling “fair”.

If you have any questions or have anything to add, come join us 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!

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

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