/// <summary> /// Updates all entities and dispatches a snapshot if applicable. Should /// be called once per game simulation tick (e.g. during Unity's /// FixedUpdate pass). /// </summary> public void Update() { DoStart(); foreach (var client in clients.Values) { client.RemoteClock.Update(); } { // ServerWorld.ServerUpdate var tick = World_Tick.GetNext(); World_Update(tick, entity => { //entity.Initialize(); if (!entity.HasStarted) { Entity_OnStart(entity); } entity.HasStarted = true; //entity.NotifyControllerChanged(); if (entity.DeferNotifyControllerChanged) { Entity_OnControllerChanged(entity); } entity.DeferNotifyControllerChanged = false; Entity_UpdateAuth(entity); Command latest = default; if (entity.Controller != null) { latest = entity.IncomingCommands.GetLatestAt(entity.Controller.EstimatedRemoteTick); } if (latest != null) { Entity_ApplyControlGeneric(entity, latest); latest.IsNewCommand = false; var latestCommandTick = entity.Controller.EstimatedRemoteTick; // Use the remote tick rather than the last applied tick // because we might be skipping some commands to keep up var shouldAck = !entity.CommandAck.IsValid || (latestCommandTick > entity.CommandAck); if (shouldAck) { entity.CommandAck = latestCommandTick; } } Entity_PostUpdate(entity); }, entity => Entity_OnShutdown(entity)); } if (World_Tick.IsSendTick()) { { // ServerWorld.StoreStates foreach (var entity in World_Entities.Values) { { // Entity.StoreRecord(); var record = CreateRecord(World_Tick, entity.StateBase, entity.OutgoingStates.Latest); /// <summary> /// Creates a record of the current state, taking the latest record (if /// any) into account. If a latest state is given, this function will /// return null if there is no change between the current and latest. /// </summary> StateRecord CreateRecord(Tick tick, State current, StateRecord latestRecord = null) { if (latestRecord != null) { var latest = latestRecord.State; var shouldReturn = current.CompareMutableData(latest) > 0 || !current.IsControllerDataEqual(latest); if (!shouldReturn) { return(null); } } var _record = _pools.RecordPool.Allocate(); { //record.Overwrite(tick, current); Debug.Assert(tick.IsValid); _record.Tick = tick; if (_record.State == null) { //this.state = state.Clone(); var clone = _pools.CreateState(current.TypeCode); clone.OverwriteFrom(current); _record.State = clone; } else { _record.State.OverwriteFrom(current); } } return(_record); } if (record != null) { entity.OutgoingStates.Store(record); } } } } { /// <summary> /// Packs and sends a server-to-client packet to each peer. /// </summary> // ServerManager.BroadcastPackets foreach (var controller in clients.Values) { var localTick = World_Tick; var active = World_Entities.Values; var destroyed = destroyedEntities.Values; { //ServerConnection.SendPacket /// <summary> /// Allocates a packet and writes common boilerplate information to it. /// Make sure to call OnSent() afterwards. /// </summary> // Packet.PrepareSend //var packet_ = controller.ReusableOutgoing; //packet_.Reset(); var packet_ = new ServerOutgoingPacket(); packet_.Initialize(localTick, controller.RemoteClock.LatestRemote, controller.ProcessedEventHistory.Latest, FilterOutgoingEvents(controller)); controller.Scope.PopulateDeltas(controller, localTick, packet_, active, destroyed, () => _pools.DeltaPool.Allocate(), typeCode => _pools.CreateState(typeCode)); var t = _protocol.Encode(packet_); controller.Connection.SendPayload(t.Item1, t.Item2); foreach (var delta in packet_.SentDeltas) { controller.Scope.LastSent.RecordUpdate(delta.EntityId, new ViewEntry(localTick, delta.IsFrozen)); } } } } } }
public void PopulateDeltas(Controller target, Tick serverTick, ServerOutgoingPacket packet, IEnumerable <ServerEntity> activeEntities, IEnumerable <ServerEntity> destroyedEntities, Func <StateDelta> deltaFactory, Func <int, State> stateFactory) { { /// <summary> /// Divides the active entities into those that are in scope and those /// out of scope. If an entity is out of scope and hasn't been acked as /// such by the client, we will add it to the outgoing frozen delta list. /// Otherwise, if an entity is in scope we will add it to the sorted /// active delta list. /// </summary> // void ProduceScoped(IController target, Tick serverTick, IEnumerable<Entity> activeEntities) entryList.Clear(); foreach (var entity in activeEntities) { // Controlled entities are always in scope with highest priority if (entity.Controller == target) { entryList.Add(new KeyValuePair <float, ServerEntity>(float.MinValue, entity)); } else { var b = GetPriority(entity, serverTick, out var priority); if (b) { entryList.Add(new KeyValuePair <float, ServerEntity>(priority, entity)); } else { // We only want to send a freeze state if we aren't already frozen var latest = AckedByClient.GetLatest(entity.Id); if (!latest.IsFrozen) { // StateDelta.CreateFrozen var delta = deltaFactory(); delta.Initialize(serverTick, entity.Id, null, true); frozenList.Add(delta); } } } } entryList.Sort(_comparer); foreach (var entry in entryList) { var latest = AckedByClient.GetLatest(entry.Value.Id); var delta = ProduceDelta(deltaFactory, stateFactory, entry.Value, latest.Tick, target); if (delta != null) { activeList.Add(delta); } } } { /// <summary> /// Produces deltas for all non-acked destroyed entities. /// </summary> // private void ProduceDestroyed(IController target, IEnumerable<Entity> destroyedEntities) foreach (var entity in destroyedEntities) { var latest = AckedByClient.GetLatest(entity.Id); if (latest.Tick.IsValid && (latest.Tick < entity.RemovedTick)) { // Note: Because the removed tick is valid, this should force-create var delta = ProduceDelta(deltaFactory, stateFactory, entity, latest.Tick, target); destroyedList.Add(delta); } } } { // packet.Populate(activeList, frozenList, destroyedList); packet.PendingDeltas.AddRange(destroyedList); packet.PendingDeltas.AddRange(frozenList); packet.PendingDeltas.AddRange(activeList); } destroyedList.Clear(); frozenList.Clear(); activeList.Clear(); }
public (byte[], int) Encode(ServerOutgoingPacket packet) { var writeSize = MessagePackSerializer.Serialize(ref bytes, 0, packet, _resolver); return(bytes, writeSize); }