User:Yosh/sandbox: Difference between revisions

From Resonite Wiki
m maybe done
nocat
 
(6 intermediate revisions by the same user not shown)
Line 1: Line 1:
'''Dynamic variables''', commonly shorted to '''dyn vars''' or '''dynvars''', is a system of data storage wherein one can store arbitrary, scoped data under slot hierarchies with arbitrary names, akin to that of an [https://en.wikipedia.org/wiki/Associative_array associative array]. Their usage is usually found in large systems with many moving parts, but can nonetheless be useful as easy "global" values that can be changed on an object.
The '''data model''' refers to the declaration, structure, and handling of [[types]], data, and changes on data within [[FrooxEngine]]. The data model allows for change detection, value synchronization, unified structure, and serialization of any and all types that FrooxEngine encounters.


For a quick-start guide on using dynamic variables, the [[How To Use Dynamic Variables]] page serves to be a how-to.
== Elements ==


== Overview ==
Everything that gets managed by the data model in some fashion implements the [[Type:IWorldElement|IWorldElement]] interface. This interface assigns elements with a host of properties, including a [[Type:RefID|reference ID]], possible parent element, and whether the element is persistent or not.
Dynamic variables are managed with two parts: ''dynamic variable spaces'' and ''dynamic variables''. Dynamic variables live under a dynamic variable space, and can be created, modified, and destroyed dynamically.


=== Variable Spaces ===
This interface doesn't have much behavior by itself--it only specifies a thing as being able to be saved and loaded into [[worlds]] in some fashion. More complex behaviors offered by the data model are provided by types that implement this interface. The three types of elements that directly implement this interface are ''sync elements'', ''workers'', and ''worlds''.
By adding a [[Component:DynamicVariableSpace|DynamicVariableSpace]] to a slot, the slot and all of its children become a part of the named variable space. A slot can be part of multiple spaces at once. Spaces are <em>not</em> nested, meaning it is advised to make unique names for variable spaces.


=== Dynamic Variables ===
== Sync elements ==
To create a dynamic variable, attach any one of the following components to a slot:


* [[Component:DynamicValueVariable|DynamicValueVariable]], for working with [[Value Types|value types]] ([[Type:int|int]], [[Type:string|string]], [[Type:float3|float3]], etc.).
[[Type:SyncElement|Sync elements]] represent a value or object that can be written to, [[field linkage|linked]], and send/receive change events from other elements. Sync elements mainly comprise of [[Type:SyncField|fields]] and various types of collections--usually any type name that starts with <code>Sync</code> derives from a sync element.
* [[Component:DynamicReferenceVariable|DynamicReferenceVariable]], for working with [[Reference Types|reference types]] ([[Type:Slot|Slot]], [[Type:User|User]], etc.).
* [[Component:DynamicTypeVariable|DynamicTypeVariable]], for working with the [[Type:Type|Type]] type.


Dynamic variables have a name and a value. A variable's name is its unique identifier within the variable space, while its value is the value of the dynamic variable. This is touched on later in [[#Binding]].
The main thing that separates sync elements from other elements is, well, synchronization. Whenever a sync element is changed, spare few exceptions like [[Type:RawOutput`1|RawOutput&lt;T&gt;]], it and its entire parent hierarchy propagate a change event. This includes the elements getting put in the world's update manager to synchronize their values at the end of the [[update loop]].


=== Dynamic Fields ===
=== Linking ===
It is possible to "transform" an existing [[Type:IField|IField]] on a component into a dynamic variable. This is done by any one of the following components:


* [[Component:DynamicField|DynamicField]], for working with [[Value Types|value types]].
{{Main|Linkage}}
* [[Component:DynamicReference|DynamicReference]], for working with [[Reference Types|reference types]].
* [[Component:DynamicTypeField|DynamicTypeField]], for working with the [[Type:Type|Type]] type.


Upon attaching the component and dragging the field to convert into <code>TargetField</code>, the pointed field can then be interfaced with like any other dynamic variable.
Linkage is a way to give exclusive control of a sync element to one source, often referred to a driver. Two types of linkage exist: ''links'' and ''drives'', while drivers may choose an optional ''hook''.


=== Naming Restrictions ===
When a field is linked, it is shown as cyan in inspectors. Linked fields still send data model events as any normal field, but can only be changed by the driver.
The names of dynamic variables and dynamic variable spaces must not contain symbols, punctuation, or whitespace, <em>except</em> for period (<code>.</code>), underscore (<code>_</code>), and space (<code> </code>). To check if a character is unable to be used in a dynamic variable name, one can use the [[ProtoFlux:Is Symbol|Is Symbol]], [[ProtoFlux:Is Punctuation|Is Punctuation]], and [[ProtoFlux:Is White Space|Is White Space]] ProtoFlux nodes, taking care of the three exceptions above.


== Binding ==
When a field is driven, it is shown as purple in inspectors. Driven fields act the same as linked fields, but the value of a driven field will not be synchronized across the network. This allows for per-user values or local calculations of things that would otherwise be expensive to network.
The process of a dynamic variable being associated with a given space is called ''binding''. A dynamic variable component will traverse up the slot hierarchy, including its current slot, looking for an applicable variable space to bind to. If a dynamic variable does not find a dynamic variable space that it can bind to, it will not be accessible outside of the component itself, essentially reducing to a glorified [[Component:ValueField|ValueField]]. A dynamic variable will go through this binding process every time any part of the component changes.


=== Direct Vs Indirect Binding ===
== Workers ==
When making a dynamic variable, the <code>VariableName</code> of a dynamic variable component can be one of the following two forms: <code>VariableName</code> or <code>VariableSpaceName/VariableName</code>. The former represents ''indirect binding'', while the latter represents ''direct binding''.


If a dynamic variable is indirectly binding, it will bind to the first dynamic variable space that does not have <code>OnlyDirectBinding</code> set to <code>True</code>. If a dynamic variable is directly binding, it will bind to the first dynamic variable space that matches <code>VariableSpaceName</code>.
[[Type:Worker|Workers]] are, in essence, containers for sync elements and other workers to provide a specific, individual task for the engine. Examples of workers include [[slots]], [[components]], sub-classes of components, and [[streams]]. Workers are able to receive events from their children and propagate events to their parent.


For example, in the following setup:
Workers themselves are a local construct. However, they usually exist in sync element collections, and thus their existence is usually synchronized across users along with their elements.


└─ Foo - Variable Space "test"
== Events ==
    └─ Bar - Variable Space "test2"
      └─ Baz - Dynamic Variable "test/var"


The dynamic variable <code>test/var</code> will bind to the variable space <code>test</code>. If the variable was instead named <code>var</code>, it will bind to the variable space <code>test2</code>.
A big part of the data model's function is capturing changes to an element or worker and propagating these changes across the element's parent and references to the element. Everything that exists within the data model can capture and send these events, while things outside the data model will usually not.


=== Warning ===
For example, the [[ProtoFlux:Changeable Source|Changeable Source]] ProtoFlux node is able to receive change events from the element that it references. A changeable source of a [[Type:slot|slot]] plugged in to a [[ProtoFlux:Get Slot Active|Get Slot Active]] node will be able to update a connected [[listener node]] whenever the active state of the slot changes. However, an [[ProtoFlux:Input|Input]] node will not update any connected listener nodes in the same fashion. This is due to the former node receiving events from the data model, while the latter does not. Whenever the active field of a slot (a sync element) changes, it propagates that change to the slot itself (the worker containing the active field), which will then get received by the changeable source and thus the entire [[context]] of the listener node.
As of the time of writing, there exist a few instances where created variables do not instantly bind/rebind to a dynamic variable space, requiring a [[ProtoFlux:Delay Updates|Delay Updates]] of 2+ updates. These include [https://github.com/Yellow-Dog-Man/Resonite-Issues/issues/1394 creating new dynamic reference variables], [https://github.com/Yellow-Dog-Man/Resonite-Issues/issues/2717 deleting dynamic variables], changing the name of a dynamic variable or dynamic variable space, and any instance where there is a dynamic variable space change without a component update, such as [https://github.com/Yellow-Dog-Man/Resonite-Issues/issues/1690 duplicating slots containing a dynamic variable space] or reparenting a slot containing a dynamic variable under a new dynamic variable space. If you find dynamic variables to be behaving weirdly, and you are using these operations, try adding a delay of 2 or more updates between such operations.


== Interfacing ==
== World ==
A variable space can be interfaced with any slot that resides within the variable space, even if the slot is not part of the hierarchy containing the exact dynamic variable being interfaced with.


=== Reading Dynvars ===
The data model exists under a [[world]]. The world [[update loop]] controls the communication and events between all individual concepts of the data model as one unified pipeline. This includes, but is not limited to:
In ProtoFlux, the [[ProtoFlux:Read Dynamic Variable|Read Dynamic Variable]] node and [[ProtoFlux:Dynamic Variable Input|Dynamic Variable Input]] nodes exist to read dynamic variables from a slot.


The Read Dynamic Variable node takes in a <code>Source</code> [[Slot]] and a <code>Path</code> variable name, and is marked as [[ContinuouslyChanging]]. The Dynamic Variable Input node uses a [[Global]] for <code>Path</code> and binds to spaces in the slot hierarchy it exists in. Therefore, when reading constant-name dynamic variables within the same dynamic variable space, the Dynamic Variable Input node is preferred. When reading dynamic variables from outside the intended space hierarchy, the Read Dynamic Variable node must be used.
* The [[Type:UpdateManager|UpdateManager]] for managing all sync elements that are "dirty" and the changes that they incur on other elements/workers.
* The [[Type:LinkManager|LinkManager]] for managing all of linkage within a world.


It is also possible to read dynamic variables by sourcing the <code>Value</code> or <code>Reference</code> field of the component directly. This is not recommended, as if the dynamic variable ever gets deleted and remade, the source will break, removing half the functionality of a dynamic variable.
== Strict exclusivity ==


=== Driving from Dynvars ===
The data model can only function without issue when everyone in a session agrees on what the data model should be. If one client tells another client about an element type or worker type that doesn't exist for the recipient, the recipient won't know what to do with the information or how to sync the potential element values. This is the reason why [[plugins]] marked as a [[How To Create Plugins/DataModelAssemblyType|core]] assembly type cause incompatible sessions with those who do not have the plugin.
By using a [[Component:DynamicValueVariableDriver|DynamicValueVariableDriver]] or [[Component:DynamicReferenceVariableDriver|DynamicReferenceVariableDriver]] component, fields can be driven using the value of a dynamic variable. This is recommended over traditional methods of driving by virtue of its flexibility and compactness.


=== Writing to Dynvars ===
== Further reading ==
Dynamic variables should be written to via the [[ProtoFlux:Write Dynamic Variable|Write Dynamic Variable]] or [[ProtoFlux:Write Or Create Dynamic Variable|Write Or Create Dynamic Variable]] ProtoFlux nodes.


If dynamic variables are written to via sourcing the component field and writing, it will incur a delay of 1 frame before the value is propagated to any read nodes. As such, it is not recommended to use this approach.
* [[:Category:Data model]] for more in-depth aspects about various parts of the data model.
 
=== Driving Dynvars ===
Driving dynamic variables must be done with caution. For one, one must drive the value field of the dynamic variable component, which incurs a delay of the value when reading it back via ProtoFlux due to drives acting as a local write to the field every frame. Additionally, if a dynamic variable is being driven, it is <em>crucial</em> that <em>all</em> instances of the same dynamic variable are driven by the same value. Otherwise, clients will fight over which value is the "true" value of the dynamic variable and cause inconsistent behavior.
 
== Best Practices ==
Even though dynamic variables allow for a wide array of freedom, there are a few practices generally considered to be favorable when working with dynamic variables:
 
* It is highly recommended to have only one instance of a dynamic variable (dynamic variable component with the same name) at any given time.
** There aren't any huge problems with having multiple dynamic variable instances if none or all of the instances are being driven, but it allows for cleaner organization of variables.
* Using variable names that directly bind allows for a clearer overview of what space the variables should be bound to. Indirectly binding variable names are more suited for variables that are dynamically created and/or destroyed as part of an object's function.
** Using <code>OnlyDirectBinding</code> on a DynamicVariableSpace strictly enforces this behavior, which can prevent misbindings and catch errors earlier.
* Dynamic variable spaces are not nested. If a system is complex enough, or if a DynamicVariableSpace is being shared by multiple objects, using periods (<code>.</code>) to pseudo-isolate objects or systems from one another is encouraged.
** e.g. an avatar and all of its features could contain the dynamic variables <code>User/Avatar.Systems.Grabbable.Enabled</code>, <code>User/Avatar.Blendshapes.Blep.MaxClamp</code>, <code>User/Avatar.Systems.Flight.Drag</code>, etc.
 
== Default Spaces ==
As of the time of writing, there are three dynamic spaces that exist "by default":
 
* <code>World</code>, which exists under the [[Root]] of any created world. This space is marked as <code>OnlyDirectBinding</code>.
** Useful for things that should globally affect the world or broadcast information throughout the world. Example items that use this space include [[BeatLink]] and the [[Redprint]] manager.
* <code>User</code>, which exists under every [[User]]'s [[ProtoFlux:Get User Root Slot|User Root Slot]]. This space is <code>OnlyDirectBinding</code>.
** Useful for systems that affect avatars, as outside objects can rely on a standardized space being available for each user to read and write variables on.
* <code>Dash</code>, which exists under the slot containing the [[Component:UserspaceRadiantDash|UserspaceRadiantDash]] in userspace.
 
== Example Usage ==
 
=== One Contained System ===
Say you are working on a large and complicated system. In lieu of trying to manage "global" settings by sourcing ValueField components, you can use dynamic variables to help assign names to settings or other useful values. This is usually accomplished by placing a DynamicVariableSpace on the root of your object (or root of your "library") and having a specialized dynamic variable slot for containing the dynamic variables. Depending on how many dynamic variables are used, it may be beneficial to delegate them to multiple slots.
 
A screenshot showing a simple setup like this is shown below. Note the use of [[ProtoFlux:Dynamic Variable Input|Dynamic Variable Input]] nodes for performance sake.
 
[[File:Dynamic Variable Example Internal System.webp|800px]]
 
=== Controlling Another System ===
Say that you want to make an external controller for a system on an avatar. This system uses the default <code>User</code> dynamic variable space and namespaces itself with <code>User/CoolAvatarSystem.</code>.
 
The simplest way to get a slot to interface with this system can be obtained by getting the [[User]] in some fashion and plugging it into the [[ProtoFlux:User Root Slot|User Root Slot]] node. Reading with this system should be done with the [[ProtoFlux:Read Dynamic Variable|Read Dynamic Variable]] node, as the controller is not part of the dynamic variable space on the user.
 
From there, it's simply a matter of knowing the right variable names and types (e.g. <code>User/CoolAvatarSystem.Enabled</code> being a <code>bool</code>). Plugging the previously obtained slot into the <code>Source</code> of a Read Dynamic Variable of the matching type and path will get you the current value, while using a Write Dynamic Variable will allow you to write to it.
 
== See Also ==
* [[Dynamic Impulses]]

Latest revision as of 06:05, 3 July 2025

The data model refers to the declaration, structure, and handling of types, data, and changes on data within FrooxEngine. The data model allows for change detection, value synchronization, unified structure, and serialization of any and all types that FrooxEngine encounters.

Elements

Everything that gets managed by the data model in some fashion implements the IWorldElement interface. This interface assigns elements with a host of properties, including a reference ID, possible parent element, and whether the element is persistent or not.

This interface doesn't have much behavior by itself--it only specifies a thing as being able to be saved and loaded into worlds in some fashion. More complex behaviors offered by the data model are provided by types that implement this interface. The three types of elements that directly implement this interface are sync elements, workers, and worlds.

Sync elements

Sync elements represent a value or object that can be written to, linked, and send/receive change events from other elements. Sync elements mainly comprise of fields and various types of collections--usually any type name that starts with Sync derives from a sync element.

The main thing that separates sync elements from other elements is, well, synchronization. Whenever a sync element is changed, spare few exceptions like RawOutput<T>, it and its entire parent hierarchy propagate a change event. This includes the elements getting put in the world's update manager to synchronize their values at the end of the update loop.

Linking

Main article: Linkage

Linkage is a way to give exclusive control of a sync element to one source, often referred to a driver. Two types of linkage exist: links and drives, while drivers may choose an optional hook.

When a field is linked, it is shown as cyan in inspectors. Linked fields still send data model events as any normal field, but can only be changed by the driver.

When a field is driven, it is shown as purple in inspectors. Driven fields act the same as linked fields, but the value of a driven field will not be synchronized across the network. This allows for per-user values or local calculations of things that would otherwise be expensive to network.

Workers

Workers are, in essence, containers for sync elements and other workers to provide a specific, individual task for the engine. Examples of workers include slots, components, sub-classes of components, and streams. Workers are able to receive events from their children and propagate events to their parent.

Workers themselves are a local construct. However, they usually exist in sync element collections, and thus their existence is usually synchronized across users along with their elements.

Events

A big part of the data model's function is capturing changes to an element or worker and propagating these changes across the element's parent and references to the element. Everything that exists within the data model can capture and send these events, while things outside the data model will usually not.

For example, the Changeable Source ProtoFlux node is able to receive change events from the element that it references. A changeable source of a slot plugged in to a Get Slot Active node will be able to update a connected listener node whenever the active state of the slot changes. However, an Input node will not update any connected listener nodes in the same fashion. This is due to the former node receiving events from the data model, while the latter does not. Whenever the active field of a slot (a sync element) changes, it propagates that change to the slot itself (the worker containing the active field), which will then get received by the changeable source and thus the entire context of the listener node.

World

The data model exists under a world. The world update loop controls the communication and events between all individual concepts of the data model as one unified pipeline. This includes, but is not limited to:

  • The UpdateManager for managing all sync elements that are "dirty" and the changes that they incur on other elements/workers.
  • The LinkManager for managing all of linkage within a world.

Strict exclusivity

The data model can only function without issue when everyone in a session agrees on what the data model should be. If one client tells another client about an element type or worker type that doesn't exist for the recipient, the recipient won't know what to do with the information or how to sync the potential element values. This is the reason why plugins marked as a core assembly type cause incompatible sessions with those who do not have the plugin.

Further reading