View

View

Views are the local, non-synchronized part of a Croquet Application. Each device and browser window creates its own independent local view. The view subscribes to events published by the synchronized model, so it stays up to date in real time.

What the view is showing, however, is completely up to the application developer. The view can adapt to the device it's running on and show very different things.

Croquet makes no assumptions about the UI framework you use - be it plain HTML or Three.js or React or whatever. Croquet only provides the publish/subscribe mechanism to hook into the synchronized model simulation.

It's possible for a single view instance to handle all the events, you don't event have to subclass Croquet.View for that. That being said, a common pattern is to make a hierarchy of Croquet.View subclasses to mimic your hierarchy of Model subclasses.

Members

# id :String

Each view has an id which can be used to scope events between views. It is unique within the session for each user.

Note: The id is not currently guaranteed to be unique for different users. Views on multiple devices may or may not be given the same id.

This property is read-only. It is assigned in the view's constructor. There will be an error if you try to assign to it.

Type:
  • String
Example:
this.publish(this.id, "changed");

# sessionId :String

Identifies the shared session.

The session id is used as "global" scope for events like the model-only "view-join" and "view-exit" events.

See Session.join for how the session id is generated.

If your app has several sessions at the same time, each session id will be different.

Type:
  • String

# session :Object|undefined

The session object

Same as returned by Session.join.

WILL BE UNDEFINED WHEN DISCONNECTED! In callbacks that can still be executed after a disconnect, you should check if (!this.session) return to avoid errors.

Type:
  • Object | undefined

# viewId :String

Identifies the View of the current user.

All users in a session share the same Model (meaning all model objects) but each user has a different View (meaning all the non-model state). The viewId identifies each user's view, or more specifically, their connection to the server. It is sent as argument in the model-only "view-join" and "view-exit" events.

The viewId is also used as a scope for local events, for example "synced".

Note: this.viewId is different from this.id which identifies each individual view object (if you create multiple views in your code). this.viewId identifies the local user, so it will be the same in each individual view object. See "view-join" event.

Type:
  • String
Example:
this.subscribe(this.viewId, "synced", this.handleSynced);

Methods

# detach()

Unsubscribes all subscriptions this view has, and removes it from the list of views

This needs to be called when a view is no longer needed, to prevent memory leaks. A session's root view is automatically sent detach when the session becomes inactive (for example, going dormant because its browser tab is hidden). A root view should therefore override detach (remembering to call super.detach()) to detach any subsidiary views that it has created.

Example
removeChild(child) {
   const index = this.children.indexOf(child);
   this.children.splice(index, 1);
   child.detach();
}

# publish(scope, event, dataopt)

Publish an event to a scope.

Events are the main form of communication between models and views in Croquet. Both models and views can publish events, and subscribe to each other's events. Model-to-model and view-to-view subscriptions are possible, too.

See Model.subscribe for a discussion of scopes and event names.

Optionally, you can pass some data along with the event. For events published by a view and received by a model, the data needs to be serializable, because it will be sent via the reflector to all users. For view-to-view events it can be any value or object.

Note that there is no way of testing whether subscriptions exist or not (because models can exist independent of views). Publishing an event that has no subscriptions is about as cheap as that test would be, so feel free to always publish, there is very little overhead.

Parameters:
Name Type Attributes Description
scope String

see subscribe()

event String

see subscribe()

data * <optional>

can be any value or object (for view-to-model, must be serializable)

Example
this.publish("input", "keypressed", {key: 'A'});
this.publish(this.model.id, "move-to", this.pos);

# subscribe(scope, eventSpec, handler) → {this}

Register an event handler for an event published to a scope.

Both scope and event can be arbitrary strings. Typically, the scope would select the object (or groups of objects) to respond to the event, and the event name would select which operation to perform.

A commonly used scope is this.id (in a model) and model.id (in a view) to establish a communication channel between a model and its corresponding view.

Unlike in a model's subscribe method, you can specify when the event should be handled:

  • Queued: The handler will be called on the next run of the main loop, the same number of times this event was published. This is useful if you need each piece of data that was passed in each publish call.

    An example would be log entries generated in the model that the view is supposed to print. Even if more than one log event is published in one render frame, the view needs to receive each one.

    { event: "name", handling: "queued" } is the default. Simply specify "name" instead.

  • Once Per Frame: The handler will be called only once during the next run of the main loop. If publish was called multiple times, the handler will only be invoked once, passing the data of only the last publish call.

    For example, a view typically would only be interested in the current position of a model to render it. Since rendering only happens once per frame, it should subscribe using the oncePerFrame option. The event typically would be published only once per frame anyways, however, while the model is catching up when joining a session, this would be fired rapidly.

    { event: "name", handling: "oncePerFrame" } is the most efficient option, you should use it whenever possible.

  • Immediate: The handler will be invoked synchronously during the publish call. This will tie the view code very closely to the model simulation, which in general is undesirable. However, if the event handler needs to set up another subscription, immediate execution ensures that a subsequent publish will be properly handled (especially when rapidly replaying events for a new user). Similarly, if the view needs to know the exact state of the model at the time the event was published, before execution in the model proceeds, then this is the facility to allow this without having to copy model state.

    Pass {event: "name", handling: "immediate"} to enforce this behavior.

The handler can be any callback function. Unlike a model's handler which must be a method of that model, a view's handler can be any function, including fat-arrow functions declared in-line. Passing a method like in the model is allowed too, it will be bound to this in the subscribe call.

Parameters:
Name Type Description
scope String

the event scope (to distinguish between events of the same name used by different objects)

eventSpec String | Object

the event name (user-defined or system-defined), or an event handling spec object

Properties
Name Type Description
event String

the event name (user-defined or system-defined)

handling String

"queued" (default), "oncePerFrame", or "immediate"

handler function

the event handler (can be any function)

Tutorials:
Returns:
Type
this
Example
this.subscribe("something", "changed", this.update); // "queued" handling implied
this.subscribe(this.id, {event: "moved", handling: "oncePerFrame"}, pos => this.sceneObject.setPosition(pos.x, pos.y, pos.z));

# unsubscribe(scope, event, handlernullable)

Unsubscribes this view's handler(s) for the given event in the given scope.

To unsubscribe only a specific handler, pass it as the third argument.

Parameters:
Name Type Attributes Description
scope String

see subscribe

event String

see subscribe

handler function <nullable>

(optional) the handler to unsubscribe (added in 1.1)

Example
this.unsubscribe("something", "changed");
this.unsubscribe("something", "changed", this.handleMove);

# unsubscribeAll()

Unsubscribes all of this view's handlers for any event in any scope.

# future(tOffset) → {this}

Schedule a message for future execution

This method is here for symmetry with Model.future.

It simply schedules the execution using globalThis.setTimeout. The only advantage to using this over setTimeout() is consistent style.

Parameters:
Name Type Default Description
tOffset Number 0

time offset in milliseconds

Returns:
Type
this

# random() → {Number}

Answers Math.random()

This method is here purely for symmetry with Model.random.

Returns:
Type
Number

# now() → {Number}

The model's current time

This is the time of how far the model has been simulated. Normally this corresponds roughly to real-world time, since the reflector is generating time stamps based on real-world time.

If there is backlog however (e.g while a newly joined user is catching up), this time will advance much faster than real time.

The unit is milliseconds (1/1000 second) but the value can be fractional, it is a floating-point value.

Returns:

the model's time in milliseconds since the first user created the session.

Type
Number

# externalNow() → {number}

The latest timestamp received from reflector

Timestamps are received asynchronously from the reflector at the specified tick rate. Model time however only advances synchronously on every iteration of the main loop. Usually now == externalNow, but if the model has not caught up yet, then now < externalNow.

We call the difference "backlog". If the backlog is too large, Croquet will put an overlay on the scene, and remove it once the model simulation has caught up. The "synced" event is sent when that happens.

The externalNow value is rarely used by apps but may be useful if you need to synchronize views to real-time.

Returns:

the latest timestamp in milliseconds received from the reflector

Type
number
Example
const backlog = this.externalNow() - this.now();

# extrapolatedNow() → {number}

The model time extrapolated beyond latest timestamp received from reflector

Timestamps are received asynchronously from the reflector at the specified tick rate. In-between ticks or messages, neither now() nor externalNow() advances. extrapolatedNow is externalNow plus the local time elapsed since that timestamp was received, so it always advances.

extrapolatedNow() will always be >= now() and externalNow(). However, it is only guaranteed to be monotonous in-between time stamps received from the reflector (there is no "smoothing" to reconcile local time with reflector time).

Returns:

milliseconds based on local Date.now() but same epoch as model time

Type
number

# update(time)

Called on the root view from main loop once per frame. Default implementation does nothing.

Override to add your own view-side input polling, rendering, etc.

If you want this to be called for other views than the root view, you will have to call those methods from the root view's update().

The time received is related to the local real-world time. If you need to access the model's time, use this.now().

Parameters:
Name Type Description
time Number

this frame's time stamp in milliseconds, as received by requestAnimationFrame (or passed into step(time) if stepping manually)

# wellKnownModel(name) → {Model}

Access a model that was registered previously using beWellKnownAs().

Note: The instance of your root Model class is automatically made well-known as "modelRoot" and passed to the constructor of your root View during Session.join.

Parameters:
Name Type Description
name String

the name given in beWellKnownAs()

Returns:

the model if found, or undefined

Type
Model
Example
const topModel = this.wellKnownModel("modelRoot");