A framework to add server-authoritative, client-predictive physics to Unity
Leveraging Unity's Netcode for GameObjects, this framework provides efficient network synchronization of RigidBodies and other game state information across the network. It includes client-side prediction, including history replaying.
- Install prerequisites:
- In Unity, open Package Manager
- Click the
+
button in the top left, and selectInstall package from Git URL...
- Paste
https://github.com/Cysharp/MemoryPack.git?path=src/MemoryPack.Unity/Assets/Plugins/MemoryPack#1.10.0
and clickInstall
- Install NetworkStateManager:
- In Unity, open Package Manager
- Click the
+
button in the top left, and selectInstall package from Git URL...
- Paste
https://github.com/xaroth8088/NetworkStateManager.git
and clickInstall
- Add a new
GameObject
to your scene and attach theNetworkStateManager
script to it. - When your scene is fully loaded, call
StartNetworkManager()
with the runtime-determined types of your game state objects (see "State management", below), like so:
// Grab the NetworkStateManager instance
NetworkStateManager networkStateManager = FindObjectOfType<NetworkStateManager>();
// Attach event handlers for lifecycle events (all are technically optional)
networkStateManager.OnGetGameState += NetworkStateManager_OnGetGameState;
networkStateManager.OnGetInputs += NetworkStateManager_OnGetInputs;
networkStateManager.OnPrePhysicsFrameUpdate += NetworkStateManager_OnPrePhysicsFrameUpdate;
networkStateManager.OnPostPhysicsFrameUpdate += NetworkStateManager_OnPostPhysicsFrameUpdate;
networkStateManager.OnApplyState += NetworkStateManager_OnApplyState;
networkStateManager.OnApplyInputs += NetworkStateManager_OnApplyInputs;
networkStateManager.OnApplyEvents += NetworkStateManager_OnApplyEvents;
networkStateManager.OnRollbackEvents += NetworkStateManager_OnRollbackEvents;
// Tell NetworkStateManager that it's good to start
networkStateManager.StartNetworkStateManager(typeof(MyGameStateObject), typeof(MyPlayerInputObject), typeof(MyGameEventObject));
To synchronize a GameObject
that contains a RigidBody
, you must add a NetworkId
component to it. If you're only doing rigidbody synchronization and are using Unity for Netcode's synchronization for other state, this is in addition to that library's NetworkObject
script. That said, this configuration is unsupported. It is strongly recommended that you move all game state into your IGameState
object, so that NSM can properly manage rollback/replay/prediction/etc.
If you're adding RigidBody
s at runtime, you'll need to register them with NetworkStateManager.networkIdManager
via the RegisterGameObject()
function in order for the state to be synchronized.
Note that there's a fixed pool of 255 network ids available, so trying to synchronize more than 255 physics-based game objects is unsupported.
IMPORTANT
All state stored in these struct
s MUST be immutable for history playback and server reconciliation to be deterministic. Make copies of your state data if needed to ensure this is the case.
There are three types of game objects that the framework needs to know about in order to do its magic. You can define these types any way you like, provided that they:
- are
struct
s, and - they implement the appropriate interface
Two of the interfaces derive from at least INetworkSerializable
, from Unity's Netcode for GameObjects library. (Unity's documentation here).
The other one requires that you implement two serialization-related methods:
byte[] GetBinaryRepresentation()
void RestoreFromBinaryRepresentation(byte[] bytes)
I recommend using MemoryPack for this purpose, as the API is simple and the conversion to/from byte[]
is highly performant. You do not need to worry about compressing this output, as NSM will take care of that for you automatically.
In any case, these game objects will be synchronized across the network automagically, and will be handed back to your game logic via the appropriate lifecycle events.
This object should hold general data about the game, such as scores, player health values, etc. Basically, if you need all your clients to be in sync on a game value, this is the object you'll put it in.
This object should hold input information from the player. Typical examples of input data might include things like x/y axis values from a gamepad's analog sticks, booleans to indicate that a player pressed a specific button, etc.
This object should hold information about an event happening in the game. This should only be used for events that need to be synchronized in time across clients. Notably, these can be scheduled for a future game tick.
To make the magic happen, this framework requires that you implement a number of event callbacks for vital parts of the process. Each callback requires you to do a small part of your overall game logic.
IMPORTANT
This framework assumes your game logic happens exclusively in FixedUpdate
. If this is not the case (collecting user input in Update
is the obvious unfortunate example), then it's up to you to coalesce any game state changes into things that can be represented in discrete game frames that happen at FixedUpdate
time steps.
During normal frame playing, the following events will be called in this order:
void OnGetInputs(ref Dictionary<byte, IPlayerInput> playerInputs)
Fill the dictionary with (playerId, <your IPlayerInput object here>)
pairs, as appropriate. Note that playerId
can be any byte you like to identify the player.
Because the playerId
is set by you, you can even have several players hosted by the same client - allowing both network players and couch co-op to play nicely together!
void OnApplyEvents(HashSet<IGameEvent> events)
Run through the collection and apply the effects of each event.
You'll want to cast the values back to your own game state object's type before using them.
void OnApplyInputs(Dictionary<byte, IPlayerInput> playerInputs)
The keys are the playerId
s you set during OnGetInputs
, above. Take whatever input is present, and apply it to your game state / GameObject
s as needed.
Similar to game events, above, you'll want to cast the objects inside of playerInputs
appropriately.
void OnPrePhysicsFrameUpdate()
Do whatever you'd normally do with your game before the physics engine runs for the frame. This is the equivalent of FixedUpdate()
.
void OnPostPhysicsFrameUpdate()
Do whatever you want with your game after the physics engine runs for the frame. This has no direct analog to a Unity lifecycle event because Unity "ends" the frame processing after physics runs, though the closest conceptually would be an Update()
that's guaranteed to only be called once between FixedUpdate()
s.
void OnGetGameState(ref IGameState state)
Populate the state
variable with your game's current state. The framework will take care of synchronizing RigidBody
states, but any other game state that exists in your GameObject
s or other game logic should be captured in this state.
In order to do client-side prediction, we have to modify history and run simulations. During this process, the flow of events is slightly different.
First, the game state is rewound by calling these events:
void OnApplyState(IGameState state)
Read from the state
object (after casting to your custom game state object type) and set your game's state accordingly, including anything on GameObject
s that require it.
void OnRollbackEvents(HashSet<IGameEvent> events, IGameState stateAfterEvent)
Undo any event handling from a previously fired event. NOTE: the game state will be set to what it was when the event originally fired, NOT the state immediately after the event originally fired (as might be expected for a strict rewinding of time). This is specifically done so that you know what data was used to originally trigger the event, which can be helpful for figuring out how to undo any side-effects your event had.
That said, the IGameState
object associated with the next frame is passed in via stateAfterEvent
for convenience.
OnApplyEvents
OnApplyInputs
Then, every frame that needs to be projected forwards is run via these events:
OnApplyEvents
OnApplyInputs
OnPrePhysicsFrameUpdate
OnPostPhysicsFrameUpdate
OnGetGameState
It's probably obvious from looking at this code, but I'm not a Unity or C# developer by trade. No doubt there's a lot of code in this project that isn't idiomatic Unity/C#. PR's that help make this code more idiomatic are welcome and encouraged!
Beyond that, there are a TON of TODO
's scattered throughout the code. PR's to remove those alongside new issue filings would be helpful, even if you're not going to implement the functionality yourself.
The demo project contained within Demo Projects~\Hello, NetworkStateManager
could also be made substantially more interesting and educational.
My non-idiomatic Unity/C# code shows especially true for the use of reflection in handling game state, so it's probably worthwhile to explain how I landed here.
The short version is that I need a way to instantiate your custom type as part of the serialization process, in order to get your data into your custom objects.
Verifying that your objects implement the various interfaces happens via reflection at runtime, mostly because I couldn't figure out a way to do that statically at compile-time.
- Unity's RPC framework requires that the state data be represented by value-type objects that implement
INetworkSerializable
.- More deeply, this is because their serialization functionality needs a concrete instance that it can copy data into, and it doesn't want to know / care about any constructor complexity. I suspect that they weren't able to come up with a cleaner way to do their RPC wrappers that include non-basic data types as arguments, because using a
class
here wouldn't work for this use-case. - This is probably net positive overall, because it's definitely nice for the state objects to be immutable.
- More deeply, this is because their serialization functionality needs a concrete instance that it can copy data into, and it doesn't want to know / care about any constructor complexity. I suspect that they weren't able to come up with a cleaner way to do their RPC wrappers that include non-basic data types as arguments, because using a
- I strongly prefer that people who use this framework be able to just drop the script onto a
GameObject
, rather than having to manually instantiateNetworkStateManager
.- My understanding is that this constraint prohibits solutions that turn it into
NetworkStateManager<T> where T : INetworkSerializable, new()
. - I'm open to changing this if this sort of thing is more idiomatic to Unity than it looks. As it stands, it seems the more "correct" thing to do is let it be part of a
GameObject
in a prefab, hence the constraint of passing the types in at runtime.
- My understanding is that this constraint prohibits solutions that turn it into
- I don't want implementers of the lifecycle events to have to do runtime type checking and coercion on their inputs.
- That is to say, a delegate of
void OnApplyGameState(object state)
would appear to make this framework trickier to use. - Casting back to one's own game objects whenever the
Apply*()
events occur is ok-ish, I guess, but it'd sure be nice to find a way where this isn't required. - Similar to the above, if this is actually the more idiomatic way to do this in Unity/C#, then I'm open to making that change.
- That is to say, a delegate of
- Wouldn't it be nice if the events could be generic like
void OnApplyState<T>(T state) where T : INetworkStateManagerGameStateDTO, new()
?- Alas, while I can make the
delegate
into a generic, theT
needs to be declared at theclass
level to make this work, which bumps into that second constraint, above.
- Alas, while I can make the
- How about just creating
INetworkStateManagerGameStateDTO
and let everything take that as a param?- Nope - C# won't let you upcast
INetworkStateManagerGameStateDTO
to your implementing struct because its type system can't be sure that the one you're getting is the one you're trying to use it as.
- Nope - C# won't let you upcast
Any and all assistance - including just saying "turns out that's actually the best way to do what you want" - would be greatly appreciated.