Foreword: I'm developing an online soccer MOBA where upwards of ten players will be jockeying for a soccer ball. This journal is a brain dump to help me get a handle on the mechanics of managing a soccer ball in an online soccer game using Unity3D and the Photon network engine. I apologize in advance for the lack of helpful diagrams, animated GIF's, and the covering of every single edge case from start to finish; I'm saving those for the day when I can confidently write an article on "These are the mechanics of ball management in a network soccer game which work well enough for a polished release."
The game is made with Unity using the Photon network library. All players can send messages to each other, and one player is the Photon "master client" which I'll refer to as the "master player" going forward.
The master player is the authority that resolves any disputes among the peers; for instance whether a goal was really scored, and how much damage a player really takes from another's attack. Without a master player, the minor differences in everyone's game experience due to latency would escalate to become major differences, and everyone could be completely out of sync before long.
There is only one ball on the soccer field. When no player possesses it, it rolls around the field managed by the physics simulation. After a player takes possession of it by running onto it, the ball will always be positioned in front of them (it becomes synchronized to the player's transform instead of its own transform).
I'm purposefully leaving the details of who controls the physics simulation and who decides what player possesses the ball out here; I explain those things in the various implementations below.
Ball Control Implementations
Failure: Ball physics centralized, player control centralized
My first implementation had the master player doing all the work: They would manage all the ball physics, detect when players were near enough the ball to possess it, and other players would have to send an RPC to the master player when they wanted to kick the ball away.
This implementation worked poorly because all the non-master players had to wait on the master player to give them ball possession or relinquish it. If I were a non-master player, I would not appear to possess the ball by running onto it; I would instead have run over it and even a few steps in front of it before the ball attached itself to me. When I kick the ball, it would stick to me for a brief moment before being teleported back to the point I actually kicked it from.
Failure: Ball physics centralized, player control decentralized
My second implementation had the master player still managing ball physics, but each player would determine when they possessed the ball and when they kicked the ball. They would tell the master player when they did either. The master player would do a check to determine whether the request was legitimate, and then relay the ball's final owner to all the players.
The problem here is that when the ball was kicked by a player, the ball would rubberband backwards on that player's instance. This is because the master player resumed the physics simulation from the kick point at the time the ball was already far away from the kick point on the kicker's instance.
Potential success: Ball physics decentralized, player control decentralized
My third implementation follows these rules:
- The PhotonView owner of the soccer ball is always the master player
- When a player possesses the ball, they control everything about it and stream ball position and rotation information to other players (the streaming is done through OnPhotonSerializeView calls in a hidden GameObject belonging to that player)
- If the master player disagrees with the player possession, it will override it and notify all players through the ball's OnPhotonSerializeView call
Here is the step-by-step process of what happens when a non-master player takes possession of the ball and kicks it later:
- Ball is rolling across the field. Player A controls the ball and is simulating its physics. All other players are getting a stream of ball position and rotation updates from Player A.
- Player B moves their player to the ball to possess it.
- Player B sends an RPC to all other players that it "tentatively" possesses the ball; its own session locks the ball to its character position and makes its rigidbody kinematic.
- All other non-master players get the RPC and lock the ball to Player B's position also making it kinematic.
- The master player gets the RPC and determines who should be in possession of the ball; in this case it is indeed Player B. The master player updates the ball's serializable data to reflect that, and its own session locks the ball to Player B's position and makes it kinematic.
- All the non-master players (including Player B) call OnPhotonSerializeView on the soccer ball and see that the master player confirmed Player B owns the ball. No action is needed.
- Player B decides it's time to kick the ball.
- Player B makes the ball not kinematic any longer, adds a kick force to it and sends an RPC to all other players that it kicked the ball.
- Now that the ball is in motion, Player B begins streaming the ball's position to all the players from its hidden GameObject.
- All other non-master players get the RPC and unlock the ball from Player B. Since Player B is streaming its position and rotation, the ball immediately begins to move in the direction of the kick.
- The master player gets the RPC, unlocks the ball from Player B, and updates its serializable data to reflect that the ball has no possessor. Since Player B is streaming its position and rotation, the ball immediately begins to move in the direction of the kick.
So what happens if two players claim the ball one right after the other? All "tentative possession" RPC's after the first one are ignored until after the ball's serializable data update is received from the master player. This is accomplished by tagging each possession assignment with a sequence number that is incremented only by the master player in the serializable data payload.
So what happens if the master player decides Player B doesn't possess the ball in step 5 because it's too far away for example? The master player will populate its serializable data with the fact that Player A still controls the ball. All other players, including Player B, will receive that and update the possessor on their instance accordingly. Player A will resume ball physics simulation.
There's a bit of a kludge with the physics however: By the time the master player denied Player B's possession, Player A will have already stopped simulating the physics. Consequently when it resumes ball physics simulation, the ball will be at a dead stop in front of Player B. The only time this is avoided is if Player A is also the master player.
There are some ways to resolve that kludge:
- Make it so Player B is never denied ball possession (and therefore cheating becomes easier)
- Make the most recent possessor, rather than the master player, verified the ball's transfer of ownership. I tried that and failed because I couldn't figure out the logistics of how to get all the players to agree on who was the most recent possessor.
When I had two network players play with the ball one at a time using this implementation, the ball movement appeared natural in both sessions during all 11 steps. How well will this work with 3+ players actually playing the game? I don't know, but I'm fairly sure it will work better than my first two failed implementations.