Generic network prediction

Generic network prediction

  • Comments 11

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.

  • Nothing specific to say, but comments seem to have fallen off recently and I thought that was sad. It's nice to know you have readership, so I thought I'd say hi.

  • This is perfect.  I'm mulling over networking in my DBP entry and this is exactly the kind of stuff I was confused about.  

    As with everything you've posted for the community, thank you so much man!

  • I agree.  I never post, but read every blog.  Thanks for all the info, Shawn!

  • Ok, and now make a whole game on the xbox and see how the garbage collector kills performance when more than 2 players are in the session...

    MS really needs to do something here. My game produces ZERO garbage without networking but a s soon as i turn it on it does a lot. Even using the unsafe code sample found in the forums didn't help much. As soon as you write float or Vectors in the session, the problem occurs.

    Nevertheless a good sample  - didn't know the where-syntax ;)

  • dpramel: it is entirely possible to make a network game with zero garbage. Have you used CLR Profiler on Windows to see exactly where your garbage is coming from? If you do that and can't work out how to avoid this, I would recommend posting about this on the creators.xna.com forums where I'm sure someone will be able to assist you.

  • Hi Shawn,

    have you seen this thread?

    http://forums.xna.com/forums/p/13255/83721.aspx

    But ok, i will profile it again. Thought it would be the packet writer (and was pretty sure it was...)

    Daniel

  • That's a very old thread: the issue it is talking about no longer exists for the last two Game Studio versions.

  • You couldn't by any chance post the ' // Ship physics goes here.' bit :p

  • > You couldn't by any chance post the ' // Ship physics goes here.' bit :p

    I don't know that that would make much sense without a ton of the supporting code. I actually filed a bug for our dev-ed team to consider if we could release any of the appweek products as mini-games, but I don't know if/when that might go anywhere.

    My physics wasn't really that interesting, though. It all started with the Tank On A Heightmap sample, then I added some inertia and simple spring model for the ship to make it hover a bit above the ground plane, but so it would gradually bounce to new heights when flying over rough ground.

  • Ok. I noticed you have the Front and Up vectors in the State structure so I guessed you are using Vector math to control the Ships direction/movement. Thats the part I'm interested in because I want to use Vector math for my ship control but I can't find any simple, boiled down explanation of exactly how to manage it. e.g. Is it better to maintain a separate Forward, Up vector apart from the WorldMatrix or maybe its better to just have the Matrix and rebuild that directly. I'm unsure which method to use. I guess with the networking stuff it is better to maintain them separately as it would be a waste to send the whole matrix over the network. Maybe it is already in the Tank On a Heightmap sample. I'll take a look there. Thanks.

  • haven't read through your code, but sounds like what is called "dead reckoning" in robotics etc.

Page 1 of 1 (11 items)
Leave a Comment
  • Please add 8 and 3 and type the answer here:
  • Post