Best techniques for timing sensitive 2p co-op game

Hi! We’re looking at retrofitting online support for our game Tentacle Tango (not released yet but available as a demo on Steam) and we’re looking at Coherence since it seems to be easy to work with and capable at retrofitting without needing to rebuild the whole core logic too much. We’d appreciate some pointers on what techniques would be best to use in our situation given the timing sensitive gameplay.

The game is a two player co-op precision platformer - you control one character each and need to time when you position them on the ground to continue the swing of the other player (it makes more sense if you have a look at the video in the Steam page :slight_smile: ). Apart from timing the characters there are threats to look out for, like lasers, spikes, bullets etc that I assume also need to be synced.

Are there any specific techniques we should build this solution around in Coherence that would work best in online play so it feels as close to as snappy and predictable as local play?

Any pointers or hints that could point us in the right direction would be greatly appreciated.

Thanks!

Hey!

Apologies for a late reply, quite a curveball you threw at us. We like those though!

I’ll focus on the movement mechanic, as it seems to pose the main challenge and could use advanced techniques - everything else you should be able to sync effortlessly using the basic coherence toolset.

Simple approach

Let’s start with a simplest approach and expand from there. We’ll have each client be authoritative over their octopus. This will give player the most responsive feel when it comes to the binding action (I’ll call so the action of octopus sticking to the ground on player’s command).

Each octopus would have a CoherenceSync component with position and a bool isBound being synced. I imagine the link (tentacles) between players to be handled fully client-side by simply connecting two player objects regardless of their position.

There are two phases in the movement - let’s call them active and passive phase, where active is when my octopus is being rotated while my partner is bound. Passive would be when I’m bound and it’s my partner’s turn to swing and bind.

The active phase with this solution will feel very good - my octopus starts rotating the very moment I notice that my partner’s isBound = true. I rotate for a while and then press the binding action. Again, since I’m authoritative over my octopus, this is executed immediately and so the experience is snappy. I set the isBound = true on my octopus and now wait for my partner.

And this is where we encounter the main problem of this approach. The movement of my partner in the passive phase will be delayed. The reason for this is, it takes take time for my isBound = true to travel to my partner. During that time I’ll see both octopuses in the bound state. See the frames #3, #4 and #5 on the diagram below, where X on a player means bound.

Prediction approach

One way to solve that problem is to use the prediction. Given that the movement is fairly deterministic, we can start predicting partner’s movement the very moment we issue a bind. The trick here is to run the prediction only until we receive an actual movement updates from the partner and to predict it in such a way that it nicely blends with the received updates.

What I would consider here is starting the movement of my partner slowly, and accelerate it to the target speed. The acceleration must be done such that the moment we reach target speed we also receive the update from our partner.

You can see the details on the diagram. On frame #3 client B instead of waiting for the position update from the partner starts predicting the movement by slowly increasing the rotation speed. Partner on the other hand starts rotating at full speed the moment he receives isBound from B. After a while B is again behind A, and the cycle repeats.

The important bit here is to adjust the predicted acceleration/speed to the ping, so we nicely blend once updates from partner come.

Like with any prediction method, there’s a risk of mispredicting - our partner could have bound immediately after our unbinding. Updates (and the binding state) could also come late due to lag spike leading to us predicting too far. This can be mitigated to a degree by limiting how far a client is allowed to predict as well as nicely blending in case of misprediction istead of just snapping the position.

All in all this should give a better user experience than just “standing still” waiting for remote updates.

GGPO approach

Another approach you could take to solve the movement problem is a GGPO style input handling, commonly used in fighting games. And by that I don’t mean that the game has to be deterministic - we can just apply some of the principles.

The key idea is to delay the binding action locally but send it immediately to the partner. When it’s my active phase and I press the bind action, this is not executed instantly. Instead, this action is delayed by a couple frames. On the active client we can already predict when this action will be executed and so we can calculate the final position. Upon issuing a bind we immediately inform our partner about the planned time of binding and the calculated position using a command.

If the delay is long enough to cover for the latency required for the command to reach our partner, we’ll have a perfect sync. Our partner can now execute the binding of our octopus just in time using the information from the command.

You could use both commands and inputs system for that, with the latter having the advantage of built-in delay and buffering.

There are no free cookies though. This solution also has some drawbacks. Just like in the first approach, there’s always room for misprediction - the input might just come too late. In that case some smooth reconciliation would make sense.

The second drawback is, the delay introduces some UX issues. First, the game feels less snappier. Secondly, to cover broad range of latencies between the players the delay will have to be dynamic (otherwise you could have constant mispredictions if latency is higher than the delay). Players can adjust their reflex to a delay, but if the delay constantly changes this can be a frustrating experience. There are some options here too - instead of changing the delay mid-game, negotiate the delay at the start of the session and stick to it.

Final thoughts

There are probably other solutions to this problem. Just keep an open mind - the (non-competetive) game networking is about making people think they play the same game, not making them play the same game. Fun > consistency.

Play with different approaches and see what produces the best UX. Also, consider testing in poor network conditions.

Whatever path you choose I’m pretty sure coherence got you covered. Should you need a more hands on support, feel free to reach out to devrel@coherence.io to chat with our team about your project.

Hope this helps!

2 Likes

Thank you Filip for the very detailed answer! We’ll do some tests here based on your suggestions, and get back to you if we have any follow-up questions. Thanks again for great support!

1 Like