Closed desplesda closed 7 months ago
As I mentioned on the Discord server, the ability to also leverage Yarn Functions with Smart Variables opens up more ways to incorporate data from the game into the Yarn story so I really do like this proposal. However I see a few issues in terms of usability that I want to highlight.
The first is an issue of syntax, smart variables look exactly like normal variables. This isn't too much of an issue when defining them, but just reading a normal story it can get confusing to distinguish the two. Of course this can be alleviated with proper naming conventions, so instead of $PieShouldAppear
its $S_PieShouldAppear
, but this could also be fixed on the language level. While this isn't the biggest issue by itself, it might be worth considering with my other issues.
Which would be the behaviour when switching Yarn Projects, with normal variables a general declare
statement for its type would be enough but Smart Variables need more information. This could mean that the function of a smart variable could change between projects, which would be an added feature, but also cause some possible unknowns like:
For all these scenarios I expect errors, but 1 and 4 are human errors that will happen. My proposed solution for scenario 4 is to have smart variables have an extra $
character, so $$PieShouldAppear
will be a smart variable while $PieShouldAppear
is a normal variable.
Scenario's 2 and 3 requires an understanding of how Smart Variables are evaluated, but it isn't an far fetch mistake to make if you assume smart and normal variables are identical, or that the evaluated value gets saved. For scenario 3 I would want some sort of feedback where the warning/error would mention if the smart variable had a definition in a previous project, but the current loaded one doesn't.
Can't regular variables also be different types between project A and project B? Maybe I'm thinking of it wrong, but I thought YarnProjects were basically their each their own domain, where variables and node names etc. could differ. I've only ever had one yarn project in my game, but I didn't think there was any special logic to carry anything over when switching projects so I'm wondering how this applies to smart variables in particular rather than all variables
That is true, I forgot scenario 1 could happen to normal variables too. Though if I'm understanding the logic of how Smart Variables would work, they should work without any problems contained in their own projects, just confusingly changing type in context to other projects. In comparison to normal variables where mixing types would be a more apparent problem.
I can definitely answer some of these with what I think should happen
- What happens when a Smart Variable defined in Project A has a different type than one with the same name in Project B?
Same as what happens with any duplicate declaration, it's an error, nothing changes here in regards to smart variables.
- What happens when a Smart Variable is accessed through the implementation but isn't declared in the project?
Smart Variables are intended to be a way to wrap up a lot of variables into one easier to interface with one. As such they must be declared in the project to be accessed externally. Another way to put it is "what happens if you access a regular variable that doesn't exist?" because the same thing should happen with Smart Variables, the variable store will go "I don't know what that is".
- What if the Smart Variable was declared in Project A, the game then switches to Project C where there isn't a declaration because it isn't used, but scenario 2 happens?
Same as Scenario 2, the variable storage won't know what it is and will tell you as such.
- What if a variable of the same name was declared as a smart variable in Project A, but a normal variable in Project D? What happens when a game switches between them?
This IMO is the same as Scenario 1, that it is a smart variable doesn't change that it is has been declared multiple times, it's an error to do this. Now the difference here is you did it at runtime but this is still an issue that persists without it being smart variables. Because you control the variable storage, swapping out the values of variables at runtime is in your hands. In the case of the WIP branch it will I think use the smart variable value because we check for variables being smart first and then fallback to store, I would have to double check to be sure.
Overall the way to think of smart variables in my mind is the same as computed properties in C#. Unless you dive into the inner workings of an object you won't know or even worry about them. In the case of Yarn Spinner because you have the ability to also control the memory you do have to at least consider them, but I am willing to bet most people use the built in variable storage in which case again they don't have to worry.
This feature has been announced as part of Yarn Spinner 3.0. Accordingly, I'll close the issue here - thanks for the discussion, everyone!
Introduction
This proposal adds support for 'smart variables', which are read-only variables whose value is determined by evaluating an expression at run-time, rather than by storing and retrieving a value on disk or in memory. Smart variables are declared in Yarn scripts, and are accessible from inside Yarn scripts and and from outside Yarn (such as from a Unity C# script).
Rationale
There are several reasons for this feature.
Expression Simplification
Developers store game state in Yarn variables, like "door unlocked" and "has met the baker". A common requirement is to have story progression depend upon multiple variables at the same time: 'door unlocked and has met the baker'. This is expressible as a boolean expression (
$Door_Unlocked && $HasMetBaker
), but this becomes unwieldy when the expressions become more complex, as the Yarn scripts become more difficult to read, and the opportunities for errors increases.External Access
Dialogue is not the only part of a game that can depend upon a more complex expression. Often, the results of a conversation affects the overall state of the world. For example, imagine that when the door is unlocked and the player has met the baker, a pie appears on the table. In order to determine whether the pie is visible or not, the game needs to be able to get an answer to the question of whether both the door is unlocked and the player has met the baker.
Proposed solution
The overall direction of this feature is that smart variables can be read by any part of the game, including outside of Yarn scripts, just like a regular ('stored') variable.
This feature makes the following changes to the Yarn Spinner compiler and runtime.
Compiler Changes
declare
statements now accept an expression, rather than requiring a constant value. Variables declared in this way are called 'smart variables'.declare
statement may be any legal Yarn expression, and may reference other smart variables.declare
statements to create a dependency loop (for example, it is an error forA
to depend onB
, and forB
to depend onA
).SetValue
on a variable storage object)Runtime Changes
InMemoryVariableStorage
) keeps a reference to a 'smart variable evaluator', which is an object that can determine the runtime value of a smart variable. (This is typically theDialogue
object that owns the variable storage.)The smart variable evaluator produces the value of a smart variable by first finding the node that contains the Yarn bytecode for the smart variable, and then evaluating that bytecode. This produces the result, which the smart variable evaluator returns.
Detailed design
The
declare_statement
rule in the grammar is updated to permit expressions, whereas it previously permitted only constant values.For each variable declaration where the type is non-constant (i.e. a runtime-expression), the compiler will emit a Node containing bytecode that evaluates the expression, leaving the expression result at the top of the stack. (This value is then popped by a smart variable evaluator as the final step of determining a variable's expression.)
The
IVariableStorage
interface is modified to require two new properties:ISmartVariableEvaluator
contains one method:Backwards Compatibility
This feature makes purely additive changes to the Yarn language grammar; previously,
declare
statements required a constant value, and this is now expanded to permit expressions.Finally, the Yarn Spinner Language Server, Try Yarn Spinner, and the Yarn TypeScript runner will need to be updated with the updated grammar and functionality.
We will mitigate the impact of these API changes by updating existing types, such as
InMemoryVariableStorage
, to have this support built-in (and updating existing code accordingly). These types are frequently subclassed by end-users, so they will receive this functionality as a built-in feature and will not need to make any changes.Alternatives considered
Other considered alternatives include:
Both of these approaches introduce more work required for end-users, and in the case of adding new function syntax, significantly change the architecture of Yarn Spinner. Accordingly, a more lightweight approach is preferred.
Acknowledgments
We'd like to thank the Yarn Spinner Discord members for their thoughts on the design of this feature; in particular, Khan-ali Ibrahim (@KXI-System) for the term 'smart variable', Robert Yang (@radiatoryang), and @dogboydog.