Constructor
new View(model, viewOptionsnullable)
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 | Attributes | Description |
---|---|---|---|
model |
Model | the view's model |
|
viewOptions |
Object |
<nullable> |
if |
Members
activeSubscription
Scope, event, and source of the currently executing subscription handler.
Example
// this.subscribe("*", "*", this.logEvents)
logEvents(data) {
const {scope, event, source} = this.activeSubscription;
console.log(`Event in view from ${source} ${scope}:${event} with`, data);
}
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");
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
sessionId :String
Identifies the shared session.
The session id is used as "global" scope for events like the model-only
"view-join"
event and "view-exit"
event.
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"
event and "view-exit"
event.
The viewId
is also used as a scope for local events, for example the "synced"
event.
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();
}
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, Multisynq 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
(but note that extrapolatedNow() is usually more useful for that).
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
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
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.
- See:
Returns:
the model's time in milliseconds since the first user created the session.
- Type
- Number
publish(scope, event, dataopt)
Publish an event to a scope.
Events are the main form of communication between models and views in Multisynq. 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);
random() → {Number}
Answers Math.random()
This method is here purely for symmetry with Model.random.
Returns:
- Type
- Number
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
|
|||||||||
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.
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 |
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");