Purrdicted Character Controller
by Shelby
Introduction
While PurrDiction's main flagship feature is supporting Rigidbody controllers and interaction, we can still predict with the CharacterController as well (as well as everything else)!
Before we begin, it's highly recommended to read through the PurrDiction docs to get a better understanding of how the system works.
For further reading, you should also check out a series of articles on Client Side Prediction by Neotime, which provides a far more in-depth look at Client Side Prediction principles.
This guide will assume you have a basic understanding of how PurrDiction works, as well as a basic understanding of PurrNet.
Getting Started
To get started, let's create a new script called PredictedCharacterController and inherit from PredictedIdentity. As well as that, let's create the STATE and INPUT structs, and implement the Simulate and UpdateInput methods.
UpdateInput is called every frame on the client and is where we will gather our input to later simulate. Simulate is called every tick, and is where we will apply our input to the state of our player.
using PurrNet.Prediction;
using UnityEngine;
public struct PredictedCharacterControllerInput : IPredictedData<PredictedCharacterControllerInput>
{
public void Dispose() { }
}
public struct PredictedCharacterControllerState : IPredictedData<PredictedCharacterControllerState>
{
public void Dispose() { }
}
[RequireComponent(typeof(CharacterController))]
public class PredictedCharacterController : PredictedIdentity<PredictedCharacterControllerInput, PredictedCharacterControllerState>
{
protected override void UpdateInput(ref PredictedCharacterControllerInput input) {}
protected override void Simulate(PredictedCharacterControllerInput input, ref PredictedCharacterControllerState state, float delta) { }
}INPUT
The INPUT struct is what holds all of the input that we want to be able to check and use inside Simulate. For now, let's keep things extremely simple and check for Movement, and jumping, which we can do by adding a Vector3 for movement and a bool for jumping.
STATE
The STATE struct is what holds the current state of our player. This can be a lot of things, but since we are just working on simple movement, we can just store our current Velocity. You may wonder why we are storing Velocity instead of Position, and that is because we will be using the PredictedTransform to handle our position and rotation state.
Gathering Input
Now that we have our INPUT and STATE structs, we can begin gathering input to later simulate. To do this, we use the UpdateInput method, which is called every frame on the client. You can gather input in any way you like, but for simplicity, we will use Unity's old input system.
You may be curious to why we are using |= for input.Jump instead of just =, and that is because while UpdateInput is called every frame, Simulate is called every tick, which means that if we press the jump button between ticks, we may miss the jump input.
Simulating with Our Input
Now that we have gathered our input, we can simulate! To do this, we can override the Simulate method, which is called every tick. If you are familiar with writing a basic CharacterController, the rest of this will be very familiar to you!
This code is modified from the Unity documentation on CharacterController.Move.
Final Script
Putting it all together, our final script looks like this:
Creating the Player Prefab
Inside of Unity, Create a new Empty
GameObjectand name it Player.Add the
PredictedCharacterControllercomponent we just created, and also add thePredictedTransformcomponent mentioned earlier to handle the Position and Rotation state.Under the Player, Create a new
Capsuleand name it Graphics. This will be our graphical representation of our player. Make sure to remove theCapsuleColliderfrom the Graphics object, as having colliders on Graphical objects is not allowed.Finally, assign the Graphics GameObject to the
PredictedTransformcomponent'sGraphicsfield.
Testing
Create a new scene and add a plane for the player to walk on and create a
Camerain the scene so we can see our player.Create an empty
GameObjectand name it NetworkManager. Add theNetworkManagercomponent to it.Create an empty
GameObject, and name it Prediction Manager. Add thePredictionManagercomponent to it. Click on New under thePredictedPrefabsfield to create a newPredictedPrefabsasset.Add the
PredictedPlayerSpawnercomponent to the Prediction Manager and assign the Player prefab to thePlayer Prefabfield.Run the scene and confirm you can jump and move around!
Conclusion
Congratulations! You (hopefully) have a capsule that can run around and jump! Fire up a client and use the PurrTransport and try it out with real world latency!
As long as you always modify your state inside Simulate, and gather your input inside UpdateInput (or GetFinalInput), you'll notice the workflow becomes very similar to writing singleplayer code.
If you have any questions, feel free to ask in the Discord server!
Last updated