Hacker News new | ask | show | jobs
by furyofantares 813 days ago
A pattern sometimes used in multiplayer video games is there's a bunch of code that is by default run on both the client and the server. The client code runs as a prediction of the server state. When the server state is received it slams the client state.

For games "prediction" is an apt description of this, because the client can make a good guess as to the result of their input, but can't know for sure since they don't know the other players' inputs. But this paradigm can also be used to simply respond immediately to client input while waiting for the official server state - say by enabling/disabling a dropdown, or showing a loading spinner.

There's also plenty of client state that's not run on the server at all. Particle systems, ragdolls - stuff that doesn't need to be exactly the same on all clients and doesn't interact with other player inputs / physics.

If we're gonna have a persistent server connection I don't see a reason this wouldn't work in a reactive paradigm.

6 comments

That's what I did with: https://territoriez.io/

It's a clone of https://generals.io/

It's built with LiveSvelte. It doesn't need any predictive features as it's basically a board game and runs at 2 ticks per second. It does use optimistic updates to show game state before it actually updates on the server. The server overrides the game state if they're not in sync.

All game logic is done inside Elixir. To do predictive correctly, you'd need to share logic between the server and the client. Otherwise you're writing your logic twice and that's just a recipe for disaster.

One possible solution which I didn't investigate, but should work, is to write all game logic in gleam (https://gleam.run/). Gleam is compatible with Elixir, AND it also can compile to js, so you could in theory run the same code on the server and the client.

Now this is a big mess to understand, you could say "why don't write it all in js and be done with it" and you'd make a very good point and I'd probably agree. The main advantage you get is that you can use the BEAM and all it's very nice features, especially when it comes to real time distributed systems. It's perfect for multiplayer games, as long as you don't take into account the frontend :)

Hey, about your clone of https://generals.io - why not make it a better designed version of that game instead of just a clone? Perhaps you have plans to. There are things about the original that kind of suck.

1) Spawning closer to the center is just strictly significantly worse, corners are best. It's essentially an auto-lose to spawn anywhere but an edge or corner in a FFA.

2) Getting literally all of someone's armies when you kill them is so good that it pushes players to just try to all-in the first person they meet every single time. There is no way to "fast expand" once you've met another player because of the first factor, and also that 40-50 armies on a neutral castle is very high. I think the "early-game" can be much better designed.

3) Perhaps you should be able to change your capital, which could solve problem 1.

4) There are many ways you could keep the simplicity of play, but boost the richness and depth of the actual gameplay. For instance, more chokepoints that would allow actual strategic use of territory and army positioning. Different tiles which have different advantages to owning. Borrowing from something like civ - forests or hills which have a defensive boost when you're inside. Rivers which attacking across is disadvantageous.

Just some feedback from someone who enjoys games like Chess and Starcraft, and thinks the core gameplay loop of generals.io is really fun, but believes that it is seriously lacking in strategic depth.

It's a little bit different from generals.io but the points you mention are very valid. I originally started working on it as a showcase of LiveSvelte + I like generals.io a lot and thought it could be improved visually + UX wise. Then afterwards started thinking a bit about the game design and what I could improve, but eventually burned out on the project. Was also at a time when I wasn't working but am working again.

Maybe I'll pick it up at some point, but also open to other people working on it if they're interested. I'd be open to partner up with someone to eventually make it monetized. It's not open source as I wanted to add paid features (cosmetic features, not pay to win).

A solution for most of these issues is a modifier system. I have a basic one in place and wanted to experiment with different modifiers. A modifier can be anything, like prevent spawning in the center, to increasing rewards for capturing cities, to even allow crossing the border of the map into the other side, like pacman (which could address point 1). Then the goal would be to see which modifiers stick with the player base, and make them the default, while still allowing for custom games with different modifiers. Generals has a similar system in place and they sometimes make the modifier the default for the day, which I like a lot.

As someone who started using Elixir this year (for work) this is really cool. I have had some ideas for some SPAs and games just like these io ones and it'd be great to use Elixir for them. Do you think a top down fps io game would be plausible with this setup? There would need to be at least 60 ticks per second I'd think.
Definitely possible. Tick rate isn't the problem, Elixir is very performant. Just the predictive elements are an issue if you're working with 2 different languages. I'd go with Gleam from the start and look into compiling to js.
Don't you lose a lot of the niceties of Elixir when switching to Gleam, just because Gleam is a younger project? LiveView would be the big one I'm thinking of. Do you see that as a worthy trade?
You can still use LiveView/Phoenix/Elixir, but have your game logic be in Gleam. I haven't used it though so I could be wrong here.

A little bit more about it here: https://katafrakt.me/2021/10/13/app-with-elixir-business-log...

You'd call Gleam code like this inside Elixir:

`:game.move(game, player_1, :left)`

And you'd receive an Elixir map `%Game{}` which you can then use in LiveView. If that makes sense.

Cool! I'm going to have to look more into Gleam. I saw it hit v1.0.0 on ElixirForum last month, but I figured it was an alternative to Elixir rather than something that played so nicely with it.
> To do predictive correctly, you'd need to share logic between the server and the client. > > One possible solution […] is to write all game logic in gleam […]

Rust, with Uniffi, can also be a good candidate. You’d be targeting WebAssembly.

Is there a way to emit WebAssembly with Uniffi? I looked for it before, but I couldn't find it (I'm using it in a project to share login between the backend and Kotlin/Swift apps. I wanted to share it with JS for a web frontend, but didn't find any docs mentioning it)
As a player of a game (Overwatch) that simulates ragdoll physics locally and non-deterministic, I wish they did it correctly.

What ends up happening is someone dies and their body flies off or gets stuck in a hilarious pose... and no one else saw it, nor can you rewatch it in the replays as every client renders it differently.

Unless you catch it with a live recording then it's lost forever. With a sometimes goofy game like Overwatch it's sad knowing no one else is seeing it.

In Counter Strike I've seen it be an actual issue too where a player's view is blocked by a body and their teammates don't have the same issue while spectating them.
Fighting games dial up these low-latency conflict resolution considerations to 11, and there’s an entire subfield of techniques for writing “netcode” to reconcile real-time event streams.

https://arstechnica.com/gaming/2019/10/explaining-how-fighti... is an incredible walkthrough of this! Discussion: https://news.ycombinator.com/item?id=34399790 and https://news.ycombinator.com/item?id=26289933

When it comes to server persistence, as in an MMO setting, you add I/O bottlenecks into the mix. https://prdeving.wordpress.com/2023/09/29/mmo-architecture-s... is a fascinating read for that end of things. Discussion: https://news.ycombinator.com/item?id=37702632

Isn’t that just optimistic updates? This has been common in client side logic for a long time, no?
I was more talking about the method to achieve optimistic updates, rather than the concept of optimistic updates in general. That is, having the client and server code be identical where applicable, with client code being run in a prediction context.

I'm not a web expert but the optimistic updates I've seen in web stuff is more like, I'm gonna fetch this url and here's the data I expect back. Nothing wrong with that, but it's achieved in a different way, where the server is all about providing data and the client is all about managing state.

The OP is talking about maintaining a persistent connection to a server which is doing most of the state management. They detail things this does well (makes the server easier to write) and things it does poorly (makes optimistic updates harder) and a solution to the things it does poorly. So I'm drawing a parallel to other systems where state is managed on the server and must be predicted on the client.

The way I’ve always done it is that I have client side event handlers that perform the UI-visible side of the logic client side, but also asynchronously request the server to do it.

In simple cases, that logic simple sets a toggle or whatever. In slightly more advanced cases it might modify something such as appending to a list. But in some cases it could be performing more complex logic such as applying a filter to a list. Sometimes this logic is only done client side (if the filtering is view-only for example) and sometimes both client and server. Basically, if you remove all of the network requests, it would still look like it’s working, more or less.

Of course only logic that is needed for data to be correctly displayed in the UI is needed client side. Performing logic that is only used server side (even if it has later UI effects — only effects that the user expects immediately need local representation) is unnecessary client side.

Eg even in a game, you might not want to run, for example, bot AI speculatively on every client but rather just the movement prediction like for other players, while everything else may be needed for rendering.

Haven't used it in years, but Meteor.js worked like this. Even to the point where it had a client-side database implementation that mirrored (parts of) the server-side database.

It would apply the updates optimistically to the client side DB, and the much of code that sat between the database could be shared between client and server. Neat stuff, even if overall Meteor wasn't my favourite thing to work with.

You need to be smart about this though, I don't want it to look like a live/interactive form has worked for example - updating derived data on the page - if it's then going to turn out that actually the server had an error or there was network trouble, and it all gets undone (or worse I close the page/navigate away not realising at all).
you would be in the minority then, most people these days want (and expect) live feedback if the action's result is predictable enough

e.g. throttle your network and upvote a hn comment. you're not sitting there waiting with a spinner while the server responds, it's all in the background.

the hn implementation isn't great though - if the upvote request fails, the optimistic update isn't rolled back, and you have no knowledge that it failed. for hn, who cares, it's just a lost upvote, but for most modern web apps you would show the user that the action failed

If you're (would be) 'sitting there waiting with a spinner' then that endpoint is too slow, regardless of what the frontend does in the meantime.
depends on the definition of "slow". unless you have servers and databases everywhere (overkill for 99% of apps), your endpoint will be probably be >400ms for some people somewhere, enough to feel as a user.

that's without accounting for patchy reception (in a tunnel?), network blips, server blips (overworked?), etc.

i'm not saying everything should be optimistic, but for something like a hn upvote, i dont care if my public wifi freaked out and took 3 seconds for that 1 request, and i think more people are like that than not

I've always heard of and referee to this pattern as "otimistic updates" - tho my frame of reference is 25y in webdev, w/ almost no exposure to game dev.