|Really old media material. Screen shots are from a PROTOTYPE build and do not reflect the final product.|
To use an existing example, in Wildstar, Adventures allow the party to vote on what area to do next. The state flow for that might look like:
|Wildstar Adventure voting, in a nutshell.|
So the server has to keep track of the current state, the clients need to know what state they're supposed to be on, and the server has to wait for all clients to respond before moving to the next state--where a response in the Wildstar case is either everyone voting, or players completing a quest. Not to mention be resilient to players losing connection, or new players joining, etc.. Perhaps some cases you want a time limit (like for voting) so you may need the server to force the clients to the next state regardless of whether they completed their step.
Networked players are pretty similar to threads in a sense. When a computer needs to do a bunch of disparate tasks that are independent, you spawn a bunch of threads and let them run off. When you need to wait for everything to come back before moving on, you use a synchronization technique called a Barrier.
In our case, instead of a single thread continuing on at the end, we just kick off another set of arrows to hit another barrier. Oh, and we need it to work across a network.
|What a single round of Voting-Quest cycle might look like.|
With Unity's networking, this isn't actually a terribly difficult thing to achieve. Unity allows you to create objects that can talk to each other across the network via Remote Procedure Call (RPC). So to get everything hooked up, the server can determine it needs to show a UI flow (say, all players are dead, so it needs to show a GameOver screen), and once it's determined that it can do this, we can Network.Instantiate a Barrier object such that the server and clients all have a linked object.
By using Network.Instantiate, any RPC calls made by that object will automatically go to the other versions of that object on other clients. We can choose to make a call to just the server (RPCMode.Server), to all others (RPCMode.Others) or to everyone including ourselves (RPCMode.All). The latter is quite useful in the case where you want to move everyone, server and clients, onto the next state--though one could do this more performantly if one were to special case the logic to have the server call it's own internal method instead of the RPC, but given we're network bound regardless, meh. It's simpler to write this way, and simpler to write means fewer bugs.
The server version of the object can keep track of all the players, and send RPC calls to the clients to say StartFlow, GoNextState, FinishFlow, and the clients can send RPC calls to say they're ready to go to the next state. The server can then determine when/how it wants to go to the next state and tell the clients when to switch over.
If you're particularly clever, you'll also notice that you've basically got two separate objects here: a Barrier, and a State Machine. You could split the two apart and have the State Machine encapsulate the Barrier to make the logic far more organized, at the expense of having to coordinate the two objects over the network, which requires a bit more programming overhead. But having them organized correctly means it'll be easier to write more complex State Machines later (like ones that aren't just a linear set of steps).
So that's all the depth I'm going into. Further implementation details are left to the reader, but once you have the flow worked out and understand Unity's APIs, it's not actually terribly onerous to implement. Took me a day and a half, and that was with a lot of testing and some prototyping as well.
For those who aren't developers and are reading for funsies, hopefully this gave you some insight to how something as simple as trying to keep everyone on the same page, literally, across a network works. Even the simplest concepts within a game can take a fair amount of programming effort!