Dev Story: Syncing 400+ objects in ‘Clash: Spaceship commander’
This guest post was written by Luuk Waarbroek, Founder of Napalmtree Studios.
Clash: Spaceship commander is a single and multiplayer RTS game for mobile. It is a game about capturing planets, making strategic decisions and managing units. An RTS wouldn’t be an RTS without eventually destroying all enemies, so yeah you have to do this too.
Clash is the studio’s first released multiplayer game and Luuk would like to give some insights in the problems he ran into during the development.
A. Desired situation
Since Clash is an RTS, it is very important that every client has exactly the same simulation of the game. This means that units, health, projectiles, planet state and other gameplay elements like that all need to be synced over the network.
The desired situation had to have the following important gameplay elements synced:
- Unit position and rotation
- Unit projectile position and rotation
- Unit projectile fire event
- Projectile hit target event
B. Problems
The above situation maybe doesn’t sound hard to do with Photon. I was familiar with syncing positions, rotations and states by using PhotonSerializeView and using RPC’s for game logic. I started out using these techniques, however I quickly ran into problems.
Syncing a lot positions and rotations
Syncing gameplay data that needs to be updated all the time is normally done by using the OnPhotonSerializeView method. This sends data 20 times per second by default and is ideal for syncing positions and rotations. However in my case sending the data for 400 units 20 times a second would be way too much traffic.
Conclusion: Units & projectiles can’t continuously update their position and rotation over the network.
Unit RPC calls
RPC calls are quite common for letting other clients know some event has happened. For example: A unit fires a projectile.
Doing this with an RPC is easy but the problem lies within the amount of units. What if 400 units would fire this RPC at the same time? First of all, as you might know Photon stores all RPC calls in a temporary queue. This queue has a limit of 100, I believe you can adjust this limit but I didn’t dare to. If you reach this limit you will get “QueueOutgoingReliableWarning” warning messages and it caused clients to disconnect, RPC calls never arriving and other bad stuff.
Conclusion: Units cannot handle their own RPC logic as it causes network issues.
Limit of 1,000 Photons Views
The game used to have 1000 units and there is a PhotonView limit of 1,000 by default. I came across a post by Tobias on the Photon forums which in he said using that many PhotonViews will always cause trouble depending on the used network and the target clients. Tobias works for Exit Games so hey, I simply followed his advice. However before I read that post I did use a PhotonView for each unit to check who the owner of that object was and I used the PhotonView ID so if I had to remove PhotonViews I had to come up with my own owner / ID system.
Conclusion: Can’t use PhotonViews for units & projectiles.
C. Solutions
Syncing a lot positions and rotations
I read through this article (1500 archers) which is referenced a million times already in the world of multiplayer games. While the system described here is quite complex in my opinion I did use certain parts of it. One of them is only sending orders over the network.
What this means is that instead of syncing positions and rotations every X times per sec you simply send an RPC with:
“unitID, currentPos, targetPos”.
Each client receives this and then looks up the unit, knows where it should be moving from and where it should be moving to. Each client then locally moves this unit towards the targetPos. This means that all units are locally controlled and that movement must be deterministic.
Unit RPC calls
I used quite a simple approach for this problem. All I did is bulk up all of the same RPC calls, by bulking I mean simply saving them all in a list and after X milliseconds send them over as one RPC. So for example when a unit gets the order to move to a new position. I store the unit ID, unit current position and target position in a UnitsManager class and keep doing this for each unit that receives this order. When the X milliseconds are over I send the entire list which consists out of all the units that received the move to new position order with one RPC.
Example: sending over multiple unit positions with 1 RPC
I use 2 lists which are needed for saving everything we need to send. Before sending them over with an RPC call I convert them into Arrays because RPC’s don’t support Lists.
List<int> soldierWanderIDs= new List<int>();
List<object> soldierDestinations= new List<object>();
Soldier picks a new target position for himself then calls this method in UnitManager.
public void SoldierOrder(Soldier soldr, Vector3 targetPos){
soldierWanderIDs.Add(soldr.ID);
soldierDestinations.Add(targetPos);
}
Each 0,3 seconds the UnitManager sends an RPC to all clients with a list of soldier ID’s and a list of soldier destinations.
void Update(){
if(soldierWanderIDs.Count > 0 && bulkTimer < .3f){
bulkTimer += Time.deltaTime;
if(bulkTimer >= .3f){
photonView.RPC("SendWanderBulkNetwork" , PhotonTargets.All,soldierWanderIDs, soldierDestinations);
}
}
}
Each client finds the soldier by ID and gives him order 0 which is move to the given destination.
[RPC]
void SendWanderBulkNetwork(int[] soldierIDs, object[] destinations){
for (int i = 0; i < soldierIDs.Length; i++) {
Soldier soldier = FindSoldierWithID(soldierIDs[i]);
soldier.SetNewOrder(0,(Vector3)destinations[i]);
}
}
Limit of 1,000 Photons Views
Without PhotonViews on my units I needed a way to be able to know which unit is which. If client A says kill unit with ID 40, all the clients must know who this unit with ID 40 is. So each unit has a unique ID and every client must know the ID’s of all the units.
I distribute the ID’s in the following way: When a client wants to spawn a new unit he asks the MasterClient to spawn a new unit for him. The MasterClient then gets an unused ID and sends an RPC to all clients telling them to spawn the unit with the unused ID.
That’s all for today! If you want to check out the game, get it here:
Thanks for reading and if you have any questions please drop them below in the comments or contact me at: luuk (at) napalmtree . nl.
Luuk