Mirror Networking
SyncList.cs
1using System;
2using System.Collections;
3using System.Collections.Generic;
4
5namespace Mirror
6{
7 public class SyncList<T> : SyncObject, IList<T>, IReadOnlyList<T>
8 {
9 public delegate void SyncListChanged(Operation op, int itemIndex, T oldItem, T newItem);
10
11 readonly IList<T> objects;
12 readonly IEqualityComparer<T> comparer;
13
14 public int Count => objects.Count;
15 public bool IsReadOnly { get; private set; }
16 public event SyncListChanged Callback;
17
18 public enum Operation : byte
19 {
20 OP_ADD,
21 OP_CLEAR,
22 OP_INSERT,
23 OP_REMOVEAT,
24 OP_SET
25 }
26
27 struct Change
28 {
29 internal Operation operation;
30 internal int index;
31 internal T item;
32 }
33
34 // list of changes.
35 // -> insert/delete/clear is only ONE change
36 // -> changing the same slot 10x caues 10 changes.
37 // -> note that this grows until next sync(!)
38 readonly List<Change> changes = new List<Change>();
39
40 // how many changes we need to ignore
41 // this is needed because when we initialize the list,
42 // we might later receive changes that have already been applied
43 // so we need to skip them
44 int changesAhead;
45
46 public SyncList() : this(EqualityComparer<T>.Default) {}
47
48 public SyncList(IEqualityComparer<T> comparer)
49 {
50 this.comparer = comparer ?? EqualityComparer<T>.Default;
51 objects = new List<T>();
52 }
53
54 public SyncList(IList<T> objects, IEqualityComparer<T> comparer = null)
55 {
56 this.comparer = comparer ?? EqualityComparer<T>.Default;
57 this.objects = objects;
58 }
59
60 // throw away all the changes
61 // this should be called after a successful sync
62 public override void ClearChanges() => changes.Clear();
63
64 public override void Reset()
65 {
66 IsReadOnly = false;
67 changes.Clear();
68 changesAhead = 0;
69 objects.Clear();
70 }
71
72 void AddOperation(Operation op, int itemIndex, T oldItem, T newItem)
73 {
74 if (IsReadOnly)
75 {
76 throw new InvalidOperationException("Synclists can only be modified at the server");
77 }
78
79 Change change = new Change
80 {
81 operation = op,
82 index = itemIndex,
83 item = newItem
84 };
85
86 if (IsRecording())
87 {
88 changes.Add(change);
89 OnDirty?.Invoke();
90 }
91
92 Callback?.Invoke(op, itemIndex, oldItem, newItem);
93 }
94
95 public override void OnSerializeAll(NetworkWriter writer)
96 {
97 // if init, write the full list content
98 writer.WriteUInt((uint)objects.Count);
99
100 for (int i = 0; i < objects.Count; i++)
101 {
102 T obj = objects[i];
103 writer.Write(obj);
104 }
105
106 // all changes have been applied already
107 // thus the client will need to skip all the pending changes
108 // or they would be applied again.
109 // So we write how many changes are pending
110 writer.WriteUInt((uint)changes.Count);
111 }
112
113 public override void OnSerializeDelta(NetworkWriter writer)
114 {
115 // write all the queued up changes
116 writer.WriteUInt((uint)changes.Count);
117
118 for (int i = 0; i < changes.Count; i++)
119 {
120 Change change = changes[i];
121 writer.WriteByte((byte)change.operation);
122
123 switch (change.operation)
124 {
125 case Operation.OP_ADD:
126 writer.Write(change.item);
127 break;
128
129 case Operation.OP_CLEAR:
130 break;
131
132 case Operation.OP_REMOVEAT:
133 writer.WriteUInt((uint)change.index);
134 break;
135
136 case Operation.OP_INSERT:
137 case Operation.OP_SET:
138 writer.WriteUInt((uint)change.index);
139 writer.Write(change.item);
140 break;
141 }
142 }
143 }
144
145 public override void OnDeserializeAll(NetworkReader reader)
146 {
147 // This list can now only be modified by synchronization
148 IsReadOnly = true;
149
150 // if init, write the full list content
151 int count = (int)reader.ReadUInt();
152
153 objects.Clear();
154 changes.Clear();
155
156 for (int i = 0; i < count; i++)
157 {
158 T obj = reader.Read<T>();
159 objects.Add(obj);
160 }
161
162 // We will need to skip all these changes
163 // the next time the list is synchronized
164 // because they have already been applied
165 changesAhead = (int)reader.ReadUInt();
166 }
167
168 public override void OnDeserializeDelta(NetworkReader reader)
169 {
170 // This list can now only be modified by synchronization
171 IsReadOnly = true;
172
173 int changesCount = (int)reader.ReadUInt();
174
175 for (int i = 0; i < changesCount; i++)
176 {
177 Operation operation = (Operation)reader.ReadByte();
178
179 // apply the operation only if it is a new change
180 // that we have not applied yet
181 bool apply = changesAhead == 0;
182 int index = 0;
183 T oldItem = default;
184 T newItem = default;
185
186 switch (operation)
187 {
188 case Operation.OP_ADD:
189 newItem = reader.Read<T>();
190 if (apply)
191 {
192 index = objects.Count;
193 objects.Add(newItem);
194 }
195 break;
196
197 case Operation.OP_CLEAR:
198 if (apply)
199 {
200 objects.Clear();
201 }
202 break;
203
204 case Operation.OP_INSERT:
205 index = (int)reader.ReadUInt();
206 newItem = reader.Read<T>();
207 if (apply)
208 {
209 objects.Insert(index, newItem);
210 }
211 break;
212
213 case Operation.OP_REMOVEAT:
214 index = (int)reader.ReadUInt();
215 if (apply)
216 {
217 oldItem = objects[index];
218 objects.RemoveAt(index);
219 }
220 break;
221
222 case Operation.OP_SET:
223 index = (int)reader.ReadUInt();
224 newItem = reader.Read<T>();
225 if (apply)
226 {
227 oldItem = objects[index];
228 objects[index] = newItem;
229 }
230 break;
231 }
232
233 if (apply)
234 {
235 Callback?.Invoke(operation, index, oldItem, newItem);
236 }
237 // we just skipped this change
238 else
239 {
240 changesAhead--;
241 }
242 }
243 }
244
245 public void Add(T item)
246 {
247 objects.Add(item);
248 AddOperation(Operation.OP_ADD, objects.Count - 1, default, item);
249 }
250
251 public void AddRange(IEnumerable<T> range)
252 {
253 foreach (T entry in range)
254 {
255 Add(entry);
256 }
257 }
258
259 public void Clear()
260 {
261 objects.Clear();
262 AddOperation(Operation.OP_CLEAR, 0, default, default);
263 }
264
265 public bool Contains(T item) => IndexOf(item) >= 0;
266
267 public void CopyTo(T[] array, int index) => objects.CopyTo(array, index);
268
269 public int IndexOf(T item)
270 {
271 for (int i = 0; i < objects.Count; ++i)
272 if (comparer.Equals(item, objects[i]))
273 return i;
274 return -1;
275 }
276
277 public int FindIndex(Predicate<T> match)
278 {
279 for (int i = 0; i < objects.Count; ++i)
280 if (match(objects[i]))
281 return i;
282 return -1;
283 }
284
285 public T Find(Predicate<T> match)
286 {
287 int i = FindIndex(match);
288 return (i != -1) ? objects[i] : default;
289 }
290
291 public List<T> FindAll(Predicate<T> match)
292 {
293 List<T> results = new List<T>();
294 for (int i = 0; i < objects.Count; ++i)
295 if (match(objects[i]))
296 results.Add(objects[i]);
297 return results;
298 }
299
300 public void Insert(int index, T item)
301 {
302 objects.Insert(index, item);
303 AddOperation(Operation.OP_INSERT, index, default, item);
304 }
305
306 public void InsertRange(int index, IEnumerable<T> range)
307 {
308 foreach (T entry in range)
309 {
310 Insert(index, entry);
311 index++;
312 }
313 }
314
315 public bool Remove(T item)
316 {
317 int index = IndexOf(item);
318 bool result = index >= 0;
319 if (result)
320 {
321 RemoveAt(index);
322 }
323 return result;
324 }
325
326 public void RemoveAt(int index)
327 {
328 T oldItem = objects[index];
329 objects.RemoveAt(index);
330 AddOperation(Operation.OP_REMOVEAT, index, oldItem, default);
331 }
332
333 public int RemoveAll(Predicate<T> match)
334 {
335 List<T> toRemove = new List<T>();
336 for (int i = 0; i < objects.Count; ++i)
337 if (match(objects[i]))
338 toRemove.Add(objects[i]);
339
340 foreach (T entry in toRemove)
341 {
342 Remove(entry);
343 }
344
345 return toRemove.Count;
346 }
347
348 public T this[int i]
349 {
350 get => objects[i];
351 set
352 {
353 if (!comparer.Equals(objects[i], value))
354 {
355 T oldItem = objects[i];
356 objects[i] = value;
357 AddOperation(Operation.OP_SET, i, oldItem, value);
358 }
359 }
360 }
361
362 public Enumerator GetEnumerator() => new Enumerator(this);
363
364 IEnumerator<T> IEnumerable<T>.GetEnumerator() => new Enumerator(this);
365
366 IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
367
368 // default Enumerator allocates. we need a custom struct Enumerator to
369 // not allocate on the heap.
370 // (System.Collections.Generic.List<T> source code does the same)
371 //
372 // benchmark:
373 // uMMORPG with 800 monsters, Skills.GetHealthBonus() which runs a
374 // foreach on skills SyncList:
375 // before: 81.2KB GC per frame
376 // after: 0KB GC per frame
377 // => this is extremely important for MMO scale networking
378 public struct Enumerator : IEnumerator<T>
379 {
380 readonly SyncList<T> list;
381 int index;
382 public T Current { get; private set; }
383
384 public Enumerator(SyncList<T> list)
385 {
386 this.list = list;
387 index = -1;
388 Current = default;
389 }
390
391 public bool MoveNext()
392 {
393 if (++index >= list.Count)
394 {
395 return false;
396 }
397 Current = list[index];
398 return true;
399 }
400
401 public void Reset() => index = -1;
402 object IEnumerator.Current => Current;
403 public void Dispose() {}
404 }
405 }
406}
Network Reader for most simple types like floats, ints, buffers, structs, etc. Use NetworkReaderPool....
Network Writer for most simple types like floats, ints, buffers, structs, etc. Use NetworkWriterPool....
override void Reset()
Resets the SyncObject so that it can be re-used
Definition: SyncList.cs:64
override void OnSerializeAll(NetworkWriter writer)
Write a full copy of the object
Definition: SyncList.cs:95
override void OnDeserializeDelta(NetworkReader reader)
Reads the changes made to the object since last sync
Definition: SyncList.cs:168
override void OnSerializeDelta(NetworkWriter writer)
Write the changes made to the object since last sync
Definition: SyncList.cs:113
override void OnDeserializeAll(NetworkReader reader)
Reads a full copy of the object
Definition: SyncList.cs:145
override void ClearChanges()
Discard all the queued changes
SyncObjects sync state between server and client. E.g. SyncLists.
Definition: SyncObject.cs:14
Action OnDirty
Used internally to set owner NetworkBehaviour's dirty mask bit when changed.
Definition: SyncObject.cs:16
Func< bool > IsRecording
Used internally to check if we are currently tracking changes.
Definition: SyncObject.cs:26