Impulses: Difference between revisions

From Resonite Wiki
update video link to timestamp link
Marked this version for translation
 
(19 intermediate revisions by 4 users not shown)
Line 1: Line 1:
{{Stub}}
<languages />


== Impulses ==
<translate>
Impulses are a way for ProtoFlux to do actions that happen during a specific time, rather than a continuous per-game-tick basis. They can be considered as a sort of action. When an impulse is triggered it's like an event in a game. When the user hits a button that sends an impulse, when your feet visibly hit the ground, that can send an impulse. When you fill a glass entirely, an impulse can be sent.
<!--T:1-->
'''Impulses''', sometimes known as '''calls''', are discrete actions within the [[ProtoFlux]] [[ExecutionContext]] and one of the two basic types of chains to build. In contrast to chains of inputs and outputs to a node, which are generally continuous to one another, impulses push a task execution along the impulse chain, much like conventional [https://en.wikipedia.org/wiki/Imperative_programming imperative programming].
</translate>


Impulses have data associated with them. This data includes
== <translate><!--T:2--> Overview</translate> ==
* The user who sent the impulse, which can be read via a [[ProtoFlux:Local User|Local User]] Node.
* The index of the impulse of the total number of impulses sent down the line.
* the start time the impulse was sent.


Think of Impulses as an instant action, that doesn't have a duration, but rather an instant moment in time which all code must run before the next smallest unit of time can happen.
<translate>
<!--T:3-->
In ProtoFlux's [[ExecutionContext]] and its derivatives, nodes that perform a discrete action require an impulse to execute. For example, [[ProtoFlux:Duplicate Slot|duplicating a slot]] can't trigger whenever it wants--it would be impossible to control such a thing. As such, a special input <code>*</code> of type ''Call'' is used to trigger the action. This chain can then be continued via the <code>Next</code> output.
</translate>


When a protoflux value socket is connected solely into nodes that recieve Impulses, the value is no longer calculated every game tick but only when the node is impulsed. This is useful if you want to search the entire root for a slot, but then [[ProtoFlux:Write|Write]] it to a variable to keep it for later. In this scenario, the search only has to be done for one frame, and then the cached value can be used instead. Searching the entire root is extremely costly, but if the value is written and then cached, the performance impact is negligible.
<translate>
<!--T:4-->
Upon an action node receiving an impulse, it evaluates all the non-impulse inputs connected to the node. This contextualizes the relationship between impulses and other node chains: Impulses push task execution, while action nodes pull their inputs, which may pull their inputs, and so on until the original input can be evaluated.
</translate>


{{Note|Non Async Impulse Code will freeze the game engine loop until the code is finished. This can cause huge performance issues, and is why it is highly recommended to make large computations Async|danger}}
<translate>
<!--T:5-->
Impulses are a local runtime by default within ProtoFlux. If interacting with the [[FrooxEngine]] [[data model]] in any fashion, however, the actions they perform get synced across users. This includes, but is not limited to, [[ProtoFlux:Write|writing]] to a non-driven [[Type:SyncField`1|synced field]], managing [[slots]], and setting non-driven [[dynamic variables]].
</translate>


== Async ==
<translate>
Async or Async Impulses are a way to take your code and run it over more than one engine update cycle. Async Impulses are required to run nodes that are in the ProtoFlux Async category as these nodes require a Async Operation to start executing and will often take some time to complete what they do. Nodes like this include but are not limited to [[ProtoFlux:Delay|Delays]], [[:Category:ProtoFlux:Variables:Cloud|Cloud variable nodes]], and [[ProtoFlux:ASync While|ASync Whiles]].  
<!--T:6-->
Impulses can come from a variety of sources. Most commonly, when building ProtoFlux, a [[ProtoFlux:Call Input|Call Input]] can be created by dragging out an impulse input and pressing [[Basic Controls|secondary]]. Impulses may also come from ''events'', or impulse chains that start once receiving some sort of signal. These include, but are not limited to, [[Dynamic Impulses|dynamic impulses]], [[ProtoFlux:Button Events|button events]], [[ProtoFlux:Fire On True|''fire on'' events]], and [[:Category:ProtoFlux:Flow:Events|world/item events]].
</translate>


Async is a way in some cases to reduce lag in some code as instead of halting the engine while you do a bunch of work in one update, you can spread the work over time and over multiple updates instead.
== <translate><!--T:7--> Context</translate> ==


Async however will not completely remove lag from actions done in world. So if you create a massive amount of data that has to be networked all at once, then making the code Async will not always prevent the lag from occurring.
<translate>
<!--T:8-->
Impulses are attached to a particular instance of an ExecutionContext. This is not to be confused with the general concept of [[contexts]] in ProtoFlux, as all impulses are from an ExecutionContext, but rather refers to the specific values and variables that the impulse sees.
</translate>


Async is currently not multi-threaded which is said in this video (at 40:00): [https://youtu.be/1losWav_AZQ?t=2392 link to the video at the specified time]
<translate>
<!--T:9-->
Context is picked up by the trigger of the original impulse and carried until the chain completes. Contexts carry certain values with them throughout the duration of the chain, including [[ProtoFlux:Local|local values]] and outputs to action nodes. Outside of the specific context (and nested contexts) that they are used, it is impossible to access the underlying value of one of these context-sensitive values.
</translate>


== Contexts ==
<translate>
<!--T:10-->
Context is kept when passing through the same ''node group'' and lost if there is a disconnect of execution to a different node group. Node groups are the nodes that are all connected in some way or another, whether through direct wire connections or by references to another node, such as referencing a variable for a write node.
</translate>


Contexts are a way for the game to store a frozen state of the game in which to keep values the way they are during execution. This means if you use a [[ProtoFlux:Fire On True|Fire On True]], [[ProtoFlux:Fire On False|Fire On False]], or a [[ProtoFlux:Fire On Change|Fire On Change]] the data in the entire world during the impulse generated by the nodes will not change except when acted upon by the code. This is why functions like creating dynamic variables, moving them, or checking protoflux indirectly will not work instantly and will require a new context from a few game ticks later.  
<translate>
<!--T:11-->
For example, if a [[dynamic impulse]] is pulsed, and that dynamic impulse pulses another one, and that third dynamic impulse writes to a local, the change will ''not'' be seen by the <code>Next</code> path of the first dynamic impulse trigger. This is because the execution leaves the node group of the first trigger after the first dynamic impulse is pulsed, causing the second dynamic impulse to not be within the same context as the initial chain. If the second dynamic impulse wrote to the same local, then all the node groups are connected, and the change <em>will</em> be seen by the initial <code>Next</code> path.
</translate>
<!-- TODO: make an example image illustrating the above... -->


For example, if you change the Position of a [[Slot]], and then check a value that should change via a write node based on the slot's position, it will not update. This is because the other impulse chain is waiting for that context to be released, and will not fire till the next game tick. New contexts can be generated through a delay, or by a new impulse in later game ticks.
== <translate><!--T:12--> Impulse flow</translate> ==


On the contrary if a value that is being driven by other protoflux or components is checked during our impulse, the code driving it will update the value first (Also known as Evaluating) before our code uses the final value for the execution.
<translate>
<!--T:13-->
Impulse flow dictates how the impulse will be executed in relation to the runtime of the engine. There are two types of impulse flows: non-async and async.
</translate>


The StartAsyncTask node will create a new context from which all previous values read from ProtoFlux nodes will be frozen and still be able to be read from any downstream nodes even if Delays are used.
<translate>
<!--T:14-->
A non-[[#Async|async]] impulse chain will run entirely in one engine update, halting the engine for as long as it takes to complete. If the impulse does actions that are synced across users, only the difference between the initial state the end of the engine update is synced. For example, if one duplicates and removes a load of slots in one non-async impulse chain, no data will be synced over the network.
</translate>


=== <translate><!--T:15--> Async</translate> ===


[[ProtoFlux:Local|Local Nodes]] will be the default value for it's data type till it is written to during a context. During that context, the local node will keep the value from every write and change to it. Once the context is released, the value instantly returns to the default value, discarding the data, preventing it from being seen or networked. However, if it's value is written to a [[ProtoFlux:Data Model Store|Data Model Store]] it will persist across all contexts, allowing it to be seen by everyone in the session through the network.
<translate>
<!--T:16-->
When an action has the potential to take multiple engine updates to complete, an '''async''' impulse flow is required. Otherwise, the node chain will raise an exception and refuse to execute.
</translate>


A [[ProtoFlux:Data Model Store|Data Model Store]] on the other hand will allow writes to it that will persist across contexts, allowing for another impulse or script to read the variable later and act on it. This is good for storing your final value after a massive calculation as a value for a smaller drive script. If a [[ProtoFlux:Data Model Store|Data Model Store]] is written to multiple times in a context, the last value the data model store has after the context is the value that gets networked.
<translate>
<!--T:17-->
Async impulse flow is generally the same as normal impulse flow, though with the added flexibility of being able to suspend and resume certain chains of execution at will. In essence, it is ProtoFlux's way of being able to use [https://en.wikipedia.org/wiki/Coroutine coroutines] within the language. One can either explicitly suspend execution with nodes such as [[ProtoFlux:Delay|Delay]], or one can implicitly wait for an action to complete before resuming execution, such as using the [[ProtoFlux:Play One Shot And Wait|Play One Shot And Wait]] node.
</translate>
 
<translate>
<!--T:18-->
An async impulse flow is able to preserve locals and action node outputs across multiple engine updates, something not possible with normal impulse flow.
</translate>
 
<translate>
<!--T:19-->
It is possible to start an async impulse chain from a normal impulse chain using the [[ProtoFlux:Start Async Task|Start Async Task]] node. This node actually creates a <em>branched</em> ExecutionContext at <code>OnTriggered</code> that is distinct from the context that triggered it. Every value used by the triggered ExecutionContext gets duplicated into the new context, but after that point, neither context is able to affect the other.
</translate>
 
<translate>
<!--T:20-->
In contrast, all the other async nodes <em>share</em> the same context. For example, a local variable modified within the <code>OnTriggered</code> chain of a [[ProtoFlux:Delay|Delay]] node <em>will</em> be reflected in the <code>Next</code> chain of the same node, and vice versa if the node gets triggered twice in one context. This is what makes async impulse flow so powerful.
</translate>
 
<translate>
<!--T:21-->
If processing a lot of data using ProtoFlux, it may be desirable to use async impulse flow and spread execution across multiple frames, as it will prevent a massive framerate hitch when executing at the cost of taking slightly longer.
</translate>
<!-- TODO: make an example image showing the difference between async sequence and sequence -> start async tasks... -->
 
== <translate><!--T:22--> See Also</translate> ==
 
* <translate><!--T:23--> [[ProtoFlux:Local]], [[ProtoFlux:Store]], and [[ProtoFlux:Data Model Store]] for the three types of variables one can access in ProtoFlux.</translate>
* <translate><!--T:24--> [[:Category:ProtoFlux:Flow]] for nodes that can control how an impulse flows.</translate>
 
[[Category:Core concepts]]

Latest revision as of 23:24, 28 January 2025

Impulses, sometimes known as calls, are discrete actions within the ProtoFlux ExecutionContext and one of the two basic types of chains to build. In contrast to chains of inputs and outputs to a node, which are generally continuous to one another, impulses push a task execution along the impulse chain, much like conventional imperative programming.

Overview

In ProtoFlux's ExecutionContext and its derivatives, nodes that perform a discrete action require an impulse to execute. For example, duplicating a slot can't trigger whenever it wants--it would be impossible to control such a thing. As such, a special input * of type Call is used to trigger the action. This chain can then be continued via the Next output.

Upon an action node receiving an impulse, it evaluates all the non-impulse inputs connected to the node. This contextualizes the relationship between impulses and other node chains: Impulses push task execution, while action nodes pull their inputs, which may pull their inputs, and so on until the original input can be evaluated.

Impulses are a local runtime by default within ProtoFlux. If interacting with the FrooxEngine data model in any fashion, however, the actions they perform get synced across users. This includes, but is not limited to, writing to a non-driven synced field, managing slots, and setting non-driven dynamic variables.

Impulses can come from a variety of sources. Most commonly, when building ProtoFlux, a Call Input can be created by dragging out an impulse input and pressing secondary. Impulses may also come from events, or impulse chains that start once receiving some sort of signal. These include, but are not limited to, dynamic impulses, button events, fire on events, and world/item events.

Context

Impulses are attached to a particular instance of an ExecutionContext. This is not to be confused with the general concept of contexts in ProtoFlux, as all impulses are from an ExecutionContext, but rather refers to the specific values and variables that the impulse sees.

Context is picked up by the trigger of the original impulse and carried until the chain completes. Contexts carry certain values with them throughout the duration of the chain, including local values and outputs to action nodes. Outside of the specific context (and nested contexts) that they are used, it is impossible to access the underlying value of one of these context-sensitive values.

Context is kept when passing through the same node group and lost if there is a disconnect of execution to a different node group. Node groups are the nodes that are all connected in some way or another, whether through direct wire connections or by references to another node, such as referencing a variable for a write node.

For example, if a dynamic impulse is pulsed, and that dynamic impulse pulses another one, and that third dynamic impulse writes to a local, the change will not be seen by the Next path of the first dynamic impulse trigger. This is because the execution leaves the node group of the first trigger after the first dynamic impulse is pulsed, causing the second dynamic impulse to not be within the same context as the initial chain. If the second dynamic impulse wrote to the same local, then all the node groups are connected, and the change will be seen by the initial Next path.

Impulse flow

Impulse flow dictates how the impulse will be executed in relation to the runtime of the engine. There are two types of impulse flows: non-async and async.

A non-async impulse chain will run entirely in one engine update, halting the engine for as long as it takes to complete. If the impulse does actions that are synced across users, only the difference between the initial state the end of the engine update is synced. For example, if one duplicates and removes a load of slots in one non-async impulse chain, no data will be synced over the network.

Async

When an action has the potential to take multiple engine updates to complete, an async impulse flow is required. Otherwise, the node chain will raise an exception and refuse to execute.

Async impulse flow is generally the same as normal impulse flow, though with the added flexibility of being able to suspend and resume certain chains of execution at will. In essence, it is ProtoFlux's way of being able to use coroutines within the language. One can either explicitly suspend execution with nodes such as Delay, or one can implicitly wait for an action to complete before resuming execution, such as using the Play One Shot And Wait node.

An async impulse flow is able to preserve locals and action node outputs across multiple engine updates, something not possible with normal impulse flow.

It is possible to start an async impulse chain from a normal impulse chain using the Start Async Task node. This node actually creates a branched ExecutionContext at OnTriggered that is distinct from the context that triggered it. Every value used by the triggered ExecutionContext gets duplicated into the new context, but after that point, neither context is able to affect the other.

In contrast, all the other async nodes share the same context. For example, a local variable modified within the OnTriggered chain of a Delay node will be reflected in the Next chain of the same node, and vice versa if the node gets triggered twice in one context. This is what makes async impulse flow so powerful.

If processing a lot of data using ProtoFlux, it may be desirable to use async impulse flow and spread execution across multiple frames, as it will prevent a massive framerate hitch when executing at the cost of taking slightly longer.

See Also