该项目的目的是同时测试和演示来自 Unity DOTS 技术堆栈的多个新包。
您最多选择25个主题 主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

1135 行
48 KiB

using System.Collections.Generic;
using UnityEngine;
using NetworkCompression;
using UnityEngine.Profiling;
using System.Net;
using Unity.Entities;
using Unity.Networking.Transport;
using Unity.NetCode;
using Unity.Sample.Core;
using NetworkCompressionModel = NetworkCompression.NetworkCompressionModel;
public interface ISnapshotConsumer
{
void ProcessEntityDespawns(int serverTime, List<int> despawns);
void ProcessEntitySpawn(int serverTime, int id, ushort typeId);
void ProcessEntityUpdate(int serverTime, int id, ref NetworkReader reader);
}
public interface INetworkClientCallbacks : INetworkCallbacks
{
void OnMapUpdate(ref NetworkReader data);
}
public class NetworkClient
{
// Hack to allow thin clients to drop snapshots after having read initial bit
public static bool m_DropSnapshots = false;
[ConfigVar(Name = "client.debug", DefaultValue = "0", Description = "Enable debug printing of client handshake etc.", Flags = ConfigVar.Flags.None)]
public static ConfigVar clientDebug;
[ConfigVar(Name = "client.blockin", DefaultValue = "0", Description = "Cut next N incoming network packges. -1 means forever.", Flags = ConfigVar.Flags.None)]
public static ConfigVar clientBlockIn;
[ConfigVar(Name = "client.blockout", DefaultValue = "0", Description = "Cut all outgoing network traffic.", Flags = ConfigVar.Flags.None)]
public static ConfigVar clientBlockOut;
[ConfigVar(Name = "client.verifyprotocol", DefaultValue = "1", Description = "Verify protocol match when connecting to server.", Flags = ConfigVar.Flags.None)]
public static ConfigVar clientVerifyProtocol;
public enum ConnectionState
{
Disconnected,
Connecting,
Connected
}
// Sent from client to server when changed
public class ClientConfig
{
public int serverUpdateRate; // max bytes/sec
public int serverUpdateInterval; // requested tick / update
}
public struct PackageSectionStats
{
public string sectionName;
public int sectionStart;
public int sectionLength;
public Color color;
}
public class Counters : NetworkConnectionCounters
{
public int snapshotsIn; // Total number of snapshots received
public int fullSnapshotsIn; // Number of snapshots without a baseline
public int commandsOut; // Number of command messages sent
public List<PackageSectionStats>[] packageContentStats = new List<PackageSectionStats>[64]; // Breakdown of bits spent in package
public int packageContentStatsPackageSequence; // incrementing when packageContentStats is filled
int lastOffset;
public void ClearSectionStats()
{
var idx = packagesIn % packageContentStats.Length;
if (packageContentStats[idx] == null)
packageContentStats[idx] = new List<PackageSectionStats>();
else
packageContentStats[idx].Clear();
lastOffset = 0;
}
public void AddSectionStats(string name, int offset, Color col)
{
packageContentStats[packagesIn % packageContentStats.Length].Add(new PackageSectionStats { sectionName = name, sectionStart = lastOffset, sectionLength = offset - lastOffset, color = col });
lastOffset = offset;
}
}
public Counters counters
{
get { return m_Connection != null ? m_Connection.counters : null; }
}
public bool isConnected
{
get { return m_Connection != null && m_Connection.connectionState == ConnectionState.Connected; }
}
public ConnectionState connectionState
{
get { return m_Connection != null ? m_Connection.connectionState : ConnectionState.Disconnected; }
}
public ClientConfig clientConfig;
public void UpdateClientId(int id)
{
m_Connection.clientId = id;
}
public int clientId { get { return m_Connection != null ? m_Connection.clientId : -1; } }
public int serverTime { get { return m_Connection != null ? m_Connection.serverTime : -1; } }
public int serverTickRate { get { return m_Connection != null ? m_Connection.serverTickRate : 60; } }
public int lastAcknowlegdedCommandTime { get { return m_Connection != null ? m_Connection.lastAcknowlegdedCommandTime : -1; } }
public float serverSimTime { get { return m_Connection != null ? m_Connection.serverSimTime : 0.0f; } }
public int rtt { get { return m_Connection != null ? m_Connection.rtt : 0; } }
public float timeSinceSnapshot { get { return m_Connection != null ? NetworkUtils.stopwatch.ElapsedMilliseconds - m_Connection.snapshotReceivedTime : -1; } }
public delegate void DataGenerator(ref NetworkWriter data);
public delegate void MapUpdateProcessor(ref NetworkReader data);
public delegate void EntitySpawnProcessor(int id, ushort typeId);
public delegate void EntityDespawnProcessor(int id);
public delegate void EntityUpdateProcessor(int id, ref NetworkReader data);
public NetworkClient(INetworkTransport transport)
{
m_Transport = transport;
clientConfig = new ClientConfig();
}
public void Shutdown()
{
Disconnect();
}
internal void UpdateClientConfig()
{
Profiler.BeginSample("NetworkClient.UpdateClientConfig");
clientConfig.serverUpdateRate = ClientGameLoop.clientUpdateRate.IntValue;
clientConfig.serverUpdateInterval = ClientGameLoop.clientUpdateInterval.IntValue;
if (m_Connection != null)
m_Connection.ClientConfigChanged();
Profiler.EndSample();
}
public bool Connect(string endpoint)
{
if (m_Connection != null)
{
GameDebug.Log("Must be disconnected before reconnecting");
return false;
}
IPAddress ipAddress;
int port;
if (!NetworkUtils.EndpointParse(endpoint, out ipAddress, out port, NetworkConfig.defaultServerPort))
{
GameDebug.Log("Invalid endpoint: " + endpoint);
return false;
}
int connectionId = -1;
var ep = NetworkEndPoint.Parse(ipAddress.ToString(), (ushort)(port+1));
foreach (var world in World.AllWorlds)
{
if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
{
var ent = world.GetExistingSystem<NetworkStreamReceiveSystem>().Connect(ep);
//connectionId = world.EntityManager.GetComponentData<NetworkStreamConnection>(ent).Value.InternalId;
}
}
m_Connection = new ClientConnection(connectionId, m_Transport, clientConfig);
return true;
}
public void Disconnect()
{
if (m_Connection == null)
return;
// Force transport layer to disconnect
m_Transport.Disconnect(m_Connection.connectionId);
// Note, we have to call OnDisconnect manually as disconnecting forcefully like this does not
// generate an disconnect event from the transport layer
OnDisconnect(m_Connection.connectionId);
}
public void QueueCommand(int time, DataGenerator generator)
{
if (m_Connection == null)
return;
m_Connection.QueueCommand(time, generator);
}
public void QueueEvent(ushort typeId, bool reliable, NetworkEventGenerator generator)
{
if (m_Connection == null)
return;
var e = NetworkEvent.Serialize(typeId, reliable, m_EventTypesOut, generator);
m_Connection.QueueEvent(e);
e.Release();
}
public void ProcessMapUpdate(INetworkClientCallbacks processor)
{
if (m_Connection == null)
return;
m_Connection.ProcessMapUpdate(processor);
}
public void Update(INetworkClientCallbacks clientNetworkConsumer, ISnapshotConsumer snapshotConsumer)
{
Profiler.BeginSample("NetworkClient.Update");
m_Transport.Update();
// Debug tracking of outstanding events
if (NetworkConfig.netDebug.IntValue > 1 && m_Connection != null)
{
var outstandingPackages = m_Connection.outstandingPackages;
for (var i = 0; i < outstandingPackages.m_Elements.Length; i++)
{
if (outstandingPackages.m_Sequences[i] != -1 && outstandingPackages.m_Elements[i].events.Count > 0)
GameDebug.Log("Outstanding Package: " + i + " (idx), " + outstandingPackages.m_Sequences[i] + " (seq), " + outstandingPackages.m_Elements[i].events.Count + " (numevs), " + ((GameNetworkEvents.EventType)outstandingPackages.m_Elements[i].events[0].type.typeId) + " (ev0)");
}
}
TransportEvent e = new TransportEvent();
while (m_Transport.NextEvent(ref e))
{
switch (e.type)
{
case TransportEvent.Type.Connect:
OnConnect(e.connectionId);
break;
case TransportEvent.Type.Disconnect:
OnDisconnect(e.connectionId);
break;
case TransportEvent.Type.Data:
OnData(e.connectionId, e.data, e.dataSize, clientNetworkConsumer, snapshotConsumer);
break;
}
}
if (m_Connection != null)
m_Connection.ProcessMapUpdate(clientNetworkConsumer);
Profiler.EndSample();
}
public void SendData()
{
if (m_Connection == null || m_Connection.connectionState == ConnectionState.Disconnected)
return;
if (clientBlockOut.IntValue > 0)
return;
Profiler.BeginSample("NetworkClient.SendData");
#pragma warning disable 0162 // unreachable code
switch (NetworkConfig.ioStreamType)
{
case NetworkCompression.IOStreamType.Raw:
m_Connection.SendPackage<RawOutputStream>();
break;
case NetworkCompression.IOStreamType.Huffman:
m_Connection.SendPackage<HuffmanOutputStream>();
break;
default:
GameDebug.Assert(false);
}
#pragma warning restore
Profiler.EndSample();
}
void OnConnect(int connectionId)
{
if (m_Connection != null && m_Connection.connectionId == connectionId)
GameDebug.Assert(connectionState == ConnectionState.Connecting);
}
void OnDisconnect(int connectionId)
{
if (m_Connection != null && m_Connection.connectionId == connectionId)
{
if (m_Connection.connectionState == ConnectionState.Connected)
{
GameDebug.Log("Disconnected from server");
GameDebug.Log(string.Format("Last package sent : {0} . Last package received {1} {2} ms ago",
m_Connection.outSequence,
m_Connection.inSequence,
NetworkUtils.stopwatch.ElapsedMilliseconds - m_Connection.inSequenceTime));
}
else if (m_Connection.connectionState == ConnectionState.Connecting)
GameDebug.Log("Server never replied when trying to connect ... disconnecting");
m_Connection.Reset();
m_Connection = null;
}
}
void OnData(int connectionId, byte[] data, int size, INetworkClientCallbacks networkClientConsumer, ISnapshotConsumer snapshotConsumer)
{
// Block A number of incoming packets. -1 for all
if (clientBlockIn.IntValue > 0)
{
clientBlockIn.Value = (clientBlockIn.IntValue - 1).ToString();
}
if (clientBlockIn.IntValue != 0)
return;
// SHould these be asserts?
if (m_Connection != null && m_Connection.connectionId == connectionId)
{
#pragma warning disable 0162 // unreached code
switch (NetworkConfig.ioStreamType)
{
case NetworkCompression.IOStreamType.Raw:
{
m_Connection.ReadPackage<RawInputStream>(data, size, m_Connection.compressionModel, networkClientConsumer, snapshotConsumer);
break;
}
case NetworkCompression.IOStreamType.Huffman:
{
m_Connection.ReadPackage<HuffmanInputStream>(data, size, m_Connection.compressionModel, networkClientConsumer, snapshotConsumer);
break;
}
default:
GameDebug.Assert(false);
break;
}
#pragma warning restore
}
}
class ClientPackageInfo : PackageInfo
{
public int commandTime;
public int commandSequence;
public override void Reset()
{
base.Reset();
commandTime = 0;
commandSequence = 0;
}
}
class ClientConnection : NetworkConnection<NetworkClient.Counters, ClientPackageInfo>
{
public long snapshotReceivedTime; // Time we received the last snapshot
public float serverSimTime; // Server simulation time (actualy time spent doing simulation regardless of tickrate)
ClientConfig clientConfig;
public ClientConnection(int connectionId, INetworkTransport transport, ClientConfig clientConfig) : base(connectionId, transport)
{
this.clientConfig = clientConfig;
}
public override void Reset()
{
base.Reset();
serverTime = 0;
entities.Clear();
spawns.Clear();
despawns.Clear();
updates.Clear();
}
public void ClientConfigChanged()
{
sendClientConfig = true;
}
unsafe public void QueueCommand(int time, DataGenerator generator)
{
var generateSchema = (commandSchema == null);
if (generateSchema)
commandSchema = new NetworkSchema(NetworkConfig.networkClientQueueCommandSchemaId);
var info = commandsOut.Acquire(++commandSequence);
info.time = time;
fixed(uint* buf = info.data)
{
var writer = new NetworkWriter(buf, info.data.Length, commandSchema, generateSchema);
generator(ref writer);
writer.Flush();
}
}
unsafe public void ProcessMapUpdate(INetworkClientCallbacks loop)
{
if (mapInfo.mapSequence > 0 && !mapInfo.processed)
{
fixed(uint* data = mapInfo.data)
{
var reader = new NetworkReader(data, mapInfo.schema);
loop.OnMapUpdate(ref reader);
mapInfo.processed = true;
}
}
}
public void ReadPackage<TInputStream>(byte[] packageData, int packageSize, NetworkCompressionModel compressionModel, INetworkClientCallbacks networkClientConsumer, ISnapshotConsumer snapshotConsumer) where TInputStream : struct, NetworkCompression.IInputStream
{
counters.bytesIn += packageSize;
NetworkMessage content;
byte[] assembledData;
int assembledSize;
int headerSize;
var packageSequence = ProcessPackageHeader(packageData, packageSize, out content, out assembledData, out assembledSize, out headerSize);
// Reset stats
counters.ClearSectionStats();
counters.AddSectionStats("header", headerSize * 8, Color.white);
// The package was dropped (duplicate or too old) or if it was a fragment not yet assembled, bail out here
if (packageSequence == 0)
return;
var input = default(TInputStream); // new TInputStream(); due to bug new generates garbage here
input.Initialize(compressionModel, assembledData, headerSize);
if ((content & NetworkMessage.ClientInfo) != 0)
ReadClientInfo(ref input);
counters.AddSectionStats("clientInfo", input.GetBitPosition2(), Color.green);
if ((content & NetworkMessage.MapInfo) != 0)
ReadMapInfo(ref input);
counters.AddSectionStats("mapInfo", input.GetBitPosition2(), new Color(0.65f, 0.16f, 0.16f));
// The package was received out of order but older than the last map reset,
// so we ignore the remaining data
if (mapInfo.ackSequence == 0 || packageSequence < mapInfo.ackSequence)
return;
if ((content & NetworkMessage.Snapshot) != 0)
{
ReadSnapshot(packageSequence, ref input, snapshotConsumer);
// Make sure the callback actually picked up the snapshot data. It is important that
// every snapshot gets processed by the game so that the spawns, despawns and updates lists
// does not end up containing stuff from different snapshots
GameDebug.Assert(spawns.Count == 0 && despawns.Count == 0 && updates.Count == 0, "Game did not consume snapshots");
}
// We have to skip this if we dropped snapshot as we will then be in the middle of the input stream
if ((content & NetworkMessage.Events) != 0 && !m_DropSnapshots)
ReadEvents(ref input, networkClientConsumer);
counters.AddSectionStats("events", input.GetBitPosition2(), new Color(1.0f, 0.84f, 0.0f));
counters.AddSectionStats("unknown", assembledSize * 8, Color.black);
counters.packageContentStatsPackageSequence = packageSequence;
}
public void SendPackage<TOutputStream>() where TOutputStream : struct, NetworkCompression.IOutputStream
{
// We don't start sending updates before we have received at
// least one content package from the server
var rawOutputStream = new BitOutputStream(m_PackageBuffer);
if (inSequence == 0 || !CanSendPackage(ref rawOutputStream))
return;
// Only if there is anything to send
// TODO (petera) should we send empty packages at a low frequency?
if (sendClientConfig == false && commandSequence > 0 && commandSequence <= lastSentCommandSeq && eventsOut.Count == 0)
return;
ClientPackageInfo info;
BeginSendPackage(ref rawOutputStream, out info);
int endOfHeaderPos = rawOutputStream.Align();
var output = default(TOutputStream); // new TOutputStream(); due to bug new generate garbage here
output.Initialize(NetworkCompressionModel.DefaultModel, m_PackageBuffer, endOfHeaderPos, null);
if (sendClientConfig)
WriteClientConfig(ref output);
if (commandSequence > 0)
{
lastSentCommandSeq = commandSequence;
WriteCommands(info, ref output);
}
WriteEvents(info, ref output);
int compressedSize = output.Flush();
rawOutputStream.SkipBytes(compressedSize);
CompleteSendPackage(info, ref rawOutputStream);
}
byte[] modelData = new byte[32];
void ReadClientInfo<TInputStream>(ref TInputStream input) where TInputStream : NetworkCompression.IInputStream
{
var newClientId = (int)input.ReadRawBits(8);
serverTickRate = (int)input.ReadRawBits(8); // TODO (petera) remove this from here. This should only be handshake code. tickrate updated by other means like configvar
uint serverProtocol = input.ReadRawBits(8);
int modelSize = (int)input.ReadRawBits(16);
if (modelSize > modelData.Length)
modelData = new byte[modelSize];
for (int i = 0; i < modelSize; i++)
modelData[i] = (byte)input.ReadRawBits(8);
if (connectionState == ConnectionState.Connected)
{
GameDebug.Log("Ignoring ClientInfo as we are already connected");
return;
}
uint ourProtocol = NetworkConfig.protocolVersion;
GameDebug.Log("Client protocol id: " + ourProtocol);
GameDebug.Log("Server protocol id: " + serverProtocol);
if (ourProtocol != serverProtocol)
{
if (clientVerifyProtocol.IntValue > 0)
{
GameDebug.LogError("Protocol mismatch. Server is: " + serverProtocol + " and we are: " + ourProtocol);
connectionState = ConnectionState.Disconnected;
return;
}
GameDebug.Log("Ignoring protocol difference client.verifyprotocol is 0");
}
GameDebug.Assert(clientId == -1 || newClientId == clientId, "Repeated client info didn't match existing client id");
compressionModel = new NetworkCompressionModel(modelData);
clientId = newClientId;
connectionState = ConnectionState.Connected;
if (clientDebug.IntValue > 0)
{
GameDebug.Log(string.Format("ReadClientInfo: clientId {0} serverTickRate {1}", newClientId, serverTickRate));
}
}
void ReadMapInfo<TInputStream>(ref TInputStream input) where TInputStream : NetworkCompression.IInputStream
{
//input.SetStatsType(NetworkCompressionReader.Type.MapInfo);
var mapSequence = (ushort)input.ReadRawBits(16);
var schemaIncluded = input.ReadRawBits(1) != 0;
if (schemaIncluded)
{
mapInfo.schema = NetworkSchema.ReadSchema(ref input); // might override previous definition
}
if (mapSequence > mapInfo.mapSequence)
{
mapInfo.mapSequence = mapSequence;
mapInfo.ackSequence = inSequence;
mapInfo.processed = false;
NetworkSchema.CopyFieldsToBuffer(mapInfo.schema, ref input, mapInfo.data);
Reset();
}
else
NetworkSchema.SkipFields(mapInfo.schema, ref input);
}
uint[] tempSnapshotBuffer = new uint[NetworkConfig.maxEntitySnapshotDataSize];
unsafe void ReadSnapshot<TInputStream>(int sequence, ref TInputStream input, ISnapshotConsumer consumer) where TInputStream : NetworkCompression.IInputStream
{
//input.SetStatsType(NetworkCompressionReader.Type.SnapshotSchema);
counters.snapshotsIn++;
// Snapshot may be delta compressed against one or more baselines
// Baselines are indicated by sequence number of the package it was in
var haveBaseline = input.ReadRawBits(1) == 1;
var baseSequence = (int)input.ReadPackedIntDelta(sequence - 1, NetworkConfig.baseSequenceContext);
bool enableNetworkPrediction = input.ReadRawBits(1) != 0;
bool enableHashing = input.ReadRawBits(1) != 0;
int baseSequence1 = 0;
int baseSequence2 = 0;
if (enableNetworkPrediction)
{
baseSequence1 = (int)input.ReadPackedIntDelta(baseSequence - 1, NetworkConfig.baseSequence1Context);
baseSequence2 = (int)input.ReadPackedIntDelta(baseSequence1 - 1, NetworkConfig.baseSequence2Context);
}
if (clientDebug.IntValue > 2)
{
if (enableNetworkPrediction)
GameDebug.Log((haveBaseline ? "Snap [BL]" : "Snap [ ]") + "(" + sequence + ") " + baseSequence + " - " + baseSequence1 + " - " + baseSequence2);
else
GameDebug.Log((haveBaseline ? "Snap [BL]" : "Snap [ ]") + "(" + sequence + ") " + baseSequence);
}
if (!haveBaseline)
counters.fullSnapshotsIn++;
GameDebug.Assert(!haveBaseline ||
(sequence > baseSequence && sequence - baseSequence < NetworkConfig.snapshotDeltaCacheSize), "Attempting snapshot encoding with invalid baseline: {0}:{1}", sequence, baseSequence);
var snapshotInfo = snapshots.Acquire(sequence);
snapshotInfo.serverTime = (int)input.ReadPackedIntDelta(haveBaseline ? snapshots[baseSequence].serverTime : 0, NetworkConfig.serverTimeContext);
var temp = (int)input.ReadRawBits(8);
serverSimTime = temp * 0.1f;
// Only update time if received in-order..
// TODO consider dropping out of order snapshots
// TODO detecting out-of-order on pack sequences
if (snapshotInfo.serverTime > serverTime)
{
serverTime = snapshotInfo.serverTime;
snapshotReceivedTime = NetworkUtils.stopwatch.ElapsedMilliseconds;
}
else
{
GameDebug.Log(string.Format("NetworkClient. Dropping out of order snaphot. Server time:{0} snapshot time:{1}", serverTime, snapshotInfo.serverTime));
}
counters.AddSectionStats("snapShotHeader", input.GetBitPosition2(), new Color(0.5f, 0.5f, 0.5f));
// Used by thinclient that wants to very cheaply just do minimal handling of
// snapshots
if (m_DropSnapshots)
return;
// Read schemas
var schemaCount = input.ReadPackedUInt(NetworkConfig.schemaCountContext);
for (int schemaIndex = 0; schemaIndex < schemaCount; ++schemaIndex)
{
var typeId = (ushort)input.ReadPackedUInt(NetworkConfig.schemaTypeIdContext);
var entityType = new EntityTypeInfo() { typeId = typeId };
entityType.schema = NetworkSchema.ReadSchema(ref input);
counters.AddSectionStats("snapShotSchemas", input.GetBitPosition2(), new Color(0.0f, (schemaIndex & 1) == 1 ? 0.5f : 1.0f, 1.0f));
entityType.baseline = new uint[NetworkConfig.maxEntitySnapshotDataSize];
NetworkSchema.CopyFieldsToBuffer(entityType.schema, ref input, entityType.baseline);
if (!entityTypes.ContainsKey(typeId))
entityTypes.Add(typeId, entityType);
counters.AddSectionStats("snapShotSchemas", input.GetBitPosition2(), new Color(1.0f, (schemaIndex & 1) == 1 ? 0.5f : 1.0f, 1.0f));
}
// Remove any despawning entities that belong to older base sequences
for (int i = 0; i < entities.Count; i++)
{
var e = entities[i];
if (e.type == null)
continue;
if (e.despawnSequence > 0 && e.despawnSequence <= baseSequence)
e.Reset();
}
// Read new spawns
m_TempSpawnList.Clear();
var previousId = 1;
var spawnCount = input.ReadPackedUInt(NetworkConfig.spawnCountContext);
for (var spawnIndex = 0; spawnIndex < spawnCount; ++spawnIndex)
{
var id = (int)input.ReadPackedIntDelta(previousId, NetworkConfig.idContext);
previousId = id;
// Register the entity
var typeId = (ushort)input.ReadPackedUInt(NetworkConfig.spawnTypeIdContext); //TODO: use another encoding
GameDebug.Assert(entityTypes.ContainsKey(typeId), "Spawn request with unknown type id {0}", typeId);
byte fieldMask = (byte)input.ReadRawBits(8);
// TODO (petera) need an max entity id for safety
while (id >= entities.Count)
entities.Add(new EntityInfo());
// Incoming spawn of different type than what we have for this id, so immediately nuke
// the one we have to make room for the incoming
if (entities[id].type != null && entities[id].type.typeId != typeId)
{
// This should only ever happen in case of no baseline as normally the server will
// not reuse an id before all clients have acknowledged its despawn.
GameDebug.Assert(haveBaseline == false, "Spawning entity but we already have with different type?");
GameDebug.Log("REPLACING old entity: " + id + " because snapshot gave us new type for this id");
despawns.Add(id);
entities[id].Reset();
}
// We can receive spawn information in several snapshots before our ack
// has reached the server. Only pass on spawn to game layer once
if (entities[id].type == null)
{
var e = entities[id];
e.type = entityTypes[typeId];
e.fieldMask = fieldMask;
spawns.Add(id);
}
m_TempSpawnList.Add(id);
}
counters.AddSectionStats("snapShotSpawns", input.GetBitPosition2(), new Color(0, 0.58f, 0));
// Read despawns
var despawnCount = input.ReadPackedUInt(NetworkConfig.despawnCountContext);
// If we have no baseline, we need to clear all entities that are not being spawned
if (!haveBaseline)
{
GameDebug.Assert(despawnCount == 0, "There should not be any despawns in a non-baseline snapshot");
for (int i = 0, c = entities.Count; i < c; ++i)
{
var e = entities[i];
if (e.type == null)
continue;
if (m_TempSpawnList.Contains(i))
continue;
GameDebug.Log("NO BL SO PRUNING Stale entity: " + i);
despawns.Add(i);
e.Reset();
}
}
for (var despawnIndex = 0; despawnIndex < despawnCount; ++despawnIndex)
{
var id = (int)input.ReadPackedIntDelta(previousId, NetworkConfig.idContext);
previousId = id;
// we may see despawns many times, only handle if we still have the entity
GameDebug.Assert(id < entities.Count, "Getting despawn for id {0} but we only know about entities up to {1}", id, entities.Count);
if (entities[id].type == null)
continue;
var entity = entities[id];
// Already in the process of being despawned. This happens with same-snapshot spawn/despawn cases
if (entity.despawnSequence > 0)
continue;
// If we are spawning and despawning in same snapshot, delay actual deletion of
// entity as we need it around to be able to read the update part of the snapshot
if (m_TempSpawnList.Contains(id))
entity.despawnSequence = sequence; // keep until baseSequence >= despawnSequence
else
entity.Reset(); // otherwise remove right away; no further updates coming, not even in this snap
// Add to despawns list so we can request despawn from game later
GameDebug.Assert(!despawns.Contains(id), "Double despawn in same snaphot? {0}", id);
despawns.Add(id);
}
counters.AddSectionStats("snapShotDespawns", input.GetBitPosition2(), new Color(0.49f, 0, 0));
// Predict all active entities
for (var id = 0; id < entities.Count; id++)
{
var info = entities[id];
if (info.type == null)
continue;
// NOTE : As long as the server haven't gotten the spawn acked, it will keep sending
// delta relative to 0, so we need to check if the entity was in the spawn list to determine
// if the delta is relative to the last update or not
int baseline0Time = 0;
uint[] baseline0 = info.type.baseline;
GameDebug.Assert(baseline0 != null, "Unable to find schema baseline for type {0}", info.type.typeId);
if (haveBaseline && !m_TempSpawnList.Contains(id))
{
baseline0 = info.baselines.FindMax(baseSequence);
GameDebug.Assert(baseline0 != null, "Unable to find baseline for seq {0} for id {1}", baseSequence, id);
baseline0Time = snapshots[baseSequence].serverTime;
}
if (enableNetworkPrediction)
{
uint num_baselines = 1; // 1 because either we have schema baseline or we have a real baseline
int baseline1Time = 0;
int baseline2Time = 0;
uint[] baseline1 = null;
uint[] baseline2 = null;
if (baseSequence1 != baseSequence)
{
baseline1 = info.baselines.FindMax(baseSequence1);
if (baseline1 != null)
{
num_baselines = 2;
baseline1Time = snapshots[baseSequence1].serverTime;
}
if (baseSequence2 != baseSequence1)
{
baseline2 = info.baselines.FindMax(baseSequence2);
if (baseline2 != null)
{
num_baselines = 3;
baseline2Time = snapshots[baseSequence2].serverTime;
}
}
}
// TODO (petera) are these clears needed?
for (int i = 0, c = info.fieldsChangedPrediction.Length; i < c; ++i)
info.fieldsChangedPrediction[i] = 0;
for (int i = 0; i < NetworkConfig.maxEntitySnapshotDataSize; i++)
info.prediction[i] = 0;
fixed(uint* prediction = info.prediction, baseline0p = baseline0, baseline1p = baseline1, baseline2p = baseline2)
{
NetworkPrediction.PredictSnapshot(prediction, info.fieldsChangedPrediction, info.type.schema, num_baselines, (uint)baseline0Time, baseline0p, (uint)baseline1Time, baseline1p, (uint)baseline2Time, baseline2p, (uint)snapshotInfo.serverTime, info.fieldMask);
}
}
else
{
var f = info.fieldsChangedPrediction;
for (var i = 0; i < f.Length; ++i)
f[i] = 0;
for (int i = 0, c = info.type.schema.GetByteSize() / 4; i < c; ++i)
info.prediction[i] = baseline0[i];
}
}
// Read updates
var updateCount = input.ReadPackedUInt(NetworkConfig.updateCountContext);
for (var updateIndex = 0; updateIndex < updateCount; ++updateIndex)
{
var id = (int)input.ReadPackedIntDelta(previousId, NetworkConfig.idContext);
previousId = id;
var info = entities[id];
uint hash = 0;
// Copy prediction to temp buffer as we now overwrite info.prediction with fully unpacked
// state by applying incoming delta to prediction.
for (int i = 0, c = info.type.schema.GetByteSize() / 4; i < c; ++i)
tempSnapshotBuffer[i] = info.prediction[i];
DeltaReader.Read(ref input, info.type.schema, info.prediction, tempSnapshotBuffer, info.fieldsChangedPrediction, info.fieldMask, ref hash);
if (enableHashing)
{
uint hashCheck = input.ReadRawBits(32);
if (hash != hashCheck)
{
GameDebug.Log("Hash check fail for entity " + id);
if (enableNetworkPrediction)
GameDebug.Assert(false, "Snapshot (" + snapshotInfo.serverTime + ") " + (haveBaseline ? "Snap [BL]" : "Snap [ ]") + " " + baseSequence + " - " + baseSequence1 + " - " + baseSequence2 + ". Sche: " + schemaCount + " Spwns: " + spawnCount + " Desp: " + despawnCount + " Upd: " + updateCount);
else
GameDebug.Assert(false, "Snapshot (" + snapshotInfo.serverTime + ") " + (haveBaseline ? "Snap [BL]" : "Snap [ ]") + " " + baseSequence + ". Sche: " + schemaCount + " Spwns: " + spawnCount + " Desp: " + despawnCount + " Upd: " + updateCount);
}
}
}
if (enableNetworkPrediction)
counters.AddSectionStats("snapShotUpdatesPredict", input.GetBitPosition2(), haveBaseline ? new Color(0.09f, 0.38f, 0.93f) : Color.cyan);
else
counters.AddSectionStats("snapShotUpdatesNoPredict", input.GetBitPosition2(), haveBaseline ? new Color(0.09f, 0.38f, 0.93f) : Color.cyan);
uint snapshotHash = 0; // sum of hash for all (updated or not) entity snapshots
uint numEnts = 0;
for (int id = 0; id < entities.Count; id++)
{
var info = entities[id];
if (info.type == null)
continue;
// Skip despawned that have not also been spawned in this snapshot
if (info.despawnSequence > 0 && !spawns.Contains(id))
continue;
// If just spawned or if new snapshot is different from the last we deserialized,
// we need to deserialize. Otherwise just ignore; no reason to deserialize the same
// values again
int schemaSize = info.type.schema.GetByteSize();
if (info.baselines.GetSize() == 0 || NetworkUtils.MemCmp(info.prediction, 0, info.lastUpdate, 0, schemaSize) != 0)
{
var data = info.baselines.Insert(sequence);
for (int i = 0; i < schemaSize / 4; ++i)
data[i] = info.prediction[i];
if (sequence > info.lastUpdateSequence)
{
if (!updates.Contains(id))
updates.Add(id);
for (int i = 0; i < schemaSize / 4; ++i)
info.lastUpdate[i] = info.prediction[i];
info.lastUpdateSequence = sequence;
}
}
if (enableHashing && info.despawnSequence == 0)
{
snapshotHash += NetworkUtils.SimpleHash(info.prediction, schemaSize);
numEnts++;
}
}
if (clientDebug.IntValue > 1)
{
if (clientDebug.IntValue > 2 || spawnCount > 0 || despawnCount > 0 || schemaCount > 0 || !haveBaseline)
{
string entityIds = "";
for (var i = 0; i < entities.Count; i++)
{
var e = entities[i];
entityIds += e.type == null ? ",-" : (e.despawnSequence > 0 ? "," + i + "(" + e.despawnSequence + ")" : "," + i);
}
string despawnIds = string.Join(",", despawns);
string spawnIds = string.Join(",", m_TempSpawnList);
string updateIds = string.Join(",", updates);
if (enableNetworkPrediction)
GameDebug.Log(("SEQ:" + snapshotInfo.serverTime + ":" + sequence) + (haveBaseline ? "Snap [BL]" : "Snap [ ]") + " " + baseSequence + " - " + baseSequence1 + " - " + baseSequence2 + ". Sche: " + schemaCount + " Spwns: " + spawnCount + "(" + spawnIds + ") Desp: " + despawnCount + "(" + despawnIds + ") Upd: " + updateCount + "(" + updateIds + ") Ents:" + entities.Count + " EntityIds:" + entityIds);
else
GameDebug.Log(("SEQ:" + snapshotInfo.serverTime + ":" + sequence) + (haveBaseline ? "Snap [BL]" : "Snap [ ]") + " " + baseSequence + ". Sche: " + schemaCount + " Spwns: " + spawnCount + "(" + spawnIds + ") Desp: " + despawnCount + "(" + despawnIds + ") Upd: " + updateCount + "(" + updateIds + ") Ents:" + entities.Count + " EntityIds:" + entityIds);
}
}
if (enableHashing)
{
uint numEntsCheck = input.ReadRawBits(32);
if (numEntsCheck != numEnts)
{
GameDebug.Log("SYNC PROBLEM: server num ents: " + numEntsCheck + " us:" + numEnts);
GameDebug.Assert(false);
}
}
counters.AddSectionStats("snapShotChecksum", input.GetBitPosition2(), new Color(0.2f, 0.2f, 0.2f));
// Snapshot reading done. Now pass on resulting pawns/despawns to the snapshotconsumer
Profiler.BeginSample("ProcessSnapshot");
consumer.ProcessEntityDespawns(serverTime, despawns);
despawns.Clear();
foreach (var id in spawns)
{
GameDebug.Assert(entities[id].type != null, "Processing spawn of id {0} but type is null", id);
consumer.ProcessEntitySpawn(serverTime, id, entities[id].type.typeId);
}
spawns.Clear();
foreach (var id in updates)
{
var info = entities[id];
GameDebug.Assert(info.type != null, "Processing update of id {0} but type is null", id);
fixed(uint* data = info.lastUpdate)
{
var reader = new NetworkReader(data, info.type.schema);
consumer.ProcessEntityUpdate(serverTime, id, ref reader);
}
}
updates.Clear();
Profiler.EndSample();
}
void WriteClientConfig<TOutputStream>(ref TOutputStream output) where TOutputStream : NetworkCompression.IOutputStream
{
AddMessageContentFlag(NetworkMessage.ClientConfig);
output.WriteRawBits((uint)clientConfig.serverUpdateRate, 32);
output.WriteRawBits((uint)clientConfig.serverUpdateInterval, 16);
sendClientConfig = false;
if (clientDebug.IntValue > 0)
{
GameDebug.Log(string.Format("WriteClientConfig: serverUpdateRate {0} serverUpdateInterval {1}", clientConfig.serverUpdateRate, clientConfig.serverUpdateInterval));
}
}
unsafe void WriteCommands<TOutputStream>(ClientPackageInfo packageInfo, ref TOutputStream output) where TOutputStream : NetworkCompression.IOutputStream
{
AddMessageContentFlag(NetworkMessage.Commands);
counters.commandsOut++;
var includeSchema = commandSequenceAck == 0;
output.WriteRawBits(includeSchema ? 1U : 0, 1);
if (includeSchema)
NetworkSchema.WriteSchema(commandSchema, ref output);
var sequence = commandSequence;
output.WriteRawBits(Sequence.ToUInt16(commandSequence), 16);
packageInfo.commandSequence = commandSequence;
packageInfo.commandTime = commandsOut[commandSequence].time;
CommandInfo previous = defaultCommandInfo;
CommandInfo command;
while (commandsOut.TryGetValue(sequence, out command))
{
// 1 bit to tell there is a command
output.WriteRawBits(1, 1);
output.WritePackedIntDelta(command.time, previous.time, NetworkConfig.commandTimeContext);
uint hash = 0;
fixed(uint* data = command.data, baseline = previous.data)
{
DeltaWriter.Write(ref output, commandSchema, data, baseline, zeroFieldsChanged, 0, ref hash);
}
previous = command;
--sequence;
}
output.WriteRawBits(0, 1);
}
byte[] zeroFieldsChanged = new byte[(NetworkConfig.maxFieldsPerSchema + 7) / 8];
protected override void NotifyDelivered(int sequence, ClientPackageInfo info, bool madeIt)
{
base.NotifyDelivered(sequence, info, madeIt);
if (madeIt)
{
if (info.commandSequence > commandSequenceAck)
{
commandSequenceAck = info.commandSequence;
lastAcknowlegdedCommandTime = info.commandTime;
}
}
else
{
// Resend user config if the package was lost
if ((info.content & NetworkMessage.ClientConfig) != 0)
sendClientConfig = true;
}
}
class MapInfo
{
public bool processed; // Map reset was processed by game
public ushort mapSequence; // map identifier to discard duplicate messages
public int ackSequence; // package sequence the map was acked in (discard packages before this)
public NetworkSchema schema; // Schema for the map info
public uint[] data = new uint[256]; // Game specific map info payload
}
class SnapshotInfo
{
public int serverTime;
}
class CommandInfo
{
public int time = 0;
public uint[] data = new uint[512];
}
class EntityTypeInfo
{
public ushort typeId;
public NetworkSchema schema;
public uint[] baseline;
}
class EntityInfo
{
//public int id;
public EntityTypeInfo type;
public int lastUpdateSequence;
public uint[] lastUpdate = new uint[NetworkConfig.maxEntitySnapshotDataSize];
public int despawnSequence;
public byte fieldMask;
public uint[] prediction = new uint[NetworkConfig.maxEntitySnapshotDataSize];
public byte[] fieldsChangedPrediction = new byte[(NetworkConfig.maxFieldsPerSchema + 7) / 8];
public void Reset()
{
type = null;
lastUpdateSequence = 0;
despawnSequence = 0;
fieldMask = 0;
// TODO(petera) needed?
for (var i = 0; i < lastUpdate.Length; i++)
lastUpdate[i] = 0;
baselines.Clear();
}
public SparseSequenceBuffer baselines = new SparseSequenceBuffer(
NetworkConfig.snapshotDeltaCacheSize, NetworkConfig.maxEntitySnapshotDataSize);
}
bool sendClientConfig = true;
public ConnectionState connectionState = ConnectionState.Connecting;
public int clientId = -1;
public int serverTickRate;
public int serverTime;
public int lastAcknowlegdedCommandTime;
public NetworkCompressionModel compressionModel = null;
MapInfo mapInfo = new MapInfo();
int commandSequence;
int lastSentCommandSeq = 0;
int commandSequenceAck;
NetworkSchema commandSchema;
SequenceBuffer<CommandInfo> commandsOut = new SequenceBuffer<CommandInfo>(3, () => new CommandInfo());
SequenceBuffer<SnapshotInfo> snapshots = new SequenceBuffer<SnapshotInfo>(NetworkConfig.snapshotDeltaCacheSize, () => new SnapshotInfo());
Dictionary<ushort, EntityTypeInfo> entityTypes = new Dictionary<ushort, EntityTypeInfo>();
//Dictionary<int, EntityInfo> entities = new Dictionary<int, EntityInfo>();
List<EntityInfo> entities = new List<EntityInfo>();
List<int> despawns = new List<int>();
List<int> spawns = new List<int>();
List<int> updates = new List<int>();
List<int> m_TempSpawnList = new List<int>();
CommandInfo defaultCommandInfo = new CommandInfo();
}
Dictionary<ushort, NetworkEventType> m_EventTypesOut = new Dictionary<ushort, NetworkEventType>();
INetworkTransport m_Transport;
ClientConnection m_Connection;
}