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 one-shot presentation across identities. By default, the owner presents predicted occurrences and other clients wait for verification. The server invokes normally.
PurrDiction: 1.4.0 and newer
PredictedEventcomes in every arity up to sixteen arguments:PredictedEvent<T1, T2>,PredictedEvent<T1, T2, T3>, and so on.AddListener,RemoveListener, andInvoketake the matchingAction<>and argument list, so you can pass the hit point, the damage, and the attacker id in one call instead of packing them into a struct.
// In an identity
private PredictedEvent onHit;
protected override void LateAwake()
{ onHit = new PredictedEvent(predictionManager, this); }
void SimulateHit()
{ onHit.Invoke(); } // will invoke only in valid contextsVerified-only events and view mode (dev branch)
The current PurrDiction dev branch also delivers an owner's occurrence when it first appears during verified simulation. For example, another player causing a respawn may never have been predicted locally. An occurrence already presented by the owner is suppressed when the same tick is verified.
Use the opt-in View mode for VFX or audio that should present the first local occurrence on any peer, including a new occurrence discovered during speculative replay:
private PredictedEvent onRespawnView;
protected override void LateAwake()
{
onRespawnView = new PredictedEvent(predictionManager, this, PredictedEventMode.View);
onRespawnView.AddListener(RespawnVisuals);
}
private void RespawnVisuals() { /* reset visual effects */ }
// Call from the simulation path that actually performs the respawn.
private void SimulateRespawn() => onRespawnView.Invoke();The mode overload is available on every event arity. The existing two-argument constructor selects PredictedEventMode.Auto.
Occurrences are identified by event instance, simulation tick, and invocation order within that tick. Multiple calls in a tick remain distinct; keep their ordering deterministic and use separate event instances for independent event streams. The first presentation's arguments are retained visually: a later correction does not invoke that occurrence again or undo a mispredicted effect. Delivery history is bounded to the world's replay window, and catch-up suppresses presentation without recording skipped occurrences as delivered.
Keep event listeners limited to presentation. Gameplay state must change on every relevant simulation pass, including replay, outside the filtered listener. PredictedEvent filters local invocations; it does not send an RPC or an event transcript, so the simulation must still invoke the event on the receiving peer. Construct events in LateAwake for their simulation lifetime. If you deliberately retain an event across a new lifetime with the same ID, call ResetDeliveryHistory(); changing the identity's ID clears its delivery history automatically.
Ownership 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
}
}