Interacting With Multiple Identities
Many features require one predicted identity to read or affect another (e.g., projectiles applying knockback, an ability system modifying player state, or a shared input bus). This guide covers safe, deterministic patterns to discover, read, and modify other identities and their input.
Discovery and References
Prefer stable handles over direct
GameObjectreferences when objects can be created/destroyed under prediction.Use the Predicted Hierarchy to resolve components from IDs:
TryCreate(...)/Create(...)→ returnsPredictedObjectIDGetComponent<T>(PredictedObjectID?)→ resolve an identity or component at runtimeTryGetId(GameObject, out PredictedObjectID)→ convert a known object into a stable ID
For long‑lived relationships, store
PredictedObjectIDin yourSTATEso it survives rollback/replay.
public struct AbilityState : IPredictedData<AbilityState>
{
public PredictedObjectID target;
public void Dispose() {}
}
// Later during simulate
var targetCtrl = predictionManager.hierarchy.GetComponent<MyController>(state.target);Reading and Writing Another Identity’s STATE
You may read or modify another identity’s state during simulation. Keep it deterministic and do it only while simulating.
Access the other identity as its concrete type and mutate via its
currentState:
// inside Simulate(...)
var target = predictionManager.hierarchy.GetComponent<MyMover>(state.target);
if (target != null)
{
ref var ts = ref target.currentState; // by ref
ts.velocity += knockback; // deterministic mutation
}Tips:
Only mutate in
Simulate/LateSimulate(the simulation phase). You can annotate helpers with[SimulationOnly]to ensure they only run while simulating.Keep inter‑identity logic order‑independent when possible. If order matters, consolidate logic into a single orchestrator identity.
Using Another Identity’s Input
You can read another identity’s input for the current tick via its
currentInput(for identities with input).Use this sparingly; prefer sharing intent/state rather than raw input when possible.
var player = predictionManager.hierarchy.GetComponent<PlayerController>(state.player);
if (player != null)
{
var input = player.currentInput; // read‑only view for this tick
// derive effects from input (e.g., combo triggers, assist behaviors)
}Note: Do not attempt to modify another identity’s input history. Instead, mutate its state or publish events that it reacts to.
Cross‑Identity Events
Use
PredictedEvent/PredictedEvent<T>for view‑safe eventing across identities. Events only invoke when it’s valid to do so (server, owner while not replaying, or verified client), preventing double‑fire during replays.
// In an identity
private PredictedEvent onHit;
protected override void LateAwake()
{ onHit = new PredictedEvent(predictionManager, this); }
void SimulateHit()
{ onHit.Invoke(); } // will invoke only in valid contextsOwnership and Control
If you only want the controller/owner to make a change, gate logic with
isControllerorIsOwner().For player‑centric lookups and events, use
predictionManager.playersto iterate or react to players joining/leaving.
Create/Destroy + IDs
When spawning transient objects (e.g., projectiles), capture their
PredictedObjectIDin the originating identity’sSTATEif you’ll need to interact with them later.Use the Hierarchy to
Delete(id)deterministically when cleaning up.
Ordering and Determinism
The manager updates identities in a consistent order (by object ID then component ID). Avoid relying on a specific order for correctness.
If strict ordering is required (e.g., system A before B), move the shared mutation into a single identity (an aggregator) and have others read the outcome.
Example: Projectile Applies Knockback
public struct ProjectileState : IPredictedData<ProjectileState>
{
public PredictedObjectID target; public Vector3 force;
public void Dispose() {}
}
public class Projectile : PredictedIdentity<ProjectileState>
{
protected override void Simulate(ref ProjectileState s, float dt)
{
if (!s.target.HasValue) return;
var mover = predictionManager.hierarchy.GetComponent<MyMover>(s.target);
if (mover == null) return;
ref var ms = ref mover.currentState;
ms.velocity += s.force; // deterministic state mutation
predictionManager.hierarchy.Delete(id.objectId); // self destroy after applying
}
}Last updated