View

View

Views are the non-replicated part of a Croquet Application. Each device and browser window creates its own independent local view. The view subscribes to events published by the replicated 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 replicated 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.

Constructor

new View(model)

A View instance is created in Session.join, and the root model is passed into its constructor.

This inherited constructor does not use the model in any way. Your constructor should recreate the view state to exactly match what is in the model. It should also subscribe to any changes published by the model. Typically, a view would also subscribe to the browser's or framework's input events, and in response publish events for the model to consume.

The constructor will, however, register the view and assign it an id.

Note: When your view instance is no longer needed, you must detach it. Otherwise it will be kept in memory forever.

Parameters:
Name Type Description
model Model

the view's model

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

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 non-replicated 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}

Tutorials:

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)

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)

Unsubscribes this view's handlers for the given event in the given scope.

Parameters:
Name Type Description
scope String

see subscribe

event String

see subscribe

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 window.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}

See:

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");