I am working on simple 2D multiplayer shooter. I want to have a list of equipped weapons that should work like a stacked - last picked up weapon is equipped until its ammo is out. I used SyncListStruct for that. I have the code that works almost fine but the state of the list is synced to clients only when I add but not when I modify an item or remove from the list.
Here is simplified code of the component (I skipped the parts that does not interact with the list).
public class PlayerShooting : NetworkBehaviour
{
public class EquippedWeaponList : SyncListStruct<EquippedWeapon> { }
public EquippedWeaponList weapons = new EquippedWeaponList();
EquippedWeapon currentWeapon;
public override void OnStartClient()
{
weapons.Callback = OnChangeWeapons;
}
// this works fine
public override void OnStartServer()
{
weapons.Add(EquippedWeapon.Create("Blaster", -1, Weapon.Blaster));
}
// this is called from outside when the player enters the trigger
// also works fine
public void AddEquippedWeapon(EquippedWeapon weapon)
{
weapons.Add(weapon);
}
[Command]
void CmdShoot()
{
var weapon = weapons[weapons.Count - 1];
// shoot with the weapon (skipped for brevity)
// this doesn't sync properly
weapon.ammo--;
if (weapon.ammo == 0) {
weapons.RemoveAt(weapons.Count - 1);
} else {
weapons[weapons.Count - 1] = weapon;
}
}
void OnChangeWeapons(EquippedWeaponList.Operation op, int itemIndex)
{
if (weapons.Count > 0) {
currentWeapon = weapons.Last();
aura.color = WeaponSettings.instance.GetColorFor(currentWeapon.weapon);
if (isLocalPlayer) {
weaponName.text = currentWeapon.name;
weaponAmmo.text = currentWeapon.ammo < 0 ? "∞" : currentWeapon.ammo + "";
}
}
}
}
This is the weapon struct:
[Serializable]
public struct EquippedWeapon
{
public string name;
public int ammo;
public Weapon weapon;
public static EquippedWeapon Create(string name, int ammo, Weapon weapon) {
var instance = new EquippedWeapon();
instance.name = name;
instance.ammo = ammo;
instance.weapon = weapon;
return instance;
}
}
And Weapon is just an enum (I also tried to change it to int but it didn’t help):
You actually have to name the SyncList in a specific way (or did). So you have
public class EquippedWeaponList : SyncListStruct<EquippedWeapon> { }
public EquippedWeaponList weapons = new EquippedWeaponList();
but i think it needs to be
public class SyncListEquippedWeaponList : SyncListStruct<EquippedWeapon> { }
public SyncListEquippedWeaponList weapons = new SyncListEquippedWeaponList();
You should never change internal field values of a SyncStruct struct by itself. For safety they should be immutable (or you must remember to set dirty bits manually every time a field is set which opens you up for errors) . Setting the internal field of a sync struct does not flag dirtyBits and therefore does not sync to client. Only assigning the entire struct sets automatically sets dirt bits.
Option1: DOES NOT WORK
syncStructList[3].field1 = 123; //this will only change value on server, will not set dirtybits
Option2: WORKS
syncStructList[3] = new CustomStruct(123); //setting the ENTIRE struct flags dirtyBits to be flagged.
Option3: WORKS (but can cause syncing to fail if you forget to set dirty bits everytime you update a field)
syncStructList[3].field1 = 123;
…then also set dirty bits manually or re-assign to flag dirty bits
However, the list is still not synced. It is interesting that the callback OnChangeWeapons is called on the client when the list should be changed but it is not synced.
Ok I finally found the problem and it has completely nothing to do with SyncList. The original code works just fine. The problem was that when the player enters the trigger AddEquippedWeapon method was called on both, client and server. Then the server was working with the last item BUT it was last but one on the client and client was displaying info about the last item which was something that the server was unaware of and doesn’t change it.