Scripter: High performance scripting in Virt-A-Mate
🕑 Added 2023-02-06 04:15:54 +0000 UTCI'm doing something a little bit crazy, but I feel like this may be a missing piece of VaM. We have _great_ plugins like LogicBricks (if you never saw it, it's amazing) and VUML, but they both require building the logic visually.
But if you know how to program stuff, or if you want to make more complex things, it can quickly become a problem.
Sooooo I tried writing my own tokenizer, abstract syntax tree, expression parser and runtime language (greatly inspired from JavaScript and C#) to allow high-performance code.
Example use cases:
- A Keybindings mapping that does multiple things, or does different things based on conditions
- Dynamic things like states, arousal meters, etc.
- Integrating multiple plugins together
- Adding conditions to triggers
Right now the language is fairly simple but already powerful enough for most; there aren't many integration points with VaM and Unity though, and I plan (if there is interest) to improve on the language.
If you want to try it, please keep in mind I'll definitely break your scenes with some language-level changes.
Language Features
- Types (string, float, int, bool)
- Boolean operators (||, &&, ==, !=, <, >, <=, >=)
- Math operators (+, -, *, /)
- String concatenation (+)
- Assignment operators (++, --, +=, -=, *=, /=)
- Comments (//, /* */)
- Keywords (var, if, else, for, while, true, false, throw, static)
- Code blocks ({, })
- Lexical scope (scoped variables)
Notable Omissions
- Objects and dictionaries cannot be created
- Custom functions cannot be declared, nor classes
- Order of precedence, like 1 + 2 * 3 + 4
- Code blocks without brackets, like if(condition) something();
Virt-A-Mate Functions
- getDateTime(format) (.NET: DateTime.Now.ToString(format))
- logMessage(message) (VAM: SuperController.LogMessage)
- logError(message) (VAM: SuperController.LogError)
- getTime() (Unity: Time.time)
- getDeltaTime() (Unity: Time.deltaTime)
- getRandom (Unity: Random.value when no arguments are provided, Random.Range(min, max) when two arguments are provided)
- getFloatParamValue(atomName, storableName, paramName)
- setFloatParamValue(atomName, storableName, paramName, value)
- getBoolParamValue(atomName, storableName, paramName)
- setBoolParamValue(atomName, storableName, paramName, value)
- getStringParamValue(atomName, storableName, paramName)
- setStringParamValue(atomName, storableName, paramName, value)
- getStringChooserParamValue(atomName, storableName, paramName)
- setStringChooserParamValue(atomName, storableName, paramName, value)
- invokeTrigger(atomName, storableName, paramName)
- invokeKeybinding(bindingName)
Demo Scene
The demo scene shows how it works (for now at least). The Person atom has the Scripter plugin, which has two triggers. An Action trigger (invoked by the button) and an Update trigger (invoked every frame). For those of you who'd like to see before they download:
OnAction: OnButtonClicked
static var currentAnimation = "";
static var expectedAnimation = "Play Happy";
static var currentBubbleText = "";
static var expectedBubbleText = "Check out my button!";
var annoyedLevel = getFloatParamValue("UISlider", "Trigger", "value");
annoyedLevel += 0.05;
setFloatParamValue("UISlider", "Trigger", "value", annoyedLevel);
if(annoyedLevel > 0.8) {
expectedBubbleText = "STOP IT!";
expectedAnimation = "Play Angry";
} else if(annoyedLevel > 0.5) {
expectedBubbleText = "That's annoying...";
expectedAnimation = "Play Annoyed";
} else if(annoyedLevel > 0.2) {
expectedBubbleText = "Okay...";
expectedAnimation = "Play Confused";
} else {
expectedBubbleText = "Hello!";
expectedAnimation = "Play Happy";
}
if(currentAnimation != expectedAnimation) {
setStringParamValue("Person", "SpeechBubble", "bubbleText", "STOP IT!");
invokeTrigger("Person", "plugin#1_VamTimeline.AtomPlugin", expectedAnimation);
currentAnimation = expectedAnimation;
}
if(currentBubbleText != expectedBubbleText) {
setStringParamValue("Person", "SpeechBubble", "bubbleText", expectedBubbleText);
currentBubbleText = expectedBubbleText;
}
OnUpdate: AnnoyMeter
var annoyedLevel = getFloatParamValue("UISlider", "Trigger", "value");
static var angry = false;
if(annoyedLevel == 0) {
return 0;
}
if(annoyedLevel > 0.8) {
angry = true;
}
if(annoyedLevel < 0.8 && !angry) {
return 0;
}
annoyedLevel -= 0.001;
if(annoyedLevel <= 0) {
annoyedLevel = 0;
angry = false;
setStringParamValue("Person", "SpeechBubble", "bubbleText", "I'm calm now.");
invokeTrigger("Person", "plugin#1_VamTimeline.AtomPlugin", "Play Happy");
}
setFloatParamValue("UISlider", "Trigger", "value", annoyedLevel);
What do you think?
I believe that with a little bit of work, this could become the ultimate power tool for more advanced scenes, and the easiest way to do stuff in VaM without tons of plugins for basic things. But this might also be overkill for users without programming knowledge, and useless because of all the amazing plugins already out there.
I'd _love_ to get feedback on that! Would you use this?
Comments
Acid Bubbles
You can wait a little bit unless you're very curious, I'm implementing objects, getters, setters, methods, arrays, static initialization and performance optimizations, they are nearly done. Then maybe the ability to declare functions and references and export them for other scripts. That will make scripting much better. I'm also considering making an npm package to write those scripts in code or webstorm directly :D Ok ok one thing at a time.
Bernie
Good point, well, I already wrote a plugin for it, I guess I would then just hook into that (if needed). I should have some time to check your scripting plugin soon btw, will provide feedback after.
Acid Bubbles
UDP will definitely not be in the scope of Scripter... that would circumvent security measures by allowing anyone to contact the outside network. That's probably more suited for a dedicated plugin (though that plugin COULD be scripted using Scripter)
Bernie
(i've also got some custom hardware hooked up to VAM, so it'd be great if we could use UDP etc)