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 GameObject references when objects can be created/destroyed under prediction.

  • Use the Predicted Hierarchy to resolve components from IDs:

    • TryCreate(...) / Create(...) → returns PredictedObjectID

    • GetComponent<T>(PredictedObjectID?) → resolve an identity or component at runtime

    • TryGetId(GameObject, out PredictedObjectID) → convert a known object into a stable ID

  • For long‑lived relationships, store PredictedObjectID in your STATE so 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 contexts

Ownership and Control

  • If you only want the controller/owner to make a change, gate logic with isController or IsOwner().

  • For player‑centric lookups and events, use predictionManager.players to iterate or react to players joining/leaving.


Create/Destroy + IDs

  • When spawning transient objects (e.g., projectiles), capture their PredictedObjectID in the originating identity’s STATE if 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