Part 2: The PurrNet Workflow – From Input to Simulation

In Part 1, we covered why we use Client-Side Prediction. Now, let’s look at how PurrNet’s “PurrDiction” system makes it happen. This page will give you the high-level map of the key components and the lifecycle of a single predicted tick, providing the context for the Simulate() methods we’ll discuss later.


The Core Components: A 2-Part System

At its heart, the PurrDiction system has two main parts that work together:

  1. PredictionManager: Think of this as the “World” or the “Director”. There is one PredictionManager per scene. Its job is to orchestrate the entire simulation. It manages a list of all predicted objects and is responsible for advancing the simulation tick by tick, handling rollbacks, and triggering reconciliations. You will rarely interact with it directly, but it’s the engine running everything under the hood. [Source: PurrNet Docs – Overview]
  2. PredictedIdentity: These are the “Actors” in our world. Any object that needs to be predicted (players, AI, projectiles) must have a script that inherits from PredictedIdentity. This is the component where we will write all of our gameplay logic. [Source: PurrNet Docs – Predicted Identities]

Choosing Your “Identity”: The 3 Flavors of Prediction

PurrNet gives us three base classes to inherit from, depending on what our object needs to do. Choosing the right one is the first step in creating a networked object.

1. PredictedIdentity<INPUT, STATE> (For Player-Controlled Objects)

  • What it is: An identity that takes player INPUT to modify its STATE.
  • Use Case: Perfect for a player character that directly responds to key presses.
  • Example: While our project uses a modular approach, a simple Player Controller would look like this
public struct PlayerInput { public Vector2 Move; public bool Jump; }
public struct PlayerState { public Vector3 Position; public Vector3 Velocity; }

public class SimplePlayer : PredictedIdentity<PlayerInput, PlayerState>
{
    protected override PlayerInput GetInput() { /* Code to read keyboard/gamepad */ }
    protected override void Simulate(PlayerInput input, ref PlayerState state, float delta) { /* Move player based on input */ }
}
C#

2. PredictedIdentity<STATE> (For State-Driven Objects)

  • What it is: An identity that has a STATE but is not directly controlled by player input. Its state changes based on time, physics, or internal logic.
  • Use Case: Perfect for AI, physics-based projectiles, or moving platforms.
  • A Perfect Example from Our Project:
// AI._Module.Movement.AIMovementModule.cs
public class AIMovementModule : PredictedIdentity<AIMovementStateData>
{
    // This AI's movement isn't driven by player input,
    // but its state (position, velocity) still needs to be predicted and reconciled.
    protected override void Simulate(ref AIMovementStateData state, float delta)
    {
        // ... AI pathfinding logic goes here ...
    }
}
C#

3. StatelessPredictedIdentity (For Logic Handlers)

  • What it is: A special type of identity that has no state of its own to reconcile. It simply hooks into the Simulate() loop to run logic every tick.
  • Use Case: Ideal for manager classes or controllers that coordinate other PredictedIdentity components.
  • A Perfect Example from Our Project:
// Player.PlayerController.cs
public class PlayerController : StatelessPredictedIdentity
{
    // This controller doesn't have its own state. Instead, it reads input
    // and tells its various modules (Movement, Combat, etc.) how to behave.
    // It's the "brain" coordinating the stateful "limbs".
}
C#

The Lifecycle of a Predicted Tick

While we will dive into the specific override methods in the next section, it’s helpful to understand the high-level lifecycle that happens for every predicted object on every tick:

  1. Gather Input: For player-controlled objects, PurrNet uses a sophisticated system to gather input. It ensures that quick, single-frame actions (like a dash command) aren’t missed between simulation ticks, while continuous actions (like holding a move key) are read at the last possible moment for accuracy. This process is explained in detail in the next part.
  2. Simulate State: The Simulate() method is called. This is where all your deterministic gameplay logic lives—updating position, changing health, modifying state based on the input from step 1.
  3. Save State: The new state produced by the simulation is saved into a history buffer, timestamped with the current tick. This history is essential for the magic of reconciliation.
  4. Reconcile (If Necessary): If a state update arrives from the server that contradicts the client’s saved history, the PredictionManager triggers a rollback. It effectively says, “Wait, my history at tick 105 was wrong.” It then rewinds to the last correct state and rapidly re-runs the simulation for every tick from that point to the present, using the saved inputs to produce a new, corrected history.

For More Detail…

This guide covers the core workflow and best practices we use in our project. However, PurrNet is a deep and powerful library. To understand exactly how components like the PredictionManager or the PredictedHierarchy work under the hood, we strongly encourage you to read the official PurrNet documentation in detail.


Next Up: Part 3: PredictedIdentity in Practice – The Core Methods

Now that we know the workflow and how to handle input, we’ll dive deep into the rules we must follow inside Simulate() to ensure our predictions match the server’s calculations.o ensure our predictions match the server’s calculations.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *