Skip to main content
The live game

Not translated yet — showing English.

Visibility

At 40 players a whole-room snapshot is fine. At 200 it is not. A visibility zone decides who receives what, as a declared predicate rather than a switch you flip per object. Broadcast and per-actor packets are two declared delivery modes of one model, so moving between them is configuration rather than a rewrite. It is a channel optimisation and not a permission — for that, see Access.

One declared model (the predicate, the layers, the detail tiers) is read two ways:

BroadcastPer-actor packets
Sendsthe whole room, to everyoneeach player only the slice their rules select
Suitsa small room; this is the defaulta crowd, where packet size must stay predictable
Reads the declarationonce, for the roomper actor

What must not leak is absent from the packet rather than hidden on the client — never sent, which makes it a security property and not a bandwidth one.

When to use it

  • Your rooms outgrow whole-room broadcast: 200 players need per-client neighbourhood streams, not every delta.
  • State must not leak: fog of war and owner-only fields should never be sent, not client-hidden.
  • Several sessions share one map and must not see each other: a layer is one more predicate.
  • The packet size must be predictable in a crowd: cap the objects and declare the order, so "the nearest N" is a promise rather than an accident of density.
  • A player at a boundary must see across it: declare which neighbouring areas are visible, because by default only their own is.
  • Skip it when the room is small. The shared-packet delivery mode already covers it.

Who does what

ActorOn this page
schema-authordeclares the visibility predicate, the object cap and its order, which neighbouring areas are visible, and the delivery mode
anysubscribes and receives what the zone admits; may lower the object cap for itself within the declared bounds

At a glance

Radius and layer rules declared on Tank; Ammo scoped to its owner beside the field
[Entity("tank")]
[Visible(Radius = 60)]                           // spatial
[Visible(Rule.SameLayer)]                        // layers of one map
public class Tank
{
    [Sync] public Vector3 Position;
    [Sync(To = Scope.Owner)] public int Ammo;    // per-field scope
}
@Entity('tank')
@Visible({ radius: 60 })                       // spatial
@Visible(Rule.SameLayer)                       // layers of one map
export class Tank {
  @Sync() position!: Vector3;
  @Sync({ to: Scope.Owner }) ammo = 0;         // per-field scope
}
@entity("tank")
@visible(radius=60)                              # spatial
@visible(Rule.SAME_LAYER)                        # layers of one map
class Tank:
    position: Vector3 = sync()
    ammo: int = sync(to=Scope.OWNER)             # per-field scope
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

// multi-entry values are one quoted list (a specifier value is a single token)
UCLASS(PSEntity = "tank",
       PSVisible = "radius:60, rule:SameMapInstance")   // spatial + instances of one map
class UTank : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSSync)                  FVector3f Position;
    UPROPERTY(PSSync = (To = "Owner")) int32     Ammo;   // per-field scope
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
Unity C# is the same C# API here — the same attributes compile in Unity, on the 2021.3 baseline — no C# 12 syntax here
[Entity("tank")]
[Visible(Radius = 60)]                           // spatial
[Visible(Rule.SameLayer)]                        // layers of one map
public class Tank
{
    [Sync] public Vector3 Position;
    [Sync(To = Scope.Owner)] public int Ammo;    // per-field scope
}

The model

What a visibility rule declares.

DeclaresWhat it is
predicatethe rule itself, in the same predicate language as access predicates and transition guards. A radius, a map instance, a team and ownership are particular cases of a predicate, not separate mechanisms — there is exactly one such language in the contract
object cap and its ordera rule may cap the number of objects, and then the selection order is declared rather than inferred: "the nearest N" is a predicate plus ordering by distance plus a cap. A predicate alone cannot express it, because a predicate answers "does this row qualify", not "which of the qualifying ones is nearer"
neighbouring areaswhether objects from a neighbouring room or map instance are visible, and which ones exactly. Never implicit: with no declaration the area is the one the recipient is in
delivery modeshared packet: the same thing to everyone, cheap on CPU. per-actor packet: each receiver gets its own, by its zone; expensive on CPU and necessary at large populations

What is true of every zone.

AlwaysWhat it is
visibilityis not a permission: what the zone hides may be available by permission, and the reverse (see below)
the recipientmay lower the cap: within the declared maximum and never below the declared minimum, because a predicate is the same for everyone whose context matched, while a packet size is the recipient's problem
truncationis observable: the recipient learns the packet was cut and by which order. Silent truncation is forbidden — it is indistinguishable from there being no more objects
degradationis declared: when the budget for assembling per-actor packets runs out the platform falls back to the shared packet as declared, rather than beginning to lose recipients arbitrarily
packet shapeis a weak promise: the size and composition of a packet should not let the existence of hidden objects be inferred

Visibility is not a permission

The zone is a channel optimisation, not access control. Don't conflate them: a fog-of-war setting would silently widen rights, or rights would start depending on distance. Packet size can hint that hidden objects exist, so where their existence must not leak, use Access permissions, not the zone.

"Who sees this?" and "what do they see?" are both queryable. Per-actor packet cost is a first-class read, in code and in the panel.

Errors

  • An undeclared subscription target is a validation refusal.
  • No permission to subscribe answers forbidden or not found depending on whether the target's existence is a secret — the refusal itself must not leak what it is refusing.
  • A resumption position that does not parse is a bad request, not a silent restart from now.
  • A subscription the platform closed, and an exhausted subscription count, are both conflicts.
  • Widening a view is fn. Granting an override view and setting detail tiers are refused forbidden to a player session, and its view is unchanged: a spectator client cannot widen its own grant.
  • An instance the caller's view excludes answers not found, the same answer as one that does not exist — a forbidden would confirm that something stands behind the wall.
  • Reading per-actor packet cost is fn adm : a cloud function or the panel, never a client.

Limits

The numbers arrive with the platform limits chapter; what happens at each ceiling is fixed now.

  • The cost of a per-actor packet: on exhaustion, declared degradation to the shared packet with a notice, never arbitrary loss of recipients.
  • Objects per rule: capped with a declared order and an observable truncation flag.
  • Subscriptions per actor: a new one is refused and the existing ones continue.
  • Delta size: the delta is split rather than truncated, and the split is observable.
  • The send rate: an upper bound, not a guarantee.