Spatial

Interactions

"Player used entity X": a validated verb pipeline; the game's policy SPI decides meaning.

Server
services.AddCrossplayInteractions();
Unity package
com.crossplay.interactions
Depends on
world

Seams you implement 1

Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayInteractions(); and it wins.

IInteractionPolicy game SPI

The game's interaction rules — THE plug-in point of this piece. The framework has already validated existence, zone, and range; the policy decides what actually happens: can this door open, what does looting this chest yield, is this button on cooldown. State changes (despawning a pickup via IServerEntities, flipping game state) belong in the implementation. The default accepts everything — the piece works out of the box as a pure "X used Y" event relay. How to implement. Register your policy in DI BEFORE AddCrossplayInteractions (it is a TryAdd seam). Branch on actionId (your verb: open/loot/press/talk) and inspect the target to decide the outcome; do any state change yourself (despawn a pickup, flip a switch, grant loot) and return Accept (optionally with a private result payload for the actor) or Reject with a reason code. The framework has ALREADY checked existence, same-zone, range and payload size, so a policy only implements game rules.

public sealed class DoorAndChestPolicy : IInteractionPolicy
{
    private readonly ILootService _loot;
    public DoorAndChestPolicy(ILootService loot) => _loot = loot;

    public InteractionDecision Decide(
        ISession actor, WorldEntity self, WorldEntity target, ushort actionId, byte[]? data)
    {
        if (actionId == Verbs.Loot && _loot.TryClaim(target.Id, out var item, out _))
            return InteractionDecision.Accept(item);   // private payload back to the actor
        if (actionId == Verbs.Open)
            return InteractionDecision.Accept();        // the EntityInteraction broadcast animates it
        return InteractionDecision.Reject(InteractionNotificationCodes.Rejected);
    }
}
  • InteractionDecision Decide(ISession actor, WorldEntity actorEntity, WorldEntity target, ushort actionId, byte[] data)

    Decides an interaction attempt (called on the poll thread, after the framework's gates).

Services you call 1

Crossplay implements these. Resolve them from DI and call them from your own systems.

IInteractionService

The interaction pipeline: validates who/what/where (in-world, same zone, existence, range, payload size — all server-authoritative), asks the game's IInteractionPolicy what happens, replies privately to the actor, and broadcasts the visible act to the target's observers.

  • void Interact(ISession session, InteractRequest request)

    Processes an interaction attempt: validates it, asks the game's IInteractionPolicy, replies to the actor, and broadcasts the visible act. The piece calls this from its message handler; a game rarely calls it directly.

Configuration 1

Every tunable lives in an options object — there are no magic numbers to hunt for.

InteractionOptions

Configurable interaction parameters (defaults here; never inlined in logic).

  • int MaxDataBytes { get; set; }

    Maximum size of the opaque request payload, in bytes.

  • float MaxRange { get; set; }

    Server-authoritative maximum distance (world units, 2D like the interest system) between the actor and the target. 0 disables the check (e.g. a game with its own reach rules in its policy).

Wire messages 3

The protocol this piece speaks. Ids are allocated per piece so they can never collide.

1701 InteractRequest

Client → server: use/act on a world entity — a door, a chest, a pickup, an NPC, another player; the framework doesn't know or care which. ActionId is the game-defined verb (open, loot, press, talk…), Data optional opaque parameters. The server validates range/existence, asks the game's interaction policy what happens, replies privately with InteractResponse, and broadcasts the visible act as EntityInteraction.

1702 InteractResponse

Server → the actor: the private result of an interaction. Data is whatever the game's policy returned (a loot payload, a dialog id, nothing) — opaque to the framework.

1703 EntityInteraction

Server → observers of the target (including the actor): an entity was interacted with — the visible act everyone should render. One game plays a chest-opening animation, another flips a door mesh, another prints "Alice pressed the button": the piece cannot tell the difference. Sent reliably — an interaction is a discrete event.

Unity services you inject 1

Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.

IInteractionClient

The Interactions puzzle piece, client side. Use/act on world entities with game-defined action ids; receive your private results (ResultReceived — loot, dialog ids, refusals) and the visible acts of everyone around you (InteractionSeen — a door opening, a chest being looted). The piece carries no presentation: a UI highlight, a mesh swap, or a full animation — your game decides. Remove this package and interactions simply stop; nothing breaks.

  • event Action<InteractResponse> ResultReceived

    Your private result for an interaction you attempted.

  • event Action<EntityInteraction> InteractionSeen

    An entity in view was interacted with (the visible act — possibly your own).

  • void Interact(long targetEntityId, ushort actionId, byte[] data = null)

    Uses/acts on a world entity with a game-defined action id and optional tiny payload.

UI views you can replace 1

Each ships a working uGUI panel you can drag into a scene. Substitute your own view to keep the logic and change the look — the presenter never knows.

ICrossplayInteractionsView

Narrow contract the presenter drives. UI-only: render calls in, user intents out.