UNET 32 [SyncVar] Limit Workaround

UNET only allows us to use 32 Syncvars+SyncLists per Script. That’s fine in most cases, but sometimes we need more, which will result in the following errors:

  • UNetWeaver error: Script class [Name] has too many SyncVars (32). (This could include base classes)

  • Failure generating network code. UnityEditor.Scripting.Serialization.Weaver:WeaveUnetFromEditor(String, String, String, String, Boolean)

The reason for ‘32’ SyncVars is that the UNET Weaver uses a 32 bit dirty mask, hence 32 SyncVars. Right now there’s just no way to modify this value without reverse engineering Unity DLLs. I hope one of the UNET developers can at least give us the option to use a 64 bit dirty mask for SyncVars in the future, so that we can use 64 of them too. Perhaps with a flag in the Global Config, similar to the new ‘use ack long’ property? 32 sounds like a lot, but I had plenty of people add dozens of attributes to uMMORPG’s Player script, running into the limit over and over again. Sending around 32 more bits should be worth making a developer’s life easier.
@aabramychev @Erik-Juhl @larus

In the meantime, there is a way around it. I implemented my own [SyncVar_] attribute that allows for abitrary amounts of SyncVars in your scripts by mapping them to SyncList entries. Here is how to use it:

  1. Create SyncVarLimitWorkaround.cs:
// (c) ummorpg.net
//
// This script provides a custom [SyncVar_] attribute to work around UNET's 32
// SyncVar limit.
//
// Usage: attach it to the Prefab; use [SyncVar_] in any component.
//
// This script uses reflection and is kind of hack. It works fine, but it would
// be even better if UNET allows more than 32 SyncVars by default some day.
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Networking;

[AttributeUsage(AttributeTargets.Field)]
public class SyncVar_Attribute : Attribute{}

// UNET has no SyncListLong. Workaround via SyncListStruct
public struct LongWrapper {
    public long value;
    public LongWrapper(long value) { this.value = value; }
}
public class SyncListLongWrapper : SyncListStruct<LongWrapper> { }

// we need a list of field,obj. a dictionary can't guarantee order.
public class FieldInfoAndObject {
    public FieldInfo field;
    public object obj;
    public FieldInfoAndObject(FieldInfo field, object obj) {
        this.field = field;
        this.obj = obj;
    }
}

public class SyncVarLimitWorkaround : NetworkBehaviour {
    List<FieldInfoAndObject> stringFields = new List<FieldInfoAndObject>();
    SyncListString strings = new SyncListString();

    List<FieldInfoAndObject> floatFields = new List<FieldInfoAndObject>();
    SyncListFloat floats = new SyncListFloat();
 
    List<FieldInfoAndObject> intFields = new List<FieldInfoAndObject>();
    SyncListInt ints = new SyncListInt();
 
    List<FieldInfoAndObject> uintFields = new List<FieldInfoAndObject>();
    SyncListUInt uints = new SyncListUInt();
 
    List<FieldInfoAndObject> longFields = new List<FieldInfoAndObject>();
    SyncListLongWrapper longs = new SyncListLongWrapper();
 
    List<FieldInfoAndObject> boolFields = new List<FieldInfoAndObject>();
    SyncListBool bools = new SyncListBool();

    public static List<FieldInfo> GetFieldsWithAttribute(Type objectType, Type attributeType) {
        // getfields with all binding flags still doesn't get base type's private
        // fields. we have to iterate through base types to get all of them.
        var result = new List<FieldInfo>();
        while (true) {
            var fields = objectType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(
                field => field.IsDefined(attributeType, true)
            );
            result.AddRange(fields);
            objectType = objectType.BaseType;
            if (objectType == null) break;
        }
        return result;
    }

    // server & client: build field lists for each type. we assume that the
    // order is the same on client and server.
    void Awake() {
        // go through each networkbehaviour component
        foreach (var component in GetComponents<NetworkBehaviour>()) {
            // find all the custom syncvars
            foreach (var field in GetFieldsWithAttribute(component.GetType(), typeof(SyncVar_Attribute))) {
                //Debug.LogWarning(component.GetType() + " => " + field + "=>" + field.GetValue(component));
                // add them to the field lists
                if (field.FieldType == typeof(string))
                    stringFields.Add(new FieldInfoAndObject(field, component));
                else if (field.FieldType == typeof(float))
                    floatFields.Add(new FieldInfoAndObject(field, component));
                else if (field.FieldType == typeof(int))
                    intFields.Add(new FieldInfoAndObject(field, component));
                else if (field.FieldType == typeof(uint))
                    uintFields.Add(new FieldInfoAndObject(field, component));
                else if (field.FieldType == typeof(long))
                    longFields.Add(new FieldInfoAndObject(field, component));
                else if (field.FieldType == typeof(bool))
                    boolFields.Add(new FieldInfoAndObject(field, component));
                else Debug.LogError("Unsupported [SyncVar_] type: " + field);
            }
        }
    }

    // server: populate the synclists with the field values
    public override void OnStartServer() {
        foreach (var fieldAndObject in stringFields) {
            //print("add string: " + fieldAndObject.field.GetValue(fieldAndObject.obj));
            strings.Add((string)fieldAndObject.field.GetValue(fieldAndObject.obj));
        }

        foreach (var fieldAndObject in floatFields) {
            //print("add float: " + fieldAndObject.field.GetValue(fieldAndObject.obj));
            floats.Add((float)fieldAndObject.field.GetValue(fieldAndObject.obj));
        }

        foreach (var fieldAndObject in intFields) {
            //print("add int: " + fieldAndObject.field.GetValue(fieldAndObject.obj));
            ints.Add((int)fieldAndObject.field.GetValue(fieldAndObject.obj));
        }

        foreach (var fieldAndObject in uintFields) {
            //print("add uint: " + fieldAndObject.field.GetValue(fieldAndObject.obj));
            uints.Add((uint)fieldAndObject.field.GetValue(fieldAndObject.obj));
        }

        foreach (var fieldAndObject in longFields) {
            //print("add long: " + fieldAndObject.field.GetValue(fieldAndObject.obj));
            longs.Add(new LongWrapper((long)fieldAndObject.field.GetValue(fieldAndObject.obj)));
        }

        foreach (var fieldAndObject in boolFields) {
            //print("add bool: " + fieldAndObject.field.GetValue(fieldAndObject.obj));
            bools.Add((bool)fieldAndObject.field.GetValue(fieldAndObject.obj));
        }
    }

    // server: copy field values to synclists all the time
    // (still works if obj becomes null)
    [ServerCallback]
    void Update() {
        for (int i = 0; i < stringFields.Count; ++i) {
            var fieldAndObject = stringFields[i];
            string value = (string)fieldAndObject.field.GetValue(fieldAndObject.obj);
            // only update if changed. don't mess with dirty flags.
            if (strings[i] != value) strings[i] = value;
        }

        for (int i = 0; i < floatFields.Count; ++i) {
            var fieldAndObject = floatFields[i];
            float value = (float)fieldAndObject.field.GetValue(fieldAndObject.obj);
            // only update if changed. don't mess with dirty flags.
            if (floats[i] != value) floats[i] = value;
        }

        for (int i = 0; i < intFields.Count; ++i) {
            var fieldAndObject = intFields[i];
            int value = (int)fieldAndObject.field.GetValue(fieldAndObject.obj);
            // only update if changed. don't mess with dirty flags.
            if (ints[i] != value) ints[i] = value;
        }

        for (int i = 0; i < uintFields.Count; ++i) {
            var fieldAndObject = uintFields[i];
            uint value = (uint)fieldAndObject.field.GetValue(fieldAndObject.obj);
            // only update if changed. don't mess with dirty flags.
            if (uints[i] != value) uints[i] = value;
        }

        for (int i = 0; i < longFields.Count; ++i) {
            var fieldAndObject = longFields[i];
            long value = (long)fieldAndObject.field.GetValue(fieldAndObject.obj);
            // only update if changed. don't mess with dirty flags.
            if (longs[i].value != value) longs[i] = new LongWrapper(value);
        }

        for (int i = 0; i < boolFields.Count; ++i) {
            var fieldAndObject = boolFields[i];
            bool value = (bool)fieldAndObject.field.GetValue(fieldAndObject.obj);
            // only update if changed. don't mess with dirty flags.
            if (bools[i] != value) bools[i] = value;
        }
    }

    // client: hook synclists and update fields when changed
    // (still works if obj becomes null)
    // we also call all hooks once to apply initial values
    // e.g. if a syncvar was set when loading from database before starting
    public override void OnStartClient() {
        strings.Callback += OnStringsChanged;
        for (int i = 0; i < strings.Count; ++i)
            OnStringsChanged(SyncListString.Operation.OP_ADD, i);

        floats.Callback += OnFloatsChanged;
        for (int i = 0; i < floats.Count; ++i)
            OnFloatsChanged(SyncListFloat.Operation.OP_ADD, i);
 
        ints.Callback += OnIntsChanged;
        for (int i = 0; i < ints.Count; ++i)
            OnIntsChanged(SyncListInt.Operation.OP_ADD, i);

        uints.Callback += OnUIntsChanged;
        for (int i = 0; i < uints.Count; ++i)
            OnUIntsChanged(SyncListUInt.Operation.OP_ADD, i);
 
        longs.Callback += OnLongsChanged;
        for (int i = 0; i < longs.Count; ++i)
            OnLongsChanged(SyncListLongWrapper.Operation.OP_ADD, i);
 
        bools.Callback += OnBoolsChanged;
        for (int i = 0; i < bools.Count; ++i)
            OnBoolsChanged(SyncListBool.Operation.OP_ADD, i);
    }

    void OnStringsChanged(SyncListString.Operation op, int index) {
        var fieldAndObject = stringFields[index];
        string value = strings[index];
        fieldAndObject.field.SetValue(fieldAndObject.obj, value);
        //print("STRING CHANGED: " + fieldAndObject.field + " => " + value);
    }

    void OnFloatsChanged(SyncListFloat.Operation op, int index) {
        var fieldAndObject = floatFields[index];
        float value = floats[index];
        fieldAndObject.field.SetValue(fieldAndObject.obj, value);
        //print("FLOAT CHANGED: " + fieldAndObject.field + " => " + value);
    }

    void OnIntsChanged(SyncListInt.Operation op, int index) {
        var fieldAndObject = intFields[index];
        int value = ints[index];
        fieldAndObject.field.SetValue(fieldAndObject.obj, value);
        //print("INT CHANGED: " + fieldAndObject.field + " => " + value);
    }

    void OnUIntsChanged(SyncListUInt.Operation op, int index) {
        var fieldAndObject = uintFields[index];
        uint value = uints[index];
        fieldAndObject.field.SetValue(fieldAndObject.obj, value);
        //print("UINT CHANGED: " + fieldAndObject.field + " => " + value);
    }

    void OnLongsChanged(SyncListLongWrapper.Operation op, int index) {
        var fieldAndObject = longFields[index];
        long value = longs[index].value;
        fieldAndObject.field.SetValue(fieldAndObject.obj, value);
        //print("LONG CHANGED: " + fieldAndObject.field + " => " + value);
    }

    void OnBoolsChanged(SyncListBool.Operation op, int index) {
        var fieldAndObject = boolFields[index];
        bool value = bools[index];
        fieldAndObject.field.SetValue(fieldAndObject.obj, value);
        //print("BOOL CHANGED: " + fieldAndObject.field + " => " + value);
    }
}
  1. Use [SyncVar_] instead of [SyncVar] after reaching the limit. Example:
[SyncVar_] int health = 0;
  1. Make sure that each Prefab that uses [SyncVar_] also has the above Script attached to it.

Enjoy!

5 Likes

that just works great, hope someone from unet team will see that.

1 Like

@[vis2k]( UNET 32 [SyncVar] Limit Workaround members/vis2k.926794/) i am facing a problem with syncVar and Command both. I have used syncVar and command attributes in my project and they are working fine but when i am adding one more at that time i m getting a problem the value are not syncing over network not even from Client to server and server to client. When i go through your answer i got to know that there will be a limit for using SyncVars but my question is what about the Command. Please help me out this got stuck. :frowning:

This solution is outdated, try my HLAPI Pro library: Mirror - Open Source Networking for Unity to get ‘true’ 64 SyncVars without workarounds.

2 Likes