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:
- 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);
}
}
- Use [SyncVar_] instead of [SyncVar] after reaching the limit. Example:
[SyncVar_] int health = 0;
- Make sure that each Prefab that uses [SyncVar_] also has the above Script attached to it.
Enjoy!