Generic network prediction

Originally posted to Shawn Hargreaves Blog on MSDN, Thursday, June 25, 2009

Government Health Warning: this post contains more C# than English!

My AppWeek game used code from the Network Prediction sample to smooth the movement of remotely controlled avatars, tanks, and hoverships. To support three types of entity with varying physics, I had to make my prediction implementation more generic than the sample code I started out with.

In the original sample, the Tank class implements prediction by storing its physics state in a nested TankState struct. This allows it to maintain three copies of the TankState, representing the current simulation state, the previous state from immediately before the last network packet was received, and the display state, which is gradually interpolated from the previous state toward the simulation state.

I wanted to move the prediction logic into a base class that could be shared by my Dude, Tank, and Ship classes. Because the physics state was different for each entity type, I had to make this base class a generic.

First, I created an interface describing all the things I needed to be able to do with a physics state structure:

    interface IPredictedState<TState>
        where TState : struct
    {
        void Update(GameInput input, Level level);

        void WriteNetworkPacket(PacketWriter packet);
        void ReadNetworkPacket(PacketReader packet);

        void Lerp(ref TState a, ref TState b, float t);
    }

Using this interface, I can declare a generic base class for network predicted objects:

    class PredictedEntity<TState>
        where TState : struct, IPredictedState<TState>
    {
        protected TState SimulationState;
        protected TState DisplayState;
        protected TState PreviousState;

        protected GameInput PredictionInput;

        float currentSmoothing;

The rest of PredictedEntity is similar to the original Tank implementation from the sample. The logic for updating locally controlled objects is simple:

        public void UpdateLocal(GameInput input, Level level)
        {
            this.PredictionInput = input;

            // Update the master simulation state.
            SimulationState.Update(input, level);

            // Locally controlled entities have no prediction or smoothing, so we
            // just copy the simulation state directly into the display state.
            DisplayState = SimulationState;
            PreviousState = SimulationState;

            currentSmoothing = 0;
        }

The update for remotely controlled objects, which use network prediction, is a little more involved:

        public void UpdateRemote(Level level)
        {
            // Update the smoothing amount, which interpolates from the previous
            // state toward the current simultation state. The speed of this decay
            // depends on the number of frames between packets: we want to finish
            // our smoothing interpolation at the same time the next packet is due.
            float smoothingDecay = 1.0f / GameplayScreen.FramesBetweenPackets;

            currentSmoothing -= smoothingDecay;

            if (currentSmoothing < 0)
                currentSmoothing = 0;

            // Predict how the remote entity will move by updating
            // our local copy of its simultation state.
            SimulationState.Update(PredictionInput, level);

            if (currentSmoothing > 0)
            {
                // If smoothing is active, also apply prediction to the previous state.
                PreviousState.Update(PredictionInput, level);

                // Interpolate the display state gradually from the
                // previous state to the current simultation state.
                DisplayState.Lerp(ref SimulationState, ref PreviousState, currentSmoothing);
            }
            else
            {
                // Copy the simulation state directly into the display state.
                DisplayState = SimulationState;
            }
        }

Sending network packets is trivial:

        public virtual void WriteNetworkPacket(PacketWriter packet)
        {
            SimulationState.WriteNetworkPacket(packet);
        }

But reading them is more complex:

        public virtual void ReadNetworkPacket(PacketReader packet, GameInput input, TimeSpan latency, Level level)
        {
            this.PredictionInput = input;

            // Start a new smoothing interpolation from our current
            // state toward this new state we just received.
            PreviousState = DisplayState;
            currentSmoothing = 1;

            // Read simulation state from the network packet.
            SimulationState.ReadNetworkPacket(packet);

            // Apply prediction to compensate for
            // how long it took this packet to reach us.
            TimeSpan oneFrame = TimeSpan.FromSeconds(1.0 / 60.0);

            while (latency >= oneFrame)
            {
                SimulationState.Update(input, level);
                latency -= oneFrame;
            }
        }

In the original sample, Tank.ReadNetworkPacket was responsible for reading the input state and packet send time. I moved this work out to the calling method, who reads those values from the start of the packet and computes the latency before calling PredictedEntity.ReadNetworkPacket. This makes things more flexible if I want to describe the state of more than one entity (for instance an avatar riding in a tank) in a single network packet, as it avoids having to send the input state and time twice.

Armed with a generic base class, I derived classes for each type of entity:

    class Ship : PredictedEntity<Ship.State>
    {
        public struct State : IPredictedState<State>
        {
            public Vector3 Position;
            public Vector3 Velocity;
            public Vector3 Front;
            public Vector3 Up;
            public float TurnVel;


            public void Update(GameInput input, Level level)
            {
                // Ship physics goes here.
            }


            public void WriteNetworkPacket(PacketWriter packet)
            {
                packet.Write(Position);
                packet.Write(Velocity);
                packet.Write(Front);
                packet.Write(Up);
                packet.Write(TurnVel);
            }


            public void ReadNetworkPacket(PacketReader packet)
            {
                Position = packet.ReadVector3();
                Velocity = packet.ReadVector3();
                Front = packet.ReadVector3();
                Up = packet.ReadVector3();
                TurnVel = packet.ReadSingle();
            }


            public void Lerp(ref State a, ref State b, float t)
            {
                Position = Vector3.Lerp(a.Position, b.Position, t);
                Velocity = Vector3.Lerp(a.Velocity, b.Velocity, t);
                Front = Vector3.Lerp(a.Front, b.Front, t);
                Up = Vector3.Lerp(a.Up, b.Up, t);
                TurnVel = MathHelper.Lerp(a.TurnVel, b.TurnVel, t);
            }
        }

Thanks to the IPredictedState interface, this is everything necessary for PredictedEntity to provide network prediction via its ReadNetworkPacket and UpdateRemote methods.

Blog index   -   Back to my homepage