Interactions
"Player used entity X": a validated verb pipeline; the game's policy SPI decides meaning.
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.
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.
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.
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> ResultReceivedYour private result for an interaction you attempted.
-
event Action<EntityInteraction> InteractionSeenAn 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.