diff --git a/docs/blueprints/templates/overview.md b/docs/blueprints/templates/overview.md index b42f493..9e2d78c 100644 --- a/docs/blueprints/templates/overview.md +++ b/docs/blueprints/templates/overview.md @@ -4,7 +4,7 @@ sidebar_position: 0 # Overview -In this section, we provide some ready-to-use blueprints that you can just import into your scene and use right away! Technically, you don't need to read any of the other sections to use these blueprints, but we still recommend you to read [Understanding Blueprints](../understanding-blueprints) to get a better understanding of how blueprints work. +In this section, we provide some ready-to-use blueprints that you can just import into your scene and use right away! Technically, you don't need to read any of the other sections to use these blueprints, but we still recommend you to read [Creating Your First Blueprint](../understanding-blueprints) to get a better understanding of how blueprints work. ## Setup diff --git a/docs/blueprints/tutorials/overview.md b/docs/blueprints/tutorials/overview.md index 3815138..5b575d6 100644 --- a/docs/blueprints/tutorials/overview.md +++ b/docs/blueprints/tutorials/overview.md @@ -5,7 +5,7 @@ sidebar_position: 0 # Overview :::info -If you have not read the [Understanding Blueprints](../understanding-blueprints.md) section, we recommend that you do so before continuing. +If you have not read the [Creating Your First Blueprint](../understanding-blueprints.md) section, we recommend that you do so before continuing. ::: Creating a blueprint is really just a two-step process: diff --git a/docs/blueprints/tutorials/toggle-meshes.md b/docs/blueprints/tutorials/toggle-meshes.md index 7ee68ad..ee7f515 100644 --- a/docs/blueprints/tutorials/toggle-meshes.md +++ b/docs/blueprints/tutorials/toggle-meshes.md @@ -11,7 +11,7 @@ Often character models will have multiple meshes for different clothing and acce ## Toggling Meshes -Without further ado, let's get started! First, let's create a new blueprint. Just like in the [Understanding Blueprints](../understanding-blueprints) tutorial, we need to add a **On Keystroke Pressed** node. I will set the hotkey to **Ctrl+Shift+J** (J for "jacket"), but you can use whatever you want. +Without further ado, let's get started! First, let's create a new blueprint. Just like in the [Creating Your First Blueprint](../understanding-blueprints.md) tutorial, we need to add a **On Keystroke Pressed** node. I will set the hotkey to **Ctrl+Shift+J** (J for "jacket"), but you can use whatever you want. ![](/doc-img/en-blueprint-toggle-meshes-1.png) diff --git a/docs/scripting/api/_category_.json b/docs/scripting/api/_category_.json new file mode 100644 index 0000000..1321d8e --- /dev/null +++ b/docs/scripting/api/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 10, + "label": "Scripting API", + "collapsible": true, + "collapsed": false +} diff --git a/docs/scripting/api/assets.md b/docs/scripting/api/assets.md new file mode 100644 index 0000000..a0d6811 --- /dev/null +++ b/docs/scripting/api/assets.md @@ -0,0 +1,173 @@ +--- +sidebar_position: 40 +--- + +# Assets + +Assets are self-contained objects that implement a feature or a behavior in the scene. Different from nodes, an asset usually encapsulates a more complex logic. They can be thought of classes in a program and are more similar to Unity's `MonoBehaviour`. + +## Type Definition + +You can create asset types that can be instantiated and stored in the scene. An asset type inherits from the `Asset` type and is decorated with the `[AssetType]` attribute, like below: + +```csharp +[AssetType( + Id = "c6500f41-45be-4cbe-9a13-37b5ff60d057", + Title = "Hello World", + Category = "CATEGORY_DEBUG", + Singleton = false +)] +public class HelloWorldAsset : Asset { + // Asset implementation +} +``` + +Here's a summary of the parameters: + +- **`Id`**: A unique identifier for the asset type; you should [generate a new GUID](https://www.guidgenerator.com/online-guid-generator.aspx) for each new asset type. Note that this is different from the asset instance's UUID (`asset.Id`). +- **`Title`**: The name of the asset type that will be displayed in the *Add Asset* menu. +- **`Category`**: Optional. The group of the asset in the *Add Asset* menu. +- **`Singleton`**: Optional. If set to `true`, only one instance of the asset can exist in the scene. Default is `false`. + +:::info +Here are some common asset categories you can use: `CATEGORY_INPUT`, `CATEGORY_CHARACTERS`, `CATEGORY_PROP`, `CATEGORY_ACCESSORY`, `CATEGORY_ENVIRONMENT`, `CATEGORY_CINEMATOGRAPHY`, `CATEGORY_EXTERNAL_INTERACTION`, `CATEGORY_MOTION_CAPTURE`. +::: + +## Components + +An asset type can define data inputs and triggers. Unlike nodes, assets do not have data outputs or flow inputs/outputs. + +![](/doc-img/en-scripting-concepts-4.png) + +## Lifecycle + +Assets have the lifecycle stages listed on the [Entities](entities#lifecycle) page. You can override these methods to perform various tasks; for example, `OnUpdate()` is called every frame, similar to Unity's `Update()` method. + +## Active State {#active-state} + +Different from nodes, assets have an active state that inform whether the asset is "active", or ready to use. For example, when a character asset does not have a `Source` selected, it is shown as inactive in the editor. + +![](/doc-img/en-custom-asset-1.png) + +By default, assets are **NOT** active when they are created. You can set the active state of an asset by calling `SetActive(bool state)`. For example, if your asset is always ready to use, you can set it to active in the `OnCreate` method: + +```csharp +public override void OnCreate() { + base.OnCreate(); + SetActive(true); +} +``` + +If your asset only works when connected to an external server, e.g., a remote tracking device, you can set it to active only if the connection is successfully established: + +```csharp +[DataInput] +public string RemoteIP = "127.0.0.1"; + +[DataInput] +public string RemotePort = "12345"; + +public override void OnCreate() { + base.OnCreate(); + WatchAll(new [] { nameof(RemoteIP), nameof(RemotePort) }, ResetConnection); // When RemoteIP or RemotePort changes, reset the connection +} + +protected void ResetConnection() { + SetActive(false); // Inactive until connection is established + if (ConnectToRemoteServer(RemoteIP, RemotePort)) { + SetActive(true); + } +} +``` + +:::tip +Determining whether your asset is "ready to use" is entirely up to you. The convention that Warudo's internal assets use is that an asset is active when all data inputs required for the asset to function properly are set. +::: + +## Creating GameObjects + +You can create GameObjects in the (Unity) scene anytime you want. For example, the following asset creates a cube GameObject when the asset is created, and destroys it when the asset is destroyed: + +```csharp +private GameObject gameObject; + +public override void OnCreate() { + base.OnCreate(); + gameObject = GameObject.CreatePrimitive(PrimitiveType.Cube); +} + +public override void OnDestroy() { + base.OnDestroy(); + Object.Destroy(gameObject); +} +``` + +However, the user cannot move this cube around, since there are no data inputs that control the cube! You can add data inputs to control the cube's position, scale, etc., but an easier way is to inherit from the `GameObjectAsset` type: + +```csharp +using UnityEngine; +using Warudo.Core.Attributes; +using Warudo.Plugins.Core.Assets; + +[AssetType( + Id = "4c00b14a-aed5-423e-abe6-6921032439c5", + Title = "My Awesome Cube", + Category = "CATEGORY_DEBUG" +)] +public class MyAwesomeCubeAsset : GameObjectAsset { + protected override GameObject CreateGameObject() { + return GameObject.CreatePrimitive(PrimitiveType.Cube); + } +} +``` + +The `GameObjectAsset` handles creating and destroying the GameObject for you, and it comes with a `Transform` data input that allows the user to control the GameObject's position, rotation, and scale: + +![](/doc-img/en-custom-asset-2.png) + +:::tip +When to use `GameObjectAsset`? If your asset is "something that can be moved by the user in the (Unity) scene", then it's probably a good idea to inherit from `GameObjectAsset`. +::: + +## Events + +The `Asset` type invokes the following events that you can listen to: + +- **`OnActiveStateChange`**: Called when the active state of the asset changes. +- **`OnSelectedStateChange`**: Called when the asset is selected or deselected in the editor. +- **`OnNameChange`**: Called when the name of the asset changes. + +For example, the built-in [Leap Motion tracking](../../mocap/leap-motion) asset listens to the `OnSelectedStateChange` event to display a Leap Motion controller model in the (Unity) scene when the asset is selected. + +```csharp +public override void OnCreate() { + base.OnCreate(); + OnSelectedStateChange.AddListener(selected => { + if (selected) { + // Show the model + } else { + // Hide the model + } + }); +} +``` + +## Code Examples + +### Basic + +- [AnchorAsset.cs](https://gist.github.com/TigerHix/c549e984df0be34cfd6f8f50e741aab2) +Attachable / GameObjectAsset example. + +### Advanced + +- [CharacterPoserAsset.cs](https://gist.github.com/TigerHix/8413f8e10e508f37bb946d8802ee4e0b) +Custom asset to pose your character with IK anchors. + + diff --git a/docs/scripting/api/entities.md b/docs/scripting/api/entities.md new file mode 100644 index 0000000..4afe32d --- /dev/null +++ b/docs/scripting/api/entities.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 2 +--- + +# Entities + +An entity is an object that is persistent in a [scene](scene) (e.g., nodes, assets) or across scenes (e.g., plugins). Scripting in Warudo is all about defining your own entity types and interacting with the Warudo runtime. You can define your own entity type by inheriting a base class provided by Warudo: + +- **Node:** Inherit from `Node` to create a new node type. A node type can define data inputs, data outputs, flow inputs, flow outputs, and triggers. A node is instantiated by dragging it from the node palette to the blueprint editor; the node instance is saved along with the blueprint. +- **Asset:** Inherit from `Asset` to create a new asset type. An asset type can define data inputs and triggers. An asset is instantiated by using the *Add Asset* menu; the asset instance is saved along with the scene. +- **Plugin:** Inherit from `Plugin` to create a new plugin type. A plugin type can define data inputs and triggers. A plugin is loaded when Warudo starts, or when the plugin is hot-reloaded. There is only one plugin instance per plugin type, and data inputs are saved across scenes. +- **Structured Data:** Inherit from `StructuredData` to create a new [structured data](structured-data) type. Structured data is a complex data type that can be used as an "embedded" data input in nodes, assets, or plugins. + +:::info +In the [Creating Your First Script](../creating-your-first-script.md) tutorial, we created a custom node type called `HelloWorldNode` and a custom asset type called `CookieClickerAsset`. +::: + +:::tip +The concept of entities is loosely similar to Unity's MonoBehaviour, but with a more structured approach. +::: + +After an entity is instantiated, it is automatically assigned a UUID (unique identifier) by Warudo. You can access it via the `Id` property. This UUID is then used to identify the entity in the scene, and is saved along with the scene. + +An entity type can contain regular C# fields and methods. However, to interface with the Warudo runtime, you need to create [**ports and triggers**](ports-and-triggers) in the entity. All entities can have [data input ports](ports-and-triggers#data-input-ports), [data output ports](ports-and-triggers#data-output-ports) and [triggers](ports-and-triggers#triggers), while nodes can additionally have [data output ports](ports-and-triggers#data-output-ports), [flow input ports](ports-and-triggers#flow-input-ports), and [flow output ports](ports-and-triggers#flow-output-ports). + +![](/doc-img/en-custom-node-1.png) + +![](/doc-img/en-scripting-concepts-4.png) + +## Lifecycle {#lifecycle} + +An entity has a lifecycle that consists of several stages: + +- **`OnCreate()`:** Called when the entity is created. This is where you should initialize the instance fields, subscribe to events, etc. + - **Assets:** Called when a new asset is added to the scene by the user, or when the scene is loaded. + - **Nodes:** Called when a new node is added to the blueprint by the user, or when the scene is loaded. Nodes are always created _after_ assets. + - **Plugins:** Called when the plugin is loaded by Warudo on startup, or when the plugin is hot-reloaded. + - **Structured Data:** Called when the parent entity is created, or when the user adds a new structured data to an array of structured data. +- **`OnDestroy()`:** Called when the entity is destroyed. This is where you should release resources, etc. + - **Assets:** Called when the asset is removed from the scene by the user, or when the scene is unloaded. + - **Nodes:** Called when the node is removed from the blueprint by the user, or when the scene is unloaded. Nodes are always destroyed _after_ assets. +- **`Deserialize(TSerialized data)`:** Called when the entity is deserialized from a saved state. For example, this method is called on an asset when the scene is just opened and loaded. _You usually do not need to override this method._ +- **`Serialize()`:** Called when the entity needs to be serialized to a saved state. For example, this method is called on an asset when the scene is about to be saved, or need to be sent to the editor. _You usually do not need to override this method._ + +Assets, nodes, and plugins have extra lifecycle stages: + +- **`OnUpdate()`:** Called every frame when the entity is not destroyed. This is similar to Unity's `Update()` method. + - `OnPreUpdate()` and `OnPostUpdate()` are also provided. They are called before and after `OnUpdate()`, respectively. +- **`OnLateUpdate()`:** Called every frame after `OnUpdate()`. This is similar to Unity's `LateUpdate()` method. +- **`OnFixedUpdate()`:** Called every fixed frame. This is similar to Unity's `FixedUpdate()` method. +- **`OnEndOfFrame()`:** Called at the end of the frame. This is similar to using Unity's [`WaitForEndOfFrame` coroutine](https://docs.unity3d.com/ScriptReference/WaitForEndOfFrame.html). +- **`OnDrawGizmos()`:** Called when the entity should draw gizmos. This is similar to Unity's `OnDrawGizmos()` method. + +:::info +The update order is as follows: plugins → assets → nodes. +::: + + diff --git a/docs/scripting/api/events.md b/docs/scripting/api/events.md new file mode 100644 index 0000000..1d836de --- /dev/null +++ b/docs/scripting/api/events.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 90 +--- + +# Global Events {#events} + +Warudo contains a `EventBus` class that allows you to subscribe to and broadcast global events. To define a custom event class, inherit from the `Warudo.Core.Events.Event` class. For example: + +```csharp +public class MyEvent : Event { + public string Message { get; } + public MyEvent(string message) { + Message = message; + } +} +``` + +To subscribe (listen) to an event: + +```csharp +Context.EventBus.Subscribe(e => { + Debug.Log(e.Message); +}); +``` + +To broadcast (fire) an event: + +```csharp +Context.EventBus.Broadcast(new MyEvent("Hello, world!")); +``` + +You should unsubscribe from events when you no longer need to listen to them: + +```csharp +var subscriptionId = Context.EventBus.Subscribe(e => { + Debug.Log(e.Message); +}); +// Later +Context.EventBus.Unsubscribe(subscriptionId); +``` + +### Subscribing Events In Entities + +If you are writing code inside an entity type, you can use the `Subscribe` method directly to avoid handling event subscription IDs: + +```csharp +// Inside an entity type, i.e., asset, node, plugin, structured data +Subscribe(e => { + Debug.Log(e.Message); +}); +``` + +The events are automatically unsubscribed when the entity is destroyed. + + diff --git a/docs/scripting/api/generating-blueprints.md b/docs/scripting/api/generating-blueprints.md new file mode 100644 index 0000000..bc44955 --- /dev/null +++ b/docs/scripting/api/generating-blueprints.md @@ -0,0 +1,69 @@ +--- +sidebar_position: 130 +--- + +# Generating Blueprints + +When you [set up tracking](../../mocap/overview) in Warudo, you will notice that a tracking blueprint is generated. Generating blueprints can be useful especially if you want to preserve some flexibility for the user to modify the blueprint, instead of hard-coding the entire logic in a custom asset or plugin. + +## By Code + +The recommended way to generate blueprints is to use code. This allows you to control the generation (e.g., conditionally add or remove nodes), and it also ensures you are correctly referencing the node types. The downside is that it is more verbose to write. + +Here's a minimal example that generates a blueprint with two nodes: `On Update` and `Show Toast`. The `On Update` node is connected to the `Show Toast` node, so that the toast message is shown each frame. + +```csharp +var graph = new Graph { + Name = "My Awesome Blueprint", + Enabled = true // Enable the blueprint by default +}; + +// Add two nodes: On Update and Show Toast +var onUpdateNode = graph.AddNode(); +var toastNode = graph.AddNode(); +toastNode.Header = "Hey!"; +toastNode.Caption = "This is a toast message!"; + +// Connect the nodes so that the toast is shown each frame +graph.AddFlowConnection(onUpdateNode, nameof(onUpdateNode.Exit), toastNode, nameof(toastNode.Enter)); + +// Add the graph to the opened scene +Context.OpenedScene.AddGraph(graph); + +// Send the updated scene to the editor +Context.Service.BroadcastOpenedScene(); +``` + +:::tip +The editor automatically formats a blueprint if all nodes are at (0, 0) position. Therefore, you do not need to worry about correctly positioning the nodes. You can still access a node's position by using `node.GraphPosition`. +::: + +:::info +Not all built-in node types are included in the Warudo SDK. If you are creating a plugin mod and want to reference a node type that is not included in the SDK, you can use the overloaded `AddNode(string nodeTypeId)` method to create a node by its type ID instead. +::: + +## By JSON + +The easier (but hacky) way to generate blueprints is to use the built-in "Import Blueprint From JSON" feature in the editor. You can create your blueprint in the editor, export it as JSON, and then store it into your plugin. Then, use the `Service` class to import the JSON file as a blueprint. + +```csharp +var fileContents = "..."; // Assume this is the JSON file contents +Context.Service.ImportGraph(fileContents); // Import the blueprint +Context.Service.BroadcastOpenedScene(); // Send the updated scene to the editor +``` + +Then you can access the generated blueprint by name: + +```csharp +var graph = Context.OpenedScene.GetGraphs().Values.First(it => it.Name == "My Awesome Blueprint"); +``` + +As you can see, this is rather hacky and not recommended for production code. The JSON format may change in future versions, and it is not as flexible as generating blueprints by code. + + diff --git a/docs/scripting/api/io.md b/docs/scripting/api/io.md new file mode 100644 index 0000000..542227e --- /dev/null +++ b/docs/scripting/api/io.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 110 +--- + +# Saving and Loading Data {#io} + +## Using Data Inputs + +[Data inputs](ports-and-triggers#data-inputs) in entities are automatically serialized. That means if you need to store any data in your custom asset or node, the best way is to simply define a data input: + +```csharp +[DataInput] +public Vector3 MySpecialVector3; +``` + +You can also have hidden data inputs if you don't want to show them to the user: + +```csharp +[DataInput] +[Hidden] +public Vector3 MySpecialVector3; +``` + +For more complex states, you can serialize them into a JSON string and deserialize when needed: + +```csharp +[DataInput] +[Hidden] +public string MyJSONState; + +// Use Newtonsoft.Json to serialize and deserialize +MyJSONState = JsonConvert.SerializeObject(mySerializableObject); +mySerializableObject = JsonConvert.DeserializeObject(MyJSONState); +``` + +## Using Persistent Data Manager + +For non-serializable data, you can use `Context.PersistentDataManager` to access a file system API restricted to the data folder. For example, to load and save an image from a custom directory: + +```csharp +var bytes = await Context.PersistentDataManager.ReadFileBytesAsync("MyPlugin/MyProfileImage.png"); +await Context.PersistentDataManager.WriteFileBytesAsync("MyPlugin/MyProfileImage.png", bytes); +``` + +## Using Plugin Mod + +A [plugin mod](../plugin-mod) can store text assets in the mod folder, which can be accessed by the plugin at runtime. For example, you can store a JSON file `Animations.json` in the mod folder and load it in the plugin: + +```csharp +protected override void OnCreate() { + base.OnCreate(); + var json = ModHost.Assets.Load("Assets/MyModFolder/Animations.json"); // Change the path to match your mod folder structure +} +``` + + diff --git a/docs/scripting/api/localization.md b/docs/scripting/api/localization.md new file mode 100644 index 0000000..3750d90 --- /dev/null +++ b/docs/scripting/api/localization.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 999 +--- + +# Localization + +Warudo comes with a built-in localization system that allows you to create multilingual plugins. + +Currently, Warudo officially supports the following languages: + +| Language | Code | +|--------------------|---------| +| English | `en` | +| Simplified Chinese | `zh_CN` | +| Japanese | `ja` | + +## Localizing Strings + +All built-in localized strings are stored in the `Localizations` directory in the Warudo data folder. If you open any of the JSON files, you will see a list of key-value pairs, where the key is the string ID and the value is the localized string. + +To get a localized string, simply call the extension method `Localized()` on a string ID: + +``` +using Warudo.Core.Localization; // Import the namespace that contains the extension method + +// Assume the user language is set to English +"FACE_TRACKING".Localized() // "Face Tracking" +"ALIVE_TIME_DESCRIPTION".Localized() // "The prop will be destroyed after this time." +``` + +Note if the string is not yet localized in the current language, the string will fall back to English; if the string is not found in English, the string ID will be returned. + +:::tip +Unity's `HumanBodyBones` enum can also be localized using the `Localized()` extension method. For example, `HumanBodyBones.Head.Localized()` will return "Head" in English and "头" in Simplified Chinese. +::: + +## Adding Localizations + +If you are creating a [plugin mod](../plugin-mod), the recommended way to add localizations is to create a directory called `Localizations` in your mod folder. Inside this directory, create a JSON file for each language you want to support. The JSON file should contain the localized strings in the following format: + +```json +{ + "en": { + "MY_STRING": "My String" + }, + "zh_CN": { + "MY_STRING": "我的字符串" + }, + "ja": { + "MY_STRING": "私の文字列" + } +} +``` + +:::info +You can also create one JSON file for each language. +::: + +When the plugin is loaded, Warudo will automatically load the localized strings from the JSON files. + +If you are not creating a plugin mod, you can also add localized strings directly using `Context.LocalizationManager`: + +```csharp +var localizationManager = Context.LocalizationManager; +localizationManager.SetLocalizedString("MY_STRING", "en", "My String"); +localizationManager.SetLocalizedString("MY_STRING", "zh_CN", "我的字符串"); +localizationManager.SetLocalizedString("MY_STRING", "ja", "私の文字列"); +``` + + diff --git a/docs/scripting/api/mixins.md b/docs/scripting/api/mixins.md new file mode 100644 index 0000000..fc28c72 --- /dev/null +++ b/docs/scripting/api/mixins.md @@ -0,0 +1,109 @@ +--- +sidebar_position: 70 +--- + +# Mixins + +Mixins are a way to reuse code between different entities when inheritance is not an option. They can be thought of a collection of data inputs and triggers with optional, reusable logic. + +## Type Definition + +You can create mixin types by inheriting from the `Mixin` type, like below: + +```csharp +public class PositionMixin : Mixin { + [DataInput] + public Vector3 Position; + + [Trigger] + public void GeneratePosition() { + SetDataInput(nameof(Position), new Vector3(Random.value, Random.value, Random.value), broadcast: true); + } +} +``` + +Then you can now include this mixin in any entity subtype. For example, to include the `PositionMixin` in a custom asset: + +```csharp +[AssetType(...)] +public class PositionAsset : Asset { + + [Mixin] + public PositionMixin PositionMixin; + +} +``` + +Same as with assets, you can include mixins in nodes: + +```csharp +[NodeType(...)] +public class PositionNode : Node { + + [Mixin] + public PositionMixin PositionMixin; + +} +``` + +Remember to add the `[Mixin]` attribute. Also, you do not need to assign a value as it will be automatically instantiated when the entity is created. That means you can access the mixin's data inputs and triggers directly: + +```csharp +// Within entity +public override void OnCreate() { + base.OnCreate(); + var initialPosition = PositionMixin.Position; + PositionMixin.GeneratePosition(); +} +``` + +However, methods such as `SetDataInput` and `InvokeTrigger` are not available in the mixin itself. The data inputs and triggers still technically belong to the entity that includes the mixin, so you should write: + +```csharp +// Within entity +SetDataInput(nameof(PositionMixin.Position), new Vector3(1, 2, 3), broadcast: true); +InvokeTrigger(nameof(PositionMixin.GeneratePosition)); +``` + +To access the entity and its methods within the mixin, use the `Owner` property: + +```csharp +// Within mixin +Position = new Vector3(9, 8, 7); +Owner.BroadcastDataInput(nameof(Position)); +Owner.Watch(nameof(Position), OnPositionChanged); +``` + +## Differences from Structured Data + +Mixins are similar to [structured data](structured-data) in that they can be included in entities and have data inputs and triggers. However, they work in very different ways: + +- A structured data is embedded and serialized in a data input; a mixin "flattens" its data inputs and triggers into the entity that includes it. +- A structured data is "lighter" in the sense that it is designed to encapsulate data, not logic, so it doesn't have as much [lifecycle events](#behavioral-mixins) as a mixin. +- You can have a structured data array in an entity, but you can only have one mixin of a specific type in an entity. + +If you are not sure which to use, we recommend to start with structured data first! + +## Behavioral Mixins {#behavioral-mixins} + +If you need entity lifecycle events in your mixin, you can inherit from `BehavioralMixin` instead of `Mixin`. This will allow you to override the following methods: `OnUpdate()`, `OnPreUpdate()`, `OnPostUpdate()`, `OnLateUpdate()`, `OnFixedUpdate()`, and `OnEndOfFrame()`. Refer to [Entities](entities#lifecycle) page for more information on entity lifecycle. + +## Built-in Mixins + +Warudo provides several built-in mixins that you can use in your entities: + +- `Attachable`: Adds the ability to attach the entity's GameObject to another asset transform. +- `PlaybackMixin`: Adds playback controls to the editor; used in the music player, the MMD player, and the motion player. +- `ToolbarItemMixin`: Adds a toolbar icon. This mixin is used in plugins to display an icon in the toolbar. Refer to the [Plugins](plugins) page for more information. + +:::warning +Currently, built-in mixin types are not well-documented. We are working on improving this, but in the meantime, we recommend referring to their inheritors for usage examples. +::: + + diff --git a/docs/scripting/api/nodes.md b/docs/scripting/api/nodes.md new file mode 100644 index 0000000..842345d --- /dev/null +++ b/docs/scripting/api/nodes.md @@ -0,0 +1,169 @@ +--- +sidebar_position: 30 +--- + +# Nodes + +Nodes are units of logic that can be connected together to create complex behaviors. They can be thought as functions in a program but with a visual representation. + +## Type Definition + +You can create node types that can be instantiated and stored in a blueprint. A node type inherits from the `Node` type and is decorated with the `[NodeType]` attribute, like below: + +```csharp +[NodeType( + Id = "c76b2fef-a7e7-4299-b942-e0b6dec52660", + Title = "Hello World", + Category = "CATEGORY_DEBUG", + Width = 1f +)] +public class HelloWorldNode : Node { + // Node implementation +} +``` + +Here's a summary of the parameters: + +- **`Id`**: A unique identifier for the node type; you should [generate a new GUID](https://www.guidgenerator.com/online-guid-generator.aspx) for each new node type. Note that this is different from the node instance's UUID (`node.Id`). +- **`Title`**: The name of the node type that will be displayed in the node palette. +- **`Category`**: Optional. The group of the node in the node palette. +- **`Width`**: Optional. The width of the node, which is by default `1f`. A node with a width of `2f` will take up twice the space of a node with a width of `1f`. + +:::info +Here are some common node categories you can use: `CATEGORY_ARITHMETIC`, `CATEGORY_ASSETS`, `CATEGORY_BLENDSHAPES`, `CATEGORY_CHARACTERS`, `CATEGORY_DATA`, `CATEGORY_DEBUG`, `CATEGORY_EVENTS`, `CATEGORY_FLOW`, `CATEGORY_INPUT`, `CATEGORY_MOTION_CAPTURE`, `CATEGORY_INTERACTIONS`. + +You can also use your own category name, but the above categories are automatically localized to the user's language. +::: + +## Components + +A node type can define data inputs, data outputs, flow inputs, flow outputs, and triggers. + +![](/doc-img/en-custom-node-1.png) + +## Lifecycle + +In addition to the lifecycle stages listed on the [Entities](entities#lifecycle) page, nodes have the following additional lifecycle stages: + +- **`OnAllNodesDeserialized()`:** Called after all nodes in the belonging blueprint are deserialized. This is useful when you need to access other nodes in the same blueprint. +- **`OnUserAddToScene()`:** Called when the node is just instantiated in the blueprint editor, by the user dragging it from the node palette. + +## Triggering Flows + +You can trigger an output flow by returning the `Continuation` of the flow output in a flow input method. For example: + +```csharp +[FlowInput] +public Continuation Enter() { + // Do something + return Exit; +} + +[FlowOutput] +public Continuation Exit; +``` + +If your flow ends here, you can return `null` to indicate that the flow has ended. + +```csharp +[FlowInput] +public Continuation Enter() { + // Do something + return null; +} +``` + +Sometimes, you may want to delay the flow output or trigger a flow output manually. You can use the `InvokeFlow(string flowOutputPortKey)` method to trigger a flow output. For example: + +```csharp +[FlowInput] +public Continuation Enter() { + async void TriggerExitLater() { + await UniTask.Delay(TimeSpan.FromSeconds(5)); + InvokeFlow(nameof(Exit)); // Start a new flow from the "Exit" output port + } + return null; // You still need to return a Continuation. Since *this* flow technically ends here, we return null +} +``` + +## Non-Serializable Data Inputs & Outputs + +One thing special about nodes is that they can have non-serializable data inputs and outputs (you can't do that in assets or plugins - they will just not display at all). This allows nodes to pass, e.g., Unity objects like `GameObject` or `Texture2D` between each other and process them. + +You can even have a generic data input or output, like this: + +```csharp +[DataInput] +public object MyGenericInput; // Note it's 'object', not 'Object' + +[DataOutput +public object MyGenericOutput() => ... +``` + +Data output port of any type can be connected to `MyGenericInput` (the value will just be upcast to `object`). Conversely, `MyGenericOutput` can be connected to any data input port, but the value received by the input port will be `null` if the input port type is not compatible with the underlying type of what `MyGenericOutput` returns. + +## Type Converters + +When connecting nodes, you will notice Warudo will automatically convert the data type if possible. For example, it is possible to connect a `float` output to an `int` input - the value is simply truncated. + +You can register custom type converters using the `DataConverters` class. The easiest way is to implement the `DataConverter` class: + +```csharp +public class MyFloatToIntConverter : DataConverter { + public override int Convert(float data) => (int) data; +} +``` + +Then, register the converter in the `OnCreate` method of your [plugin](plugins): + +```csharp +public override void OnCreate() { + base.OnCreate(); + DataConverters.RegisterConverter(new MyFloatToIntConverter()); +} +``` + +:::caution +You should not register type converters in your node type's `OnCreate` - you will register them each time a node instance is created! +::: + +## Code Examples + +### Basic + +- [ExampleNode.cs](https://github.com/HakuyaLabs/WarudoPlaygroundExamples/blob/master/ExampleNode.cs) +Standard example of node, showing the basic format of various node components. + +- [StructuredDataExampleNode.cs](https://gist.github.com/TigerHix/81cfa66a8f810165c426d1b5157677b5) +Creating "inner" data types. (StructuredData) + +- [GetRandomSoundNode.cs](https://gist.github.com/TigerHix/f0f1a7e3c53ca65450fdca1ff06eb343) +Get Random Sound node. + +- [LiteralCharacterAnimationSourceNode.cs](https://gist.github.com/TigerHix/2dc58213defe400ddb280a8cc1e6334b) +Character Animation (source) node. + +- [SmoothTransformNode.cs](https://gist.github.com/TigerHix/eaf8e05e5e1b687b8265420b9943903d) +Smooth Transform node. + +### Advanced + +- [FindAssetByTypeNode.cs](https://gist.github.com/TigerHix/ab3522bb25669457cc583abc4fb025d2) +AutoCompleteList (Dropdown) example. + +- [MultiGateNode.cs](https://gist.github.com/TigerHix/8747793a68f0aa15a469f9823812e221) +Dynamic data / flow ports example. + +- [ThrowPropAtCharacterNode.cs](https://gist.github.com/TigerHix/18e9f20152c0cfac38fd5528c7af16b6) +Throw Prop At Character. + +- [SpawnStickerNode.cs](https://gist.github.com/TigerHix/fe35442e9052cd8c4ea80e0261349321) +Spawn Local / Online Image Sticker. + + diff --git a/docs/scripting/api/overview.md b/docs/scripting/api/overview.md new file mode 100644 index 0000000..149fac2 --- /dev/null +++ b/docs/scripting/api/overview.md @@ -0,0 +1,28 @@ +--- +sidebar_position: 0 +--- + +# Overview + +:::tip +This section deep dives into Warudo's scripting APIs. You don't need to be familiar with every detail to start scripting in Warudo, but we recommend at the very least skimming through this section to get a sense of how things work! +::: + +Warudo is two applications in one: the editor and the runtime (we called it the _main window_ in the non-programmer sections of this handbook). The editor is where you create and edit scenes: update asset and plugin settings, add and connect nodes in blueprints, etc. The runtime acts as a rendering/scripting engine and runs the scenes you create. + +![](/doc-img/en-scripting-concepts-1.png) +

The editor (right) and the runtime (left).

+ +The editor and runtime communicate with each other via a WebSocket connection. When you make changes in the editor, the changes are sent to the runtime, which then updates the scene accordingly. This architecture allows you to see the changes you make in real-time. Conversely, when you make changes in the runtime (e.g., changing a node's data input programmatically), the changes need to be sent back to the editor (see [Accessing Data Inputs](ports-and-triggers#accessing-data-inputs)). + +The Warudo runtime is built using [Unity 2021.3](https://unity.com/); however, the core framework is designed to be as engine-agnostic as possible, which allows nodes, assets, and plugins to interact with each other and with the user through a set of simple and clean APIs, as well as enabling features such as [hot-reloading](../playground). + +Instead of writing C# classes that derive from `MonoBehaviour` in typical Unity projects, you will write classes that derive from Warudo's base classes such as `Node` and `Asset`. These classes are then instantiated and managed by Warudo's runtime. Then, inside these classes, you can call Unity APIs to interact with the Unity scene, such as creating GameObjects, adding components, etc. + + diff --git a/docs/scripting/api/plugins.md b/docs/scripting/api/plugins.md new file mode 100644 index 0000000..229adff --- /dev/null +++ b/docs/scripting/api/plugins.md @@ -0,0 +1,157 @@ +--- +sidebar_position: 50 +--- + +# Plugins + +Plugins are the parents of all custom scripts in Warudo. They are also used to perform tasks that are independent of the scene, such as registering [resource providers & resolvers](resource-providers-and-resolvers), authenticating with an external service, or allowing the user to configure the global settings of the plugin's assets and nodes. + +:::tip +You can use [Playground](../playground) to load custom assets and nodes without creating a plugin; those will be automatically registered and managed by Warudo's "Core" plugin. However, to distribute your custom assets and nodes to other users, you must create a [plugin mod](../plugin-mod) in which your own plugin will register your custom assets and nodes. +::: + +:::info +This page discusses the scripting APIs for creating plugins. Please also refer to the [Plugin Mod](../plugin-mod) page. +::: + +## Type Definition + +You can create a plugin type that can be loaded and executed in Warudo. A plugin type inherits from the `Plugin` type and is decorated with the `[PluginType]` attribute, like below: + +```csharp +[PluginType( + Id = "hakuyatira.helloworld", + Name = "Hello World", + Description = "A simple plugin that says hello to the world.", + Version = "1.0.0", + Author = "Hakuya Tira", + Icon = null, + SupportUrl = "https://docs.warudo.app", + AssetTypes = new [] { typeof(HelloWorldAsset) }, + NodeTypes = new [] { typeof(HelloWorldNode) } +)] +public class HelloWorldPlugin : Plugin { + // Plugin implementation +} +``` + +Here's a summary of the parameters: + +- **`Id`**: A unique identifier for the plugin type; we recommend using the reverse domain name notation (e.g., `com.example.pluginname`). Since the plugin type is a singleton, its `Id` property (`plugin.Id`) is identical to the plugin type's ID (`plugin.PluginType.Id`). +- **`Name`**: The name of the plugin. +- **`Description`**: A brief description of the plugin. +- **`Version`**: The version of the plugin. We recommend following the [Semantic Versioning](https://semver.org/) standard. +- **`Author`**: The author of the plugin. +- **`Icon`**: Optional. The SVG icon of the plugin. It is recommended to define a `const string` field to store this value. +- **`SupportUrl`**: Optional. The URL to the support page of the plugin. +- **`AssetTypes`**: An array of asset types in this plugin. +- **`NodeTypes`**: An array of node types in this plugin. + +:::tip +`icon` should be a single SVG element (e.g., `...`). For example: +```html + + ... + +``` +::: + +## Lifecycle + +In addition to the lifecycle stages listed on the [Entities](entities#lifecycle) page, plugins have the following additional lifecycle stages: + +- **`OneTimeSetup()`:** Called once when the plugin is loaded by Warudo the first time, determined by the existence of the plugin's data file in `Plugins/Data`. +- **`OnSceneLoaded(Scene scene, SerializedScene serializedScene)`:** Called when a scene is loaded, after all assets and nodes are deserialized. +- **`OnSceneUnloaded(Scene scene)`:** Called when a scene is unloaded. + +## Components + +A plugin type can define data inputs and triggers. + +![](/doc-img/en-custom-plugin-1.png) + +## Loading Unity Assets {#loading-unity-assets} + +As shown in the [Creating Your First Plugin Mod](../creating-your-first-plugin-mod) tutorial, you can load Unity assets in your plugin's mod folder by using `ModHost`. For example, to load a prefab: + +```csharp +var prefab = ModHost.LoadAsset("Assets/MyModFolder/MyPrefab.prefab"); +``` + +To load a text asset (e.g., JSON file): + +```csharp +var text = ModHost.LoadAsset("Assets/MyModFolder/MyText.json"); +``` + +:::tip +Warudo uses [uMod 2.0](https://trivialinteractive.co.uk/products.html) as the underlying modding framework. You can refer to the [ModHost documentation](https://trivialinteractive.co.uk/products/documentation/umod_20/scriptingreference/html/T_UMod_ModHost.htm) for more information on loading assets. +::: + +## Toolbar Icon + +You may notice some built-in plugins display an icon in the toolbar. Furthermore, the icon is clickable and can be used to open the plugin's settings. + +![](/doc-img/en-custom-plugin-2.png) + +To achieve this, you can include the `ToolbarItemMixin` [mixin](mixins) in your plugin class: + +```csharp +[Mixin] +public ToolbarItemMixin ToolbarItem; + +private bool serverConnected; // Assume this is a field that indicates whether the plugin is connected to an external server + +public override void OnCreate() { + base.OnCreate(); + ToolbarItem.SetIcon(ToolbarIcon); // ToolbarIcon is a const string that contains a SVG icon + ToolbarItem.OnTrigger = () => Context.Service.NavigateToPlugin(Type.Id); // Open the plugin settings when the icon is clicked +} + +public override void OnUpdate() { + base.OnUpdate(); + ToolbarItem.SetEnabled(serverConnected); // Only show the icon if connected to the server + ToolbarItem.SetTooltip("Connected! Current time: " + DateTime.Now); // Set a tooltip that is displayed when the user hovers over the icon +} +``` + +## Accessing Owner Plugin from Nodes and Assets + +You can access the owner plugin from nodes and assets by using the `Plugin` property. For example: + +```csharp +public override void OnCreate() { + base.OnCreate(); + var myPlugin = (HelloWorldPlugin) Plugin; + // Do something with myPlugin +} +``` + +## Plugin vs. Node + +Custom scripting in Warudo works best when it complements, not replaces, the [blueprint system](../../blueprints/overview.md). If you find yourself writing a huge `Plugin` class that handles a lot of logic, think carefully if it can be written as a custom node instead. For example, if your idea is to play a fireworks effect when you receive a custom UDP packet over the LAN, instead of a `Plugin` class that does both, it is much better (and easier to maintain) to implement two custom nodes - one that triggers the output flow when the custom UDP packet is received, and one that straightly plays a firework effect when the input flow is received. This also gives your users more flexibility in using your custom nodes in their blueprints. + +## Code Examples + +### Basic + +- [Example plugin](https://gist.github.com/TigerHix/b78aabffc2d03346ff3da526706ce2ca) +The template of a plugin (single file) with the basic use of `Plugin` class. + +- [WarudoPluginExamples](https://github.com/HakuyaLabs/WarudoPluginExamples) +The complete plugin examples (multiple files), including the **Stream Deck Plugin** and **VMC Plugin**. + - **Stream Deck Plugin**: This plugin communicates with an external application (Warudo's [Stream Deck plugin](https://apps.elgato.com/plugins/warudo.streamdeck)) via WebSocket. It demonstrates the use of the WebSocket service base class `WebSocketService`. + - **VMC Plugin**: This plugin adds [VMC](https://protocol.vmc.info/english) as a supported motion capture method via registering a `FaceTrackingTemplate` and a `PoseTrackingTemplate`. + +### Advanced + +- [KatanaAnimations.cs](https://gist.github.com/TigerHix/2cb8052b0e8aeeb7f9cb796dc7edc6a3) +Custom plugin to load AnimationClips from the mod folder and registers them as character animations. + + diff --git a/docs/scripting/api/ports-and-triggers.md b/docs/scripting/api/ports-and-triggers.md new file mode 100644 index 0000000..c2093ca --- /dev/null +++ b/docs/scripting/api/ports-and-triggers.md @@ -0,0 +1,449 @@ +--- +sidebar_position: 2 +--- + +# Ports & Triggers + +Ports and triggers belong to [entities](entities) and are arguably the most important part of Warudo scripting. They are used to pass data between entities, trigger actions, and provide user interaction in the editor. + +![](/doc-img/en-custom-node-1.png) + +![](/doc-img/en-scripting-concepts-4.png) + +## Data Input Ports {#data-input-ports} + +Data input ports are used to provide data to an entity by either the user (using the editor) or another entity. Data inputs can be of various types, such as strings, numbers, booleans, or even complex types like [structured data](structured-data.md) or arrays. + +A data input is defined as a public field in an entity subclass, decorated with the `[DataInput]` attribute. In the [Getting Started](getting-started.md) example, we saw a `DataInput` that defines a number slider: + +```csharp +[DataInput] +[IntegerSlider(1, 100)] +public int LuckyNumber = 42; +``` + +Here are a few more examples: + +- **String Input:** + ```csharp + [DataInput] + public string MyName = "Alice"; + ``` +- **Enum Input:** (Shown as a dropdown in the editor) + ```csharp + public enum Color { + [Label("RED!!!")] + Red, + [Label("GREEN!!!")] + Green, + [Label("BLUE!!!")] + Blue + } + + [DataInput] + public Color MyColor = Color.Red; + ``` + Note you can add labels to enum values using the `[Label(string label)]` attribute. +- **Array Input:** (Shown as an editable list in the editor) + ```csharp + [DataInput] + public float[] MyFavoriteNumbers = new float[] { 3.14f, 2.718f }; + ``` + +This is how they look in the editor (we use a node here, but these data inputs work the same way in assets and plugins): + +
+
+ +
+
+ +Note how they are initialized with the default values we specified in the code. These default values will be assigned to the data input again when the user clicks the "Reset" button next to the data input label. + +A data input typically has a serializable type ("serializable" here means that the data value can be saved and restored when Warudo is closed and reopened). The following types are serializable by default: + +- Primitive types: `int`, `float`, `bool`, `string`, any Enum type +- Unity types: `Vector2`, `Vector3`, `Vector4`, `Color` +- [Structured data](structured-data.md) types +- [Asset references](#asset-references) +- Arrays of serializable types + +For nodes, it is possible to define non-serializable data inputs that cannot be edited in the editor but instead processed by the node itself. For example, the following code calls the `ToString()` method on the generic `object` data input: + +```csharp +[NodeType(Id = "dc28819e-5149-4573-945e-40e81e2874c4", Title = "ToString()", Category = "CATEGORY_ARITHMETIC")] +public class ToStringNode : Node { + + [DataInput] + public object A; // Not serialized + + [DataOutput] + [Label("OUTPUT_STRING")] + public string Result() { + return A?.ToString(); + } + +} +``` + +Other common data input types that are not serializable but can be passed between nodes include `Dictionary`, `GameObject` and `Quaternion`. + +### Attributes {#data-input-attributes} + +Data input ports can be annotated with attributes to provide additional context to the editor. For example, the `[IntegerSlider]` attribute we saw earlier specifies that the data input should be displayed as a slider with a range of 1 to 100. Here are the supported attributes: + +- `[Label(string label)]`: Specifies the label of the data input. +- `[HideLabel]`: Specifies that the label of the data input should be hidden. +- `[Description(string description)]`: Specifies the description of the data input. +- `[HiddenIf(string methodName)]`: Specifies that the data input should be hidden if the specified method returns `true`. The method must be a `public` or `protected` method in the entity class that returns a `bool`. + ```csharp + [DataInput] + public int MyNumber = 0; + + [DataInput] + [HiddenIf(nameof(IsSecretDataInputHidden))] + public string SecretDataInput = "I am hidden unless MyNumber is 42!"; + + public bool IsSecretDataInputHidden() => MyNumber != 42; + ``` +- `[HiddenIf(string dataInputPortName, If @if, object value)]`: Specifies that the data input should be hidden if the specified data input port satisfies the `@if` condition. The value must be a constant. + ```csharp + [DataInput] + public int MyNumber = 0; + + [DataInput] + [HiddenIf(nameof(MyNumber), If.NotEqual, 42)] + public string SecretDataInput = "I am hidden unless MyNumber is 42!"; + ``` +- `[HiddenIf(string dataInputPortName, Is @is)]`: Specifies that the data input should be hidden if the specified data input port satisfies the `@is` condition. + ```csharp + [DataInput] + public CharacterAsset MyCharacter; + + [DataInput] + [HiddenIf(nameof(MyCharacter), Is.NullOrInactive)] + public string SecretDataInput = "I am hidden unless MyCharacter is selected and active!"; + ``` +- `[DisabledIf(...)]`: Similar to `[HiddenIf(...)]`, but the data input is disabled instead of hidden. +- `[Hidden]`: Specifies that the data input should be always hidden. This is useful when you want to use a data input in code but not expose it to the user. +- `[Disabled]`: Specifies that the data input should be always disabled (non-editable). This is useful for array data inputs that have a constant length, or for visualizing data inputs that are always programmatically set, etc. +- `[Section(string title)]`: Specifies that the data input and all subsequent data inputs should be displayed in a new section with the specified title, unless another section is specified. +- `[SectionHiddenIf(string methodName)]`: Requires attribute `[Section]`. Specifies that the section should be hidden if the specified method returns `true`. The method must be a `public` or `protected` method in the entity class that returns a `bool`. The `@if` and `@is` conditions are also supported. +- `[Markdown(bool primary = false)]`: Specifies that the data input should be displayed as a Markdown text and cannot be edited. The data input must be of type `string`. If `primary` is `true`, the text will be displayed in a larger font size without a color background. + +:::info +`[HiddenIf]` and `[DisabledIf]` attributes are evaluated every frame when the asset or node is visible in the editor. Therefore, you should avoid using expensive operations in these methods. +::: + +Some attributes are specific to certain data input types: +- `[IntegerSlider(int min, int max, int step = 1)]`: Requires data type `int` or `int[]`. Specifies that the data input should be displayed as an integer slider with the specified range. +- `[FloatSlider(float min, float max, float step = 0.01f)]`: Requires data type `float` or `float[]`. Specifies that the data input should be displayed as a float slider with the specified range. +- `[AutoCompleteResource(string resourceType, string defaultLabel = null)]`: Requires data type `string`. Specifies that the data input should be displayed as an auto-complete list of resources of the specified type. For example, the "Character → Default Idle Animation" data input is defined as `[AutoCompleteResource("CharacterAnimation")]`. Please refer to the [Resource Providers & Resolvers](resource-providers-and-resolvers.md) page for more information. +- `[AutoCompleteList(string methodName, bool forceSelection = false, string defaultLabel = null)]`: Requires data type `string`. Specifies that the data input should be displayed as a dropdown menu generated by the specified method. The method must be a `public` or `protected` method in the entity class that returns a `UniTask`. Note that the method can be asynchronous. If `forceSelection` is `true`, the user can only select a value from the dropdown list, or the value is assigned `null`. + ```csharp + [DataInput] + [AutoComplete(nameof(AutoCompleteVipName), forceSelection: true)] + public string VipName = "Alice"; + + protected async UniTask AutoCompleteVipName() { + return AutoCompleteList.Single(vipNames.Select(name => new AutoCompleteEntry { + label = name, // This is what the user sees + value = name // This is what the field stores + }).ToList()); + } + + private List vipNames; // Entity-controlled runtime data + + // Some other code should update the vipNames list + ``` + :::tip + Autocomplete lists are useful when you want to provide a list of options that are not known at compile time. For example, you can use an autocomplete list to provide a list of files from a directory, a list of emotes from the remote server, etc. They are used very frequently in Warudo's internal nodes and assets! + ::: +- `[MultilineInput]`: Requires data type `string`. Specifies that the data input should be displayed as a multiline text input field. +- `[CardSelect]`: Requires an enum data type. Specifies that the data input should be displayed as a card selection list (similar to "Camera → Control Mode"). + ```csharp + public enum Color { + [Label("#00FF00")] + [Description("I am so hot!")] + Red, + [Label("#00FF00")] + [Description("I am so natural!")] + Green, + [Label("#0000FF")] + [Description("I am so cool!")] + Blue + } + + [DataInput] + [CardSelect] + public Color MyColor = Color.Red; + ``` + +### Enums + +You can customize the editor labels of enum values by using the `[Label(string label)]` attribute in your enum type. For example: + +```csharp +public enum Color { + [Label("#FF0000")] + Red, + [Label("#00FF00")] + Green, + [Label("#0000FF")] + Blue +} +``` + +As mentioned in the [Attributes](#data-input-attributes) section, you can also use the `[Description(string description)]` and `[Icon(string icon)]` attributes to show the enum input as a list of cards. + +:::tip +`icon` should be a single SVG element (e.g., `...`). For example: +```html + + ... + +``` +::: + +### Asset References {#asset-references} + +:::warning +Asset references are only available in assets, nodes, and non-plugin structured data. +::: + +A data input can be used to reference another asset in the current scene. For example, the following code defines a data input that references a `CharacterAsset`: + +```csharp +[DataInput] +public CharacterAsset MyCharacter; +``` + +In the editor, the user can select a character asset from the dropdown list: + +
+
+ +
+
+ +You can then access ports and triggers of the referenced asset: + +```csharp +[FlowInput] +public Continuation Enter() { + if (MyCharacter.IsNonNullAndActive()) { + MyCharacter.EnterExpression("Joy", transient: true); // Make the character smile! + } + return Exit; +} +``` + +:::tip +You can check if the asset is `null` or inactive by using `asset.IsNullOrInactive()`, and vice versa, `asset.IsNonNullAndActive()`. +::: + +What if you want to filter the list of assets shown in the dropdown? You can use the `[AssetFilter(string methodName)]` attribute to specify a method that is used to filter the assets in the scene. The method must be a `public` or `protected` method that receives a parameter of the asset type and returns a `bool`. For example: + +```csharp +[DataInput] +[AssetFilter(nameof(FilterCharacterAsset))] +public CharacterAsset MyCharacter; + +protected bool FilterCharacterAsset(CharacterAsset character) { + return character.Active; // Only show active characters +} +``` + +### Accessing Data Inputs Programmatically {#accessing-data-inputs} + +Let's say you have an entity. There are two ways to read its data inputs: + +1. Access the data input field directly. For example, if you have a node with a _public_ data input field `public int MyNumber = 42;`, you can read the value of `MyNumber` directly by using `node.MyNumber`. +2. Use the `T GetDataInput(string key)` or `object GetDataInput(string key)` method. This method is available in all entities and returns the value of the data input with the specified name. For example, if you have a node with a data input named `MyNumber`, you can read the value of `MyNumber` by using `node.GetDataInput("MyNumber")` (or `node.GetDataInput(nameof(node.MyNumber))` which is stylistically preferred). + +:::tip +The key of a port is always the name of the field, unless the port is dynamically added (see [Dynamic Ports](#dynamic-ports)) +::: + +The second method is useful when you need to access data inputs dynamically, for example, when you need to access a data input based on a string variable. (Also see [Dynamic Ports](#dynamic-ports).) Otherwise, the two methods do not have practical differences. + +Similarly, to write to a data input, you can either assign a value to the data input field directly or use the `void SetDataInput(string key, T value)` method. For example, to set the value of a data input named `MyNumber`, you can use `node.MyNumber = 42` or `node.SetDataInput("MyNumber", 42, broadcast: true)` (or `node.SetDataInput(nameof(node.MyNumber), 42, broadcast: true)` which is stylistically preferred). + +:::tip +Another alternative to `SetDataInput(nameof(MyNumber), 42, broadcast: true)` is to write: +```csharp +MyNumber = 42; +BroadcastDataInput(nameof(MyNumber)); +``` +::: + +However, in this case, the second method is **strongly recommended** due to two reasons: + +1. It ensures [watchers](operations#watchers) of this data input are notified of the change. +2. By setting the `broadcast` parameter to `true`, the change is sent to the editor. Otherwise, you need to use `BroadcastDataInput(string key)` to manually send the change to the editor. + +You should use the first method only if: + +1. You are updating the data input extremely frequently, and you do not need to send every change to the editor, i.e., you will call `BroadcastDataInput(string key)` sporadically. This saves performance. +2. You explicitly do not want to notify watchers of this data input. This is rare. + +## Data Output Ports + +Data output ports are node-specific and are used to provide data to other nodes. A data output is defined as a public method in a node subclass, decorated with the `[DataOutput]` attribute. The method can return any non-void type. Here is an example: + +```csharp +[DataOutput] +public int RandomNumber() { + return Random.Range(1, 100); // Return a random number between 1 and 100 +} +``` + +Data outputs support a subset of [data input attributes](#data-input-attributes): `[Label]`, `[HideLabel]`, `[Description]`, `[HiddenIf]`, and `[DisabledIf]`. + +## Flow Input Ports {#flow-inputs} + +Flow input ports are node-specific and are used to receive flow signals from other nodes to trigger certain actions. A flow input is defined as a public method in a node subclass, decorated with the `[FlowInput]` attribute. The method must return a flow output `Continuation`. Here is an example: + +```csharp +[DataInput] +public bool FlowToA = true; + +[FlowInput] +public Continuation Enter() { + return FlowToA ? ExitA : ExitB; // If FlowToA is true, trigger ExitA; otherwise, trigger ExitB +} + +[FlowOutput] +public Continuation ExitA; + +[FlowOutput] +public Continuation ExitB; +``` + +Flow inputs support a subset of [data input attributes](#data-input-attributes): `[Label]`, `[HideLabel]`, and `[Description]`. Note that if the method is named `Enter()` and without a `[Label]` attribute, the label will be automatically set to the word "Enter" localized in the editor's language. + +## Flow Output Ports + +Flow output ports are node-specific and are used to send flow signals to other nodes. A flow output is defined as a public field in a node subclass, decorated with the `[FlowOutput]` attribute. The field must be of type `Continuation`. See [Flow Inputs](#flow-inputs) for an example. + +Flow outputs support a subset of [data input attributes](#data-input-attributes): `[Label]`, `[HideLabel]`, and `[Description`]. Note that if the field is named `Exit` and without a `[Label]` attribute, the label will be automatically set to the word "Exit" localized in the editor's language. + +## Triggers + +Triggers are, simply put, buttons that can be clicked in the editor to trigger certain actions. A trigger is defined as a public method in an entity subclass, decorated with the `[Trigger]` attribute. Here is an example: + +```csharp +[Trigger] +public void ShowPopupMessage() { + Context.Service.PromptMessage("Title of the message", "Content of the message"); +} +``` + +Which is rendered in the editor as: + +![](/doc-img/en-scripting-concepts-5.png) + +When the user clicks the button, the `ShowPopupMessage` method is called. + +Triggers support a subset of [data input attributes](#data-input-attributes): `[Label]`, `[HideLabel]`, `[Description]`, `[HiddenIf]`, `[DisabledIf]`, `[Section]`, and `[SectionHiddenIf]`. + +### Asynchronous Triggers + +Trigger methods can be asynchronous. For example, if you want to show a message after a delay, you can use `UniTask`: + +```csharp +[Trigger] +public async void ShowPopupMessageAfterDelay() { // Note the async keyword + await UniTask.Delay(TimeSpan.FromSeconds(1)); // Wait for 1 second + Context.Service.PromptMessage("Title of the message", "Content of the message"); +} +``` + +A more practical example is to show a confirmation dialog before proceeding: + +```csharp +[Trigger] +public async void ShowConfirmationDialog() { + bool confirmed = await Context.Service.PromptConfirmation("Are you sure?", "Do you want to proceed?"); + if (confirmed) { + // Proceed + } +} +``` + +:::info +A list of available operations in the `Service` class can be found in [Common Operations](operations#service). +::: + +### Invoking Triggers Programmatically + +Similar to [accessing data inputs](#accessing-data-inputs), you can invoke triggers programmatically by calling the method directly or using the `void InvokeTrigger(string key)` method on the entity. For example, to invoke a trigger named `ShowPopupMessage`, you can use `entity.ShowPopupMessage()` or `entity.InvokeTrigger("ShowPopupMessage")`. + +## Port Order + +By default, ports are automatically ordered based on their declaration order in the entity class. However, you can manually specify the order of ports by using the `order` parameter in the `[DataInput]`, `[DataOutput]`, `[FlowInput]`, `[FlowOutput]`, and `[Trigger]` attributes. For example: + +```csharp +[DataInput(order = 1)] +public int MyNumber = 42; + +[DataInput(order = -1)] +public string MyString = "Hello, World!"; // This will be displayed before MyNumber +``` + +## Dynamic Ports + +Sometimes, you want to dynamically add or remove ports based on certain conditions. For example, the built-in **Multi Gate** node has a dynamic number of flow outputs (exits) based on the "Exit Count" data input. + +To achieve this, you can access the underlying port collections at runtime: + +```csharp +FlowOutputPortCollection.GetPorts().Clear(); // Clear all flow output ports +for (var i = 1; i <= ExitCount; i++) { + AddFlowOutputPort("Exit" + i, new FlowOutputProperties { + label = "EXIT".Localized() + " " + i + }); // Create a new flow output port for each exit +} +Broadcast(); // Notify the editor that the ports have changed +``` + +:::tip +You can find the full source code of the **Multi Gate** node [here](https://gist.github.com/TigerHix/8747793a68f0aa15a469f9823812e221). +::: + +:::info +You should not add or remove ports frequently (i.e., on every frame), as it may cause performance issues. +::: + +## Dynamic Port Properties + +You can also dynamically change the properties of a port, such as label, description, or type-specific properties. For example, consider the following: + +```csharp +[DataInput] +[IntegerSlider(1, 10)] +public int CurrentItem = 1; + +private int itemCount = 10; // Assume we have 10 items initially +``` + +The range of the `[IntegerSlider]` is determined at compile time. But if `itemCount` changes, we will want to update the range of the slider. To do this, you can access the port properties directly: + +```csharp +var properties = GetDataInputPort(nameof(CurrentItem)).Properties; +properties.description = $"Select from {itemCount} items."; // Change the description + +var typeProperties = (IntegerDataInputTypeProperties) properties.typeProperties; // Get type-specific properties +typeProperties.min = 1; +typeProperties.max = itemCount; // Change the slider range + +BroadcastDataInputProperties(nameof(CurrentItem)); // Notify the editor that the properties have changed +``` + + diff --git a/docs/scripting/api/resource-providers-and-resolvers.md b/docs/scripting/api/resource-providers-and-resolvers.md new file mode 100644 index 0000000..353dae6 --- /dev/null +++ b/docs/scripting/api/resource-providers-and-resolvers.md @@ -0,0 +1,184 @@ +--- +sidebar_position: 200 +--- + +# Resource Providers & Resolvers + +Resources in Warudo are any external data that can be used by assets and nodes. For example, character resources are the `.vrm` and `.warudo` files in the `Characters` directory; character animation resources are the 500+ built-in animations provided by Warudo plus any custom [character animation mods](../../modding/character-animation-mod) in the `CharacterAnmations` directory; [screen](../../assets/screen) image resources are the image files in the `Images` directory; and so on. + +## Overview + +Internally, each resource is uniquely identified by a **resource URI**, which looks like `character://data/Characters/MyModel.vrm` or `character-animation://resources/Animations/AGIA/01_Idles/AGIA_Idle_generic_01`. + +When you open a resource dropdown such as the `Source` dropdown in the character asset, the dropdown queries **resource providers** to obtain a list of compatible resource URIs; in the case of the character asset, two resource providers will return results: one that look for files in the `Characters` directory, and the other that look for installed character mods installed from the Steam Workshop. + +
+
+![](/doc-img/en-resource-providers-1.png) +
+
+ +When you select a resource URI, Warudo invokes the corresponding **resource URI resolver** to load the resource data. For example, a `.vrm` file and a `.warudo` file can be loaded by two different resolvers, but both resolvers return a `GameObject` (which is the character loaded into the Unity scene), so the character asset can treat them the same way. + +Note that resource providers and resolvers must be registered by a [plugin](plugins). + +## Provider + +Let's go through a toy plugin example to register a custom resource provider that provides prop resources which are just cubes and spheres. + +```csharp +using System; +using System.Collections.Generic; +using Warudo.Core; +using Warudo.Core.Attributes; +using Warudo.Core.Plugins; +using Warudo.Core.Resource; + +[PluginType( + Id = "hakuyatira.primitiveprops", + Name = "Primitive Props", + Description = "A simple plugin that registers primitive props.", + Version = "1.0.0", + Author = "Hakuya Tira", + SupportUrl = "https://docs.warudo.app")] +public class PrimitivePropsPlugin : Plugin { + + protected override void OnCreate() { + base.OnCreate(); + Context.ResourceManager.RegisterProvider(new PrimitivePropResourceProvider(), this); + } + +} + +public class PrimitivePropResourceProvider : IResourceProvider { + public string ResourceProviderName => "Primitives"; // The name of your provider + + public List ProvideResources(string query) { + if (query != "Prop") return null; // If the query is not "Prop", we don't have any compatible resources + return new List { + new Resource { + category = "Primitives", // Category that will be shown in the dropdown + label = "Cube", // Label that will be shown in the dropdown + uri = new Uri("prop://primitives/cube") // Underlying resource URI + }, + new Resource { + category = "Primitives", + label = "Sphere", + uri = new Uri("prop://primitives/sphere") + } + }; + } +} +``` + +In the example above, we register a custom resource provider that provides two prop resources: a cube and a sphere. Note the `ProvideResources` method returns the list of resources only when the query is `"Prop"`; this is because the prop asset queries for prop resources with the query `"Prop"`, like this: + +```csharp +// In the prop asset class +[AutoCompleteResource("Prop")] +public string Source; +``` + +:::tip +A list of queries used by the built-in assets can be found in the [Built-in Resource Types](#built-in-resource-types) section. +::: + +:::tip +We write the URIs like `prop://primitives/cube` because it is a convention to use the `prop://` scheme for prop resources, but you don't necessarily have to follow, especially if you will write a custom resolver for your resource URIs. +::: + +Once the plugin is loaded, when you open the `Source` dropdown in a prop asset, you should be able to see the two resources provided by our custom resource provider: + +![](/doc-img/en-resource-providers-2.png) + +Selecting them will invoke the corresponding resource URI resolver to load the prop data. But no one knows how to resolve our URIs! Let's create a resolver for our URIs. + +## URI Resolver + +Add the following class to our plugin: + +```csharp +public class PrimitivePropResourceUriResolver : IResourceUriResolver { + public object Resolve(Uri uri) { + if (uri.Scheme != "prop" || uri.Authority != "primitives") return null; + var path = uri.LocalPath.TrimStart('/'); + + return path switch { + "cube" => GameObject.CreatePrimitive(PrimitiveType.Cube), + "sphere" => GameObject.CreatePrimitive(PrimitiveType.Sphere), + _ => throw new Exception("Unknown primitive prop: " + path) + }; + } +} +``` + +The first few two lines check if the URI matches our custom format, i.e., `prop://primitives/xxx`. If so, we extract the last part of the URI (`path`) and create a cube or a sphere accordingly. We return a `GameObject` directly, because it is what the prop asset expects when it selects a prop resource. + +:::tip +The returned object type must be compatible with the asset that uses the resource. For example, a character asset expects a `GameObject` when it selects a character resource, a screen asset expects a `ImageResource` when it selects an image resource, etc. A list of expected types for internal assets can be found in the [Built-in Resource Types](#built-in-resource-types) section. +::: + +Now we just need to register it in the `OnCreate` method of the plugin: + +```csharp +Context.ResourceManager.RegisterUriResolver(new PrimitivePropResourceUriResolver(), this); +``` + +Once the plugin is reloaded, when you select a cube or a sphere in the prop asset, Warudo will create a cube or a sphere in the Unity scene! + +## Mod Collection + +A common use case of resource providers and resolvers is to provide a collection of mods. For example, if you have 100 prop prefabs in Unity and would like to use them in Warudo, compared to exporting 100 [prop mods](../../modding/prop-mod) to the `Props` directory, you can write a custom resource provider and resolver to load the prefabs directly from the plugin's mod folder (see [Loading Unity Assets](plugins#loading-unity-assets)). This comes with the added benefit that your users would be able to see all of your resources under the same category in the dropdown. + +:::tip +For best practices on writing a mod collection plugin, please refer to the [Katana Animations](https://gist.github.com/TigerHix/2cb8052b0e8aeeb7f9cb796dc7edc6a3) sample plugin. +::: + +## Custom Resource Types + +Resources are designed to be generic, so you can use them for any type of data. For example, if you are writing a plugin that lets the user spawn an emote, you may want to create a custom resource type called `Emote`: + +```csharp +[AutoCompleteResource("Emote")] +public string Emote; // This will be used by the user to select the emote URI +``` + +Then write a custom resource provider that provides a list of emote URIs (say `emote://xxx/yyy`) when the query is `"Emote"`, and a custom resource URI resolver that loads the emote data when the URI scheme matches your custom scheme. + +## Built-in Resource Types {#built-in-resource-types} + +Here is a list of built-in resource types used by the built-in assets: + +| Query | Example(s) | Expects | +|--------------------|------------------------------------------|--------------------------------------------| +| `"Character"` | Character | `GameObject ` | +| `"CharacterAnimation"` | Character, Play Character Idle Animation | `AnimationClip` | +| `"Environment"` | Environment | `Scene` or `ValueTuple` | +| `"Image"` | Screen | `Warudo.Plugins.Core.Utils.ImageResource` | +| `"Music"` | Music Player | `string` (absolute file path) | +| `"Particle"` | Throw Prop At Character | `GameObject` | +| `"Prop"` | Prop | `GameObject` | +| `"Sound"` | Play Sound, Throw Prop At Character | `AudioClip` | +| `"Video"` | Screen | `string` (absolute file path) | + +## Thumbnail Resolver + +When resource dropdowns are annotated with the `[PreviewGallery]` attribute, the user can click the "Preview Gallery" button to see a grid of thumbnails of the resources. This is useful when the resources are images, props, poses, or any other visual data. + +To provide thumbnails, you need to implement the `IResourceUriThumbnailResolver` interface that asynchronously returns a `byte[]` of the thumbnail image data. Then, register the resolver in the plugin's `OnCreate` method: + +```csharp +Context.ResourceManager.RegisterUriThumbnailResolver(new MyUriThumbnailResolver(), this); +``` + +:::tip +For best experience, make sure your thumbnail images can be loaded within 50ms. +::: + + diff --git a/docs/scripting/api/scene.md b/docs/scripting/api/scene.md new file mode 100644 index 0000000..952b588 --- /dev/null +++ b/docs/scripting/api/scene.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 1 +--- + +# Scene + +A scene is a JSON file that stores a list of assets, a list of blueprints, and plugin settings specific to that scene. When you open a saved scene in the editor, the saved assets are created and deserialized (i.e., restored) first; then, the saved blueprints are instantiated one by one, with nodes in each created and deserialized. + +:::info +In the codebase, blueprints are called _graphs_. +::: + +## Accessing Scene Data + +You can access the currently opened scene's data using `Context.OpenedScene`. Then, you can access the in-scene assets, blueprints, etc.: + +```csharp +var scene = Context.OpenedScene; + +var assets = scene.GetAssets(); +var characterAssets = scene.GetAssets(); +var blueprints = scene.GetGraphs(); +``` + +You can instantiate new assets or nodes: + +```csharp +var newCharacterAsset = scene.AddAsset(); // Instantiate a new character asset +var newCharacterAssetByTypeId = scene.AddAsset("726ab674-a550-474e-8b92-66526a5ad55e"); // Instantiate a new character asset by type ID + +var blueprint = scene.GetGraphs().Values.First(); // Get the first blueprint in the scene +var newNode = blueprint.AddNode(); // Instantiate a new node +var newNodeByTypeId = blueprint.AddNode("e931f780-e41e-40ce-96d0-a4d47ca64853"); // Instantiate a new node by type ID + +Context.Service.BroadcastOpenedScene(); // Send the updated scene to the editor +``` + + diff --git a/docs/scripting/api/service.md b/docs/scripting/api/service.md new file mode 100644 index 0000000..5e7bfc4 --- /dev/null +++ b/docs/scripting/api/service.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 75 +--- + +# Communicating with the Editor {#service} + +Any communication between the runtime and the editor is done through the `Service` class, accessible via the `Context` singleton object. For example, to display a popup message in the editor, you can use the following code: + +```csharp +Context.Service.PromptMessage("Hey!", "I'm a message!"); +``` + +Here are some of the commonly used methods: + +- **`void BroadcastOpenedScene()`:** Send the entire scene to the editor. You only need to do this after programmatically adding/removing assets and blueprints to/from the scene. +- **`void Toast(ToastSeverity severity, string header, string summary, string message = null, TimeSpan duration = default)`:** Show a toast message in the editor. If `message` is not `null`, the user can click on the toast to see the full message. +- **`void PromptMessage(string header, string message, bool markdown = false)`:** Show a popup message in the editor. If `markdown` is `true`, the message will be rendered as Markdown text. +- **`UniTask PromptConfirmation(string header, string message)`:** Show a confirmation dialog in the editor. Returns `true` if the user clicks "OK", and `false` if the user clicks "Cancel". +- **`UniTask PromptStructuredDataInput(string header, T structuredData = null)`:** Show a [structured data input dialog](structured-data#input) in the editor. If passed a `structuredData` object, the dialog will be pre-filled with the data. Returns the structured data object after the user clicks "OK", or `null` if the user clicks "Cancel". +- **`UniTask PromptStructuredDataInput(string header, Action structuredDataInitializer)`:** Similar to the above, but the `structuredDataInitializer` function is called to initialize the structured data object. +- **`void ShowProgress(string message, float progress, TimeSpan timeout = default)`:** Show a progress bar in the editor. The `progress` value should be between 0 and 1. If `timeout` is specified, the progress bar will automatically hide after the specified duration. +- **`void HideProgress()`:** Hide the progress bar. +- **`void NavigateToGraph(Guid graphId, Guid nodeId = default)`:** Navigate to the specified graph in the editor. If `nodeId` is specified, the editor will select the specified node in the graph. +- **`void NavigateToPlugin(string pluginId, string port = default)`:** Navigate to the specified plugin in the editor. If `port` is specified, the editor will navigate to the specified port of the plugin. + +:::tip +You can safely assume `Context.Service` is always available in the runtime, even if the editor is closed. +::: + + diff --git a/docs/scripting/api/structured-data.md b/docs/scripting/api/structured-data.md new file mode 100644 index 0000000..2c35733 --- /dev/null +++ b/docs/scripting/api/structured-data.md @@ -0,0 +1,316 @@ +--- +sidebar_position: 60 +--- + +# Structured Data + +Structured data is a way to define an embedded data structure within an entity. They are useful for defining complex data inputs that need to be reused within the same entity or across multiple entities. + +## Type Definition + +You can create structured data types by inheriting from the `StructuredData` type, like below: + +```csharp +public class MyTransformData : StructuredData { + [DataInput] + public Vector3 Position; + + [DataInput] + public Vector3 Rotation; + + [DataInput] + public Vector3 Scale = Vector3.one; + + [Trigger] + public void ResetAll() { + Position = Vector3.zero; + Rotation = Vector3.zero; + Scale = Vector3.one; + Broadcast(); + } +} +``` + +Then you can use this structured data type as any data input field's type in an entity: + +```csharp +[DataInput] +public MyTransformData MyTransform; +``` + +Which looks like this: + +
+
+![](/doc-img/en-structured-data-1.png) +
+
+ +Note that you do not need to assign a value to the structured data field as it will be automatically instantiated when the entity is created. That means you can access the structured data's data inputs directly: + +```csharp +public override void OnCreate() { + base.OnCreate(); + MyTransform.ResetAll(); +} +``` + +## Components + +A structured data type can define data inputs and triggers. + +
+
+![](/doc-img/en-structured-data-5.png) +
+
+ +## Lifecycle + +A structured data only has one additional lifecycle event `OnUpdate()`, other than the standard lifecycle events (e.g., `OnCreate()`) listed on the [Entities](entities#lifecycle) page. + +:::tip +Structured data are best thought as data containers, so you should not perform complicated logic inside them. It is recommended to keep structured data as simple as possible and let the parent entity handle the heavy lifting. +::: + +## Updating Structured Data + +Structured data are just entities, so you can update their data inputs like any other entity: + +```csharp +MyTransform.SetDataInput(nameof(MyTransform.Position), new Vector3(1, 2, 3), broadcast: true); + +// or + +MyTransform.Position = new Vector3(1, 2, 3); +MyTransform.BroadcastDataInput(nameof(MyTransform.Position)); +``` + +When updating multiple data inputs, you can also just assign to the fields directly, and use the `Broadcast` method to broadcast all data inputs at once: + +```csharp +MyTransform.Position = new Vector3(1, 2, 3); +MyTransform.Rotation = new Vector3(0, 0, 0); +MyTransform.Scale = new Vector3(1, 1, 1); +MyTransform.Broadcast(); // Or BroadcastDataInput(nameof(MyTransform)); +``` + +## Nested Structured Data + +Structured data can be nested: + +```csharp +public class MyData1 : StructuredData { + [DataInput] + public MyData2 NestedData; + + public class MyData2 : StructuredData { + [DataInput] + public MyData3 NestedData; + + public class MyData3 : StructuredData { + [DataInput] + public bool SuperNestedBool; + } + } +} +``` + +## Arrays + +You can also define arrays of structured data: + +```csharp +[DataInput] +public MyTransformData[] MyTransforms; +``` + +Note you do not need to initialize the array. Warudo will automatically initialize an empty array: + +
+
+![](/doc-img/en-structured-data-2.png) +
+
+ +When the user clicks the **+** button, structured data elements are appended to the array: + +
+
+![](/doc-img/en-structured-data-3.png) +
+
+ +## Custom Initializer + +Elements in a structured data array are automatically initialized by Warudo: + +```csharp +[DataInput] +public MyTransformData[] MyTransforms; // Each MyTransforms[i].Scale is initialized to (1, 1, 1) +``` + +However, sometimes you may want to initialize the new element with dynamic values. We can use the `[StructuredDataInitializer]` attribute to specify a method that will be called to initialize the structured data: + +```csharp +[DataInput] +[StructuredDataInitializer(nameof(InitializeTransform))] +public MyTransformData[] MyTransforms; + +protected void InitializeTransform(MyTransformData transform) { + transform.Position = new Vector3(Random.value, Random.value, Random.value); + transform.Rotation = new Vector3(Random.value, Random.value, Random.value); + transform.Scale = new Vector3(Random.value, Random.value, Random.value); + // Note there is no need to broadcast - the structured data has not be sent to the editor anyway +} +``` + +When the user clicks the **+** button, the `InitializeTransform` method is called to initialize the new structured data element. + +## Programmatically Creating Structured Data + +To manually populate the structured data array, or to add structured data elements programmatically, use the `StructuredData.Create()` method: + +```csharp +public override void OnCreate() { + base.OnCreate(); + + // Create a new structured data instance + var mySampleData = StructuredData.Create(); + mySampleData.Position = new Vector3(1, 2, 3); + // Or equivalently + // var mySampleData = StructuredData.Create(sd => sd.Position = new Vector3(1, 2, 3)); + + SetDataInput(nameof(MyTransforms), new [] { mySampleData }, broadcast: true); +} + +[Trigger] +public void AddNewRandomTransform() { + var newTransform = StructuredData.Create(); + newTransform.Position = new Vector3(Random.value, Random.value, Random.value); + newTransform.Rotation = new Vector3(Random.value, Random.value, Random.value); + newTransform.Scale = new Vector3(Random.value, Random.value, Random.value); + + var newTransforms = new List(MyTransforms); + newTransforms.Add(newTransform); + + SetDataInput(nameof(MyTransforms), newTransforms.ToArray(), broadcast: true); +} +``` + +:::caution +Do not use `new MyTransformData()` to create structured data instances. Always use `StructuredData.Create()` to ensure proper entity initialization. +::: + +## Collapsible Structured Data + +Structured data can be collapsed to save space: + +
+
+![](/doc-img/en-structured-data-4.png) +
+
+ +You only need to implement the `ICollapsibleStructuredData` interface: + +```csharp +public class MyTransformData : StructuredData, ICollapsibleStructuredData { + // ... + + public string GetHeader() => Position + " " + Rotation + " " + Scale; +} +``` + +You can use the `CollapsedSelf` and `CollapsedInHierarchy` properties to determine whether the structured data is collapsed. For example, you can toggle the visibility of a GameObject in the Unity scene based on the structured data's collapsed state: + +```csharp +public override void OnUpdate() { + base.OnUpdate(); + gameObject.SetActive(!CollapsedInHierarchy); +} +``` + +## Accessing Parent Entity + +If you need to access the parent entity from the structured data, you can change the base class to `StructuredData` and use the `Parent` property, which is automatically casted to `TParent`. Using the `MyTransformData` example: + +```csharp +[AssetType(...)] +public class MyTransformAsset : Asset { + + [DataInput] + public MyTransformData MyTransform; + + protected void ParentMethod() { + // ... + } + + public class MyTransformData : StructuredData { + // ... + + public void AccessParentMethod() { + Parent.ParentMethod(); + } + } +} +``` + +Note that `Parent` is `null` when `OnCreate` is called, so you should not access the parent entity in `OnCreate`. Instead, implement the `OnAssignedParent()` callback: + +```csharp +public class MyTransformData : StructuredData { + public override void OnAssignedParent() { + Parent.ParentMethod(); + } +} +``` + +## Structured Data Input Dialog {#input} + +A very common use case for structured data is to allow the user to input data in a dialog. Remember the onboarding assistant? Every popup window that showed up during the onboarding process was actually a structured data! + +To create a structured data input dialog, you simply define a structured data type and call `Context.Service.PromptStructuredDataInput`: + +```csharp +[Trigger] +public async void PromptUserInput() { + var sd = await Context.Service.PromptStructuredDataInput("Customize Your Transform"); + if (sd == null) return; // The user clicked cancel + + Context.Service.Toast(ToastSeverity.Success, + "Thank you for your input!", + "Your new transform is: " + sd.GetHeader()); + SetDataInput(nameof(MyTransform), sd, broadcast: true); +} +``` + +When combined with `[Markdown(primary: true)]` on string data inputs and `[CardSelect]` on enum data inputs, plus some clever use of `[HiddenIf]`, you will be able to create great-looking and user-friendly dialogs in your plugins! + +### Retry Dialog + +You can "retry" the current dialog by calling `PromptStructuredDataInput` again with the current structured data: + +```csharp +[Trigger] +public async void PromptUserInput() { + var sd = await Context.Service.PromptStructuredDataInput("Customize Your Transform"); + if (sd == null) return; // The user clicked cancel + + while (sd.Position == Vector3.zero) { + Context.Service.Toast(ToastSeverity.Error, "Invalid Input", "Position cannot be zero!"); + sd = await Context.Service.PromptStructuredDataInput("Customize Your Transform", sd); // Note the second parameter + if (sd == null) return; // The user clicked cancel + } +} +``` + +This saves the user's edits to the structured data while allowing them to correct the invalid inputs. + + diff --git a/docs/scripting/api/watchers.md b/docs/scripting/api/watchers.md new file mode 100644 index 0000000..06f4c06 --- /dev/null +++ b/docs/scripting/api/watchers.md @@ -0,0 +1,106 @@ +--- +sidebar_position: 80 +--- + +# Data Input Watchers {#watchers} + +Often you are interested in knowing when the value of a data input changes. For example, let's say you have the following code that lets the user select a file from the `MyPluginFiles` directory in the data folder: + +```csharp +[DataInput] +[AutoCompleteList(nameof(AutoCompletePluginFiles), forceSelection: true)] +public string SelectedFile; + +protected async UniTask AutoCompletePluginFiles() { + return AutoCompleteList.Single(Context.PersistentDataManager.GetFileEntries("MyPluginFiles").Select(it => new AutoCompleteEntry { + label = it.fileName, + value = it.path + })); +} +``` + +You want to know when the user selects a new file. The naive way is to check the value of `SelectedFile` every frame, but this is inefficient and cumbersome. Instead, you can register a watcher in `OnCreate()`: + +```csharp +protected override void OnCreate() { + base.OnCreate(); + Watch(nameof(SelectedFile), OnSelectedFileChanged); +} + +protected void OnSelectedFileChanged(string from, string to) { + if (to == null) { + // The user has cleared the selected file + } + // More logic to handle the file change +} +``` + +You can also watch another entity's data input by using the overloaded `Watch(Entity otherEntity, string dataInputKey, Action onChange, bool deep = true)` method. + +:::tip +Watchers are automatically unregistered when the entity is destroyed. +::: + +## Watching Asset States + +Sometimes, you want to watch more than just the value change. For example, consider the following simple asset that prints a message whenever the user selects a new `Character`: + +```csharp +[DataInput] +public CharacterAsset Character; + +protected override void OnCreate() { + base.OnCreate(); + Watch(nameof(Character), OnCharacterChanged); +} + +protected void OnCharacterChanged(CharacterAsset from, CharacterAsset to) { + if (to != null) Debug.Log("New character file selected! " + to.Source); +} +``` + +However, the watcher is only triggered when the value of `Character` data input in the current asset changes; that means if the scene has two characters, the watcher is triggered when the user changes from one character to another. However, let's say there is only one character asset in the scene, and the user goes into the character asset and change the `Source` data input, then this watcher will not be triggered. + +In this case, you can use `WatchAsset`: + +```csharp +protected override void OnCreate() { + base.OnCreate(); + WatchAsset(nameof(Character), OnCharacterChanged); +} + +protected void OnCharacterChanged() { + if (Character.IsNonNullAndActive()) Debug.Log("New character file selected! " + Character.Source); +} +``` + +The watcher is now triggered both when the value of `Character` changes and when the `Source` data input of the `Character` asset changes. + +:::info +Why does this work? The `WatchAsset` method will additionally trigger the watcher when an asset's [active state](assets#active-state) changes. In Warudo, all assets that have a `Source` data input toggles the active state based on the `Source` data input. For example, `CharacterAsset` uses `Source` to load a .vrm model or .warudo character mod, `PropAsset` uses `Source` to load a .warudo prop mod, and so on. The active state is only `true` if the `Source` is successfully loaded. + +Say we have a currently active asset with a `Source` selected. When the user selects a new `Source`, the active state is set to `false` (triggering the watcher), and then if the new `Source` is successfully loaded, set to `true` again (triggering the watcher again). In the above code, only the second trigger will print the message, since when the first trigger is called, the character asset is not yet active. +::: + +## Watching Multiple Data Inputs + +If you need to watch multiple data inputs and only care about executing a callback function when any of them changes, you can use the `WatchAll` method: + +```csharp +protected override void OnCreate() { + base.OnCreate(); + WatchAll(new [] { nameof(A), nameof(B), nameof(C) }, OnAnyDataInputChanged); +} + +protected void OnAnyDataInputChanged() { + // Called when A, B, or C changes +} +``` + + diff --git a/docs/scripting/creating-your-first-plugin-mod.md b/docs/scripting/creating-your-first-plugin-mod.md new file mode 100644 index 0000000..4bdee04 --- /dev/null +++ b/docs/scripting/creating-your-first-plugin-mod.md @@ -0,0 +1,196 @@ +--- +sidebar_position: 2 +--- + +# Creating Your First Plugin Mod + +In the last tutorial, we used [Playground](playground) to load a custom node and a custom asset. However, this comes with limitations: you aren't able to reference any Unity assets, so if you are looking to create a node that spawns a custom Unity particle prefab, you are out of luck with Playground. + +In this tutorial, we will use the [Warudo SDK](../modding/mod-sdk) to build a plugin mod that contains the `HelloWorldNode` and `CookieClickerAsset` we just created. A plugin mod not only allows you to store and reference Unity assets, but also allows you to distribute your custom nodes and assets to other users. + +## Step 1: Create a Warudo SDK Project + +If you haven't yet, create a new Warudo SDK project by following the [Warudo SDK Installation](../modding/sdk-installation) guide. We also recommend following the [Creating Your First Mod](../modding/creating-your-first-mod) tutorial to get familiar with the Warudo SDK first. + +:::tip +Building a plugin mod is extremely similar to building other types of mod! The only difference is that a plugin mod needs to contain a C# script that inherits from `Plugin`, as we will see below. +::: + +## Step 2: Create a New Mod + +To create a new mod, go to the menu bar and select **Warudo → New Mod**: + +![](/doc-img/en-mod-sdk-3.webp) + +Type "HelloWorldPlugin" in **Mod Name**, and click "Create Mod!": + +![](/doc-img/en-plugin-mod-1.png) + +You should see a folder for your mod has just been created under the Assets folder. + +## Step 3: Create a Plugin Script + +In the "HelloWorldPlugin" mod folder, right click and create a new C# script called `HelloWorldPlugin.cs`. Paste the following code into the script: + +```csharp +using UnityEngine; +using Warudo.Core.Attributes; +using Warudo.Core.Plugins; + +[PluginType( + Id = "hakuyatira.helloworld", + Name = "Hello World", + Description = "A simple plugin that says hello to the world.", + Version = "1.0.0", + Author = "Hakuya Tira", + SupportUrl = "https://docs.warudo.app", + AssetTypes = new [] { typeof(CookieClickerAsset) }, + NodeTypes = new [] { typeof(HelloWorldNode) })] +public class HelloWorldPlugin : Plugin { + + protected override void OnCreate() { + base.OnCreate(); + Debug.Log("The Hello World plugin is officially enabled! Hooray!"); + } + +} +``` + +Copy the `HelloWorldNode.cs` and `CookieClickerAsset.cs` files from the [previous tutorial](creating-your-first-script) into the "HelloWorldPlugin" mod folder. + +Your mod folder should now look like this: + +
+
+ +
+
+ +## Step 4: Export the Plugin Mod + +We are almost done! Before exporting the mod, you can check the mod settings are correct in **Warudo → Mod Settings**. You can set the mod's name, version, author, and description, which should be the same as the `[PluginType]` parameters. + +By default, the **Mod Export Directory** is empty; in this case, the mod will be exported to the project's root folder. You can instead set it to the `Plugins` directory in Warudo's data folder, for example `C:\Program Files (x86)\Steam\steamapps\common\Warudo\Warudo_Data\StreamingAssets\Plugins`. + +:::warning +Before exporting, close Warudo and delete `HelloWorldNode.cs` and `CookieClickerAsset.cs` from the `Playground` directory. This is to prevent conflicts between the Playground and the plugin mod. +::: + +Select **Warudo → Export Mod** to export the plugin mod. If everything goes well, you should see a `BUILD SUCCEEDED!` message in the console: + +![](/doc-img/en-plugin-mod-4.png) + +It is good to perform a sanity check to ensure the scripts in the mod folder are indeed compiled into the plugin mod. Scroll up in the console, and you should find the following line: + +![](/doc-img/en-plugin-mod-5.png) + +:::warning +If you do not see this line, your Unity project may not have been set up for C# scripting correctly. Please follow the steps in [this section](../modding/mod-sdk#custom-scripts) and try exporting the plugin mod again. +::: + +Make sure the exported `HelloWorldPlugin.warudo` folder is in the `Plugins` directory in Warudo's data folder. You can now open Warudo and check if the plugin mod is loaded in the About dialog: + +![](/doc-img/en-plugin-mod-3.png) + +And of course, the Cookie Clicker asset is now in the **Add Asset** menu, along with the Hello World node in the node palette: + +![](/doc-img/en-getting-started-playground-7.png) + +![](/doc-img/en-getting-started-playground-2.png) + +Voilà! Your first plugin mod in Warudo! + +## Step 5: Load An Unity Asset + +Now that you have a plugin mod, you can load custom Unity assets into Warudo! Let's try loading a Unity asset in the `CookieClickerAsset` script. + +Download [Cartoon FX Remaster Free](https://assetstore.unity.com/packages/vfx/particles/cartoon-fx-remaster-free-109565) from the Unity Asset Store. Import the package into your Unity project. Then, select a particle prefab from the package, hold **Ctrl**, and drag it into the "HelloWorldPlugin" folder. We will use the "CFXR Explosion 1" prefab in this example. + +![](/doc-img/en-plugin-mod-6.png) + +Rename the prefab into `Particle`. Your mod folder should now look like this: + +![](/doc-img/en-plugin-mod-7.png) + +Open the `CookieClickerAsset.cs` script and replace with below: + +```csharp +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; +using Warudo.Core.Attributes; +using Warudo.Core.Scenes; +using Object = UnityEngine.Object; +using Random = UnityEngine.Random; + +[AssetType(Id = "82ae6c21-e202-4e0e-9183-318e2e607672", Title = "Cookie Clicker")] +public class CookieClickerAsset : Asset { + + [Markdown] + public string Status = "You don't have any cookies."; + + [DataInput] + [IntegerSlider(1, 10)] + [Description("Increase me to get more cookies each time!")] + public int Multiplier = 1; + + private int count; + private GameObject particlePrefab; // New field to store the particle prefab + + [Trigger] + public async void GimmeCookie() { // Note the async keyword + count += Multiplier; + SetDataInput(nameof(Status), "You have " + count + " cookie(s).", broadcast: true); + + // Spawn the particle prefab Multiplier times + for (var i = 0; i < Multiplier; i++) { + var particle = Object.Instantiate(particlePrefab, Random.insideUnitSphere * 2f, Quaternion.identity); + particle.SetActive(true); + Object.Destroy(particle, 3f); // Automatically destroy the cloned particle after 3 seconds + + await UniTask.Delay(TimeSpan.FromSeconds(0.2f)); // Delay 0.2 seconds before spawning the next particle + } + } + + protected override void OnCreate() { + base.OnCreate(); + SetActive(true); + + // Load the particle prefab from the mod folder. Change this path if your prefab is in a different folder + particlePrefab = Plugin.ModHost.Assets.Instantiate("Assets/HelloWorldPlugin/Particle.prefab"); + // Disable it so that it doesn't show up in the scene + particlePrefab.SetActive(false); + } + +} +``` + +Export the mod again. Warudo supports hot-reloading of plugins, so you can simply export the mod to the `Plugins` folder and see the changes in Warudo immediately! + +Press the "Gimme Cookie" button in the Cookie Clicker asset, and you should see the particle prefab spawning in the scene: + +![](/doc-img/en-plugin-mod-8.png) + +:::info +For Warudo Pro users, please switch to the built-in rendering pipeline to view the particle effect. Alternatively, you can use a URP-compatible particle asset instead. +::: + +Isn't that cool you can do this in Warudo? + +## Comparison to Playground + +Creating a plugin mod is more powerful than using [Playground](playground), but it also has disadvantages: + +* Developing a plugin mod can be much slower, as you need to export the mod every time you make a change; +* Playground can currently access more libraries used by Warudo, such as MessagePack, WebSocketSharp, etc. (we are working on improving this for plugin mods); +* Plugin mods have more [security limitations](plugin-mod#limitations). For example, you cannot access the `System.IO` namespace (though we provide a [sandboxed file persistence API](api/io)). + +When to use a plugin mod or Playground depends on your use case. If you are prototyping a new feature or testing a new idea, Playground is a great tool to quickly iterate and see the results, especially if you don't need to reference Unity assets. If you work on custom development for VTubers, Playground is brilliant for implementing small, self-contained features for your clients. However, if you need to load custom Unity assets or distribute your custom nodes and assets on [our Steam Workshop](../modding/sharing), a plugin mod is the way to go. (Even still, you can use Playground to prototype your custom nodes and assets before moving them to a plugin mod!) + + diff --git a/docs/scripting/creating-your-first-script.md b/docs/scripting/creating-your-first-script.md new file mode 100644 index 0000000..59cb278 --- /dev/null +++ b/docs/scripting/creating-your-first-script.md @@ -0,0 +1,163 @@ +--- +sidebar_position: 1 +--- + +# Creating Your First Script + +There are two ways of scripting in Warudo: using **Playground**, or creating a [plugin mod](distribution.md). In this tutorial, we will look into using Playground to write your first custom node! + +:::tip +**What is playground?** Think of Playground as a sandbox where you can write and test your custom nodes and assets. Instead of compiling and packaging your C# code, you can write your code directly in the `Playground` directory in your Warudo data folder. This allows you to quickly iterate and test your code without the need to build mods or restart Warudo. +::: + +Without further ado, let's dive in! + +:::info +Keep in mind that if you have any questions, we have a dedicated **#plugins-scripting** channel on [our Discord server](https://discord.gg/warudo) where you can ask for help! +::: + +## Step 1: Environment Setup + +First, make sure you have [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) installed. Then, download the `.csproj` file [here](/scripts/Playground.csproj) and put it into the `Playground` directory inside your Warudo data folder (Menu → Open Data Folder). This file helps your IDE to provide code auto-completion and syntax highlighting. + +Open your favorite C# IDE - we will use [JetBrains Rider](https://www.jetbrains.com/rider/), but other IDEs such as [Visual Studio Code](https://code.visualstudio.com/) would work just fine (for Visual Studio Code, you may need to install the [C# language extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)). + +Open the `Playground` directory in your IDE. Create a file called `HelloWorldNode.cs` in the directory, and paste the following into the file: + +```csharp +using Warudo.Core; +using Warudo.Core.Attributes; +using Warudo.Core.Graphs; + +[NodeType(Id = "c76b2fef-a7e7-4299-b942-e0b6dec52660", Title = "Hello World")] +public class HelloWorldNode : Node { + + [FlowInput] + public Continuation Enter() { + Context.Service.PromptMessage("Hello World!", "This node is working!"); + return Exit; + } + + [FlowOutput] + public Continuation Exit; + +} +``` + +## Step 2: Custom Node + +Open Warudo. Upon startup, you should see a pop-up message (called a _toast_) like this: + +![](/doc-img/en-getting-started-playground-1.png) + +This indicates the `HelloWorldNode.cs` has been successfully compiled and loaded, and since the code above defines a custom node, we see `Nodes: 1` in the message. + +Go to the blueprints tab. Create a new blueprint and find the `Hello World` node in the node palette. Drag it out and you should see something like this: + +![](/doc-img/en-getting-started-playground-2.png) + +Click on the `Enter` flow input, and you should see this message: + +![](/doc-img/en-getting-started-playground-3.png) + +If all is working, congratulations! You have just created your very first Warudo node! + +## Step 3: Hot Reload + +One thing awesome is that you don't need to restart Warudo every time you make code changes. In fact, Warudo automatically detects any changes you have made in the `Playground` directory and hot-reload your code! + +Let's see that in action. Go back to the `HelloWorldNode.cs` and paste the following new code (or update your existing code if you want to practice typing): + +```csharp +using Warudo.Core; +using Warudo.Core.Attributes; +using Warudo.Core.Graphs; + +[NodeType(Id = "c76b2fef-a7e7-4299-b942-e0b6dec52660", Title = "Hello World")] +public class HelloWorldNode : Node { + + // New code below + [DataInput] + [IntegerSlider(1, 100)] + public int LuckyNumber = 42; + // New code above + + [FlowInput] + public Continuation Enter() { + // Changed code below + Context.Service.PromptMessage("Hello World!", "This node is working! My lucky number: " + LuckyNumber); + return Exit; + } + + [FlowOutput] + public Continuation Exit; + +} +``` + +Immediately after you press Ctrl+S, another toast message should appear, indicating the node has been successfully compiled and hot-reloaded. The node in the blueprint is also updated accordingly: + +![](/doc-img/en-getting-started-playground-4.png) + +Try set a lucky number and verify the node still works! + +![](/doc-img/en-getting-started-playground-5.png) + +## Step 4: Custom Asset + +Warudo can also detect new `.cs` files. Let's try creating a custom asset this time. Create a file called `CookieClickerAsset.cs` in the `Playground` directory, and paste the following code: + +```csharp +using Warudo.Core.Attributes; +using Warudo.Core.Scenes; + +[AssetType(Id = "82ae6c21-e202-4e0e-9183-318e2e607672", Title = "Cookie Clicker")] +public class CookieClickerAsset : Asset { + + [Markdown] + public string Status = "You don't have any cookies."; + + [DataInput] + [IntegerSlider(1, 10)] + [Description("Increase me to get more cookies each time!")] + public int Multiplier = 1; + + private int count; + + [Trigger] + public void GimmeCookie() { + count += Multiplier; + SetDataInput(nameof(Status), "You have " + count + " cookie(s).", broadcast: true); + } + + protected override void OnCreate() { + base.OnCreate(); + SetActive(true); + } + +} +``` + +You should see a toast message that indicates one custom node (`HelloWorldNode.cs`) and one custom asset (`CookieClickerAsset.cs`) have been loaded: + +![](/doc-img/en-getting-started-playground-6.png) + +You can find the new asset in the **Add Asset** menu: + +![](/doc-img/en-getting-started-playground-7.png) + +Add it to your scene and have fun! Who needs VTubing when I can do this all day? + +![](/doc-img/en-getting-started-playground-8.png) + +## Summary + +In this short tutorial, we learned how to use Playground and took a glimpse into how custom nodes and assets work. In the next tutorial, we will see how to create a plugin mod to distribute the custom node and asset we have just created! + + diff --git a/docs/scripting/custom-asset.md b/docs/scripting/custom-asset.md deleted file mode 100644 index 6c2c5f4..0000000 --- a/docs/scripting/custom-asset.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -sidebar_position: 20 ---- - -# Custom Asset - -:::warning - -This page is still under development and is not completed. We are working to improve the content. - -::: - -## Examples - -### Basic - -- [AnchorAsset.cs](https://gist.github.com/TigerHix/c549e984df0be34cfd6f8f50e741aab2) -Attachable / GameObjectAsset example. - -### Advanced - -- [CharacterPoserAsset.cs](https://gist.github.com/TigerHix/8413f8e10e508f37bb946d8802ee4e0b) -Custom asset to pose your character with IK anchors. - - - diff --git a/docs/scripting/custom-node.md b/docs/scripting/custom-node.md deleted file mode 100644 index 1dfb7ce..0000000 --- a/docs/scripting/custom-node.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -sidebar_position: 30 ---- - -# Custom Node - -:::warning - -This page is still under development and is not completed. We are working to improve the content. - -::: - -## Examples - -### Basic - -- [ExampleNode.cs](https://github.com/HakuyaLabs/WarudoPlaygroundExamples/blob/master/ExampleNode.cs) -Standard example of node, showing the basic format of various node components. - -- [StructuredDataExampleNode.cs](https://gist.github.com/TigerHix/81cfa66a8f810165c426d1b5157677b5) -Creating "inner" data types. (StructuredData) - -- [GetRandomSoundNode.cs](https://gist.github.com/TigerHix/f0f1a7e3c53ca65450fdca1ff06eb343) -Get Random Sound node. - -- [LiteralCharacterAnimationSourceNode.cs](https://gist.github.com/TigerHix/2dc58213defe400ddb280a8cc1e6334b) -Character Animation (source) node. - -- [SmoothTransformNode.cs](https://gist.github.com/TigerHix/eaf8e05e5e1b687b8265420b9943903d) -Smooth Transform node. - -### Advanced - -- [FindAssetByTypeNode.cs](https://gist.github.com/TigerHix/ab3522bb25669457cc583abc4fb025d2) -AutoCompleteList (Dropdown) example. - -- [MultiGateNode.cs](https://gist.github.com/TigerHix/8747793a68f0aa15a469f9823812e221) -Dynamic data / flow ports example. - -- [ThrowPropAtCharacterNode.cs](https://gist.github.com/TigerHix/18e9f20152c0cfac38fd5528c7af16b6) -Throw Prop At Character. - -- [SpawnStickerNode.cs](https://gist.github.com/TigerHix/fe35442e9052cd8c4ea80e0261349321) -Spawn Local / Online Image Sticker. - - - diff --git a/docs/scripting/custom-plugin.md b/docs/scripting/custom-plugin.md deleted file mode 100644 index 3540c97..0000000 --- a/docs/scripting/custom-plugin.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -sidebar_position: 40 ---- - -# Custom Plugin - -:::warning - -This page is still under development and is not completed. We are working to improve the content. - -::: - -## Examples - -### Basic - -- [Example plugin](https://gist.github.com/TigerHix/b78aabffc2d03346ff3da526706ce2ca) -The template of a plugin (single file) with the basic use of `Plugin` class. - -- [WarudoPluginExamples](https://github.com/HakuyaLabs/WarudoPluginExamples) -The complete plugin examples (multiple files), including the **Stream Deck Plugin** and **VMC Plugin**. - - **Stream Deck Plugin**: This plugin communicates with an external application (Warudo's [Stream Deck plugin](https://apps.elgato.com/plugins/warudo.streamdeck)) via WebSocket. It demonstrates the use of the WebSocket service base class `WebSocketService`. - - **VMC Plugin**: This plugin adds [VMC](https://protocol.vmc.info/english) as a supported motion capture method via registering a `FaceTrackingTemplate` and a `PoseTrackingTemplate`. - -### Advanced - -- [KatanaAnimations.cs](https://gist.github.com/TigerHix/2cb8052b0e8aeeb7f9cb796dc7edc6a3) -Custom plugin to load AnimationClips from the mod folder and registers them as character animations. - -- [KatanaAnimations.cs #Comment](https://gist.github.com/TigerHix/2cb8052b0e8aeeb7f9cb796dc7edc6a3?permalink_comment_id=4633225#gistcomment-4633225) -Built in resource types and what URI resolvers are expected to return. -( if you are looking to use `Context.ResourceManager.ResolveResourceUri` ) - -## File Structure - -The recommended file structure of a plugin project is as follows: -Note that all files and folders except `*Plugin.cs` are optional, you can create them only when needed. - -``` - -│ -├── Plugin.cs -│ -├── Localizations -│ └── Warudo..json -│ -├── Assets -│ ├── Asset1.cs -│ ├── Asset2.cs -│ └── ... -│ -├── Nodes -│ ├── Node1.cs -│ ├── Node2.cs -│ └── ... -│ -└── ... -``` - - - diff --git a/docs/scripting/debugging.md b/docs/scripting/debugging.md new file mode 100644 index 0000000..c9798a8 --- /dev/null +++ b/docs/scripting/debugging.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 1000 +--- + +# Debugging + +Scripts not working as intended? Here's some debugging tips! + +## Restart Warudo + +As cliché as it sounds, restarting Warudo can solve a lot of issues, especially when your mods or scripts are hot-reloaded. Always try this first if you encounter a head-scratching issue. + +## Logging + +You can use Unity's `Debug.Log` to print messages to the console as usual. You can access the logs folder by using **Menu → Open Logs Folder**. The `Player.log` file contains the current session's logs, while `Player-prev.log` contains the logs from the previous session. + +## Enable Full Build Logs + +If you are creating a [plugin mod](plugin-mod), we recommend to set **Log Level** to All and uncheck **Clear Console On Build** in the Mod Settings window: + +![](/doc-img/en-mod-13.png) + +Please also refer to the [Modding](../modding/mod-sdk#custom-scripts) documentation. + +## Join the Discord + +If you are still having trouble, feel free to ask for help on our [Discord](https://discord.gg/warudo). We have a dedicated **#plugins-scripting** channel for anything related to scripting, and experienced folks there would be happy to help you diagnose the issue! + + diff --git a/docs/scripting/overview.md b/docs/scripting/overview.md index 1a5a99f..b2d7c3f 100644 --- a/docs/scripting/overview.md +++ b/docs/scripting/overview.md @@ -4,24 +4,65 @@ sidebar_position: 0 # Overview -:::caution +Warudo's scripting system allows you to extend Warudo's functionality by writing C# code that adds new [assets](../assets/overview) and [nodes](../blueprints/overview) to your VTubing setup. You can share your custom assets and nodes on the Steam workshop by creating a [plugin mod](plugin-mod). -This section is under development and is not completed. We are still working on it. -But you can check our examples in following pages: +Here are just a few wonderful plugins created by the Warudo community: -- [Custom Asset](custom-asset.md) -- [Custom Node](custom-node.md) -- [Custom Plugin](custom-plugin.md) +* [Feline's Headpat Node](https://steamcommunity.com/sharedfiles/filedetails/?id=3010238299&searchtext=) +* [Input Receiver and Tools for Mouse/Pen/Gamepad](https://steamcommunity.com/sharedfiles/filedetails/?id=3221461980&searchtext=) +* [Emotes From Twitch Message + Emote Dropper](https://steamcommunity.com/sharedfiles/filedetails/?id=3070622133&searchtext=) +* [Rainbow Color Node](https://steamcommunity.com/sharedfiles/filedetails/?id=3016521495&searchtext=) +* [Streamer.bot integration](https://steamcommunity.com/sharedfiles/filedetails/?id=3260939914&searchtext=) -Meanwhile, you can also check out our [GitHub Repo](https://github.com/HakuyaLabs/WarudoPlaygroundExamples/) and [Discord](https://discord.gg/warudo) for getting started with C# scripting in Warudo. +:::tip +**Warudo is built with custom scripting in mind.** In fact, the entirety of Warudo's features is built with the same APIs (the `Warudo.Core` namespace) that are available to you when creating custom nodes, assets, and plugins. +For example, the Stream Deck integration in Warudo is actually a built-in plugin! We have made its source code available [here](https://github.com/HakuyaLabs/WarudoPluginExamples) for your reference. ::: +To give you a taste of how Warudo's scripting system works, here is a simple example of a custom node that plays a random expression on a character when triggered: + +![](/doc-img/en-scripting-overview.png) + +And the corresponding C# code: + +```csharp +using UnityEngine; +using Warudo.Core.Attributes; +using Warudo.Core.Graphs; +using Warudo.Plugins.Core.Assets.Character; + +// Define a custom node type that will be shown in the note palette +[NodeType(Id = "95cd88ae-bebe-4dc0-b52b-ba94799f08e9", Title = "Character Play Random Expression")] +public class CharacterPlayRandomExpressionNode : Node { + + [DataInput] + public CharacterAsset Character; // Let the user select a character + + [FlowInput] + public Continuation Enter() { // When the node is triggered via the "Enter" flow input + if (Character.Expressions.Length == 0) return Exit; // If the character has no expressions, exit + + Character.ExitAllExpressions(); // Exit all current expressions + + var randomExpression = Character.Expressions[Random.Range(0, Character.Expressions.Length)]; + Character.EnterExpression(randomExpression.Name, transient: false); // Play a random expression + + return Exit; // Continue the flow and trigger whatever connected to the "Exit" flow output + } + + [FlowOutput] + public Continuation Exit; + +} +``` + +Intrigued? Read on! diff --git a/docs/scripting/playground.md b/docs/scripting/playground.md index 0aada45..89e9bd9 100644 --- a/docs/scripting/playground.md +++ b/docs/scripting/playground.md @@ -1,19 +1,44 @@ --- -sidebar_position: 10 +sidebar_position: 50 --- # Playground -:::warning +Playground is a development environment in Warudo that allows you to load custom scripts without creating a [plugin mod](plugin-mod). C# scripts (`.cs` files) put in the `Playground` folder will be automatically compiled and loaded by Warudo, and any changes to the scripts will also be automatically reloaded. -This page is still under development. +:::tip +Check out our [Creating Your First Script](creating-your-first-script) tutorial to get started with Playground! +::: + +You can put any C# scripts in Playground, but for Warudo to recognize them, at least one class must inherit one of the [entity types](api/entities.md), i.e., a `Node`, an `Asset`, or a `Plugin`. + +## Environment Setup + +You do not need an IDE to use Playground, but it is recommended to use one for code completion and syntax highlighting. We recommend [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). + +Then, download the `.csproj` file [here](/scripts/Playground.csproj) and put it into the `Playground` directory inside your Warudo data folder (Menu → Open Data Folder). Open the file with your IDE, and you are good to go! + +## Entity Hot-reloading +Hot-reloading works by serializing the entity data, unloading the old entity type, loading and instantiating the new entity type, and then deserializing the old entity data. This process usually takes only a few seconds. + +When hot-reloading completes, you should see a toast message indicating how many assets, nodes, or plugins have been reloaded. + +:::tip +If you see an error, you can click on the toast message to see the compilation error. ::: +## Limitations + +Playground has the following limitations: + +* You cannot (easily) use third-party DLLs or NuGet packages. We will improve this in the future. +* You cannot add or reference Unity assets (e.g., prefabs, materials, textures) in Playground scripts (use [plugin mods](plugin-mod) instead). + diff --git a/docs/scripting/plugin-mod.md b/docs/scripting/plugin-mod.md new file mode 100644 index 0000000..e29e6ed --- /dev/null +++ b/docs/scripting/plugin-mod.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 100 +--- + +# Plugin Mod + +A plugin mod is a type of [Warudo mod](../modding/mod-sdk) that adds custom assets and nodes to Warudo. + +:::tip +Check out our [Creating Your First Plugin Mod](creating-your-first-plugin-mod) tutorial to get started with creating a plugin mod! +::: + +:::info +This page discusses the general guidelines for creating a plugin mod. Please also refer to the [Plugins](api/plugins) scripting API page. +::: + +## Environment Setup + +A plugin mod is just a regular mod that contains a C# script that inherits from `Plugin`. You can create a new mod by following the [Creating Your First Mod](../modding/creating-your-first-mod) tutorial. Then, create a new C# script in the mod folder and inherit from `Plugin`. If you have any custom asset or node types, make sure they are registered in the `AssetTypes` and `NodeTypes` properties of the `[PluginType]` attribute (see [Plugin API](api/plugins)). + +## Including Unity Assets + +You can include Unity assets (e.g., prefabs, materials, textures) in your plugin mod by placing them in the mod folder. Warudo will automatically include these assets when exporting the mod. + +:::caution +If your prefabs or materials use custom shaders or scripts, make sure to include those shaders or scripts in the mod folder as well. +::: + +Then, you can load these assets in scripts during runtime. See [Loading Unity Assets](api/plugins#loading-unity-assets) for more information. + +## Limitations {#limitations} + +Plugin mods have the following limitations: + +- You cannot use third-party DLLs or NuGet packages. +- You cannot export C# scripts that are part of an assembly, i.e., you cannot have any `.asmdef` files in your mod folder. +- You cannot use reflection (`System.Reflection`) or access the file system (`System.IO`). + * To invoke methods on `MonoBehaviour` components, consider using Unity's [`SendMessage`](https://docs.unity3d.com/ScriptReference/GameObject.SendMessage.html) or [`BroadcastMessage`](https://docs.unity3d.com/ScriptReference/Component.BroadcastMessage.html) methods. + * To store and retrieve data, see [Saving and Loading Data](api/io). + + diff --git a/static/doc-img/en-custom-asset-1.png b/static/doc-img/en-custom-asset-1.png new file mode 100644 index 0000000..2bb9e7c Binary files /dev/null and b/static/doc-img/en-custom-asset-1.png differ diff --git a/static/doc-img/en-custom-asset-2.png b/static/doc-img/en-custom-asset-2.png new file mode 100644 index 0000000..be50520 Binary files /dev/null and b/static/doc-img/en-custom-asset-2.png differ diff --git a/static/doc-img/en-custom-node-1.png b/static/doc-img/en-custom-node-1.png new file mode 100644 index 0000000..46ab13a Binary files /dev/null and b/static/doc-img/en-custom-node-1.png differ diff --git a/static/doc-img/en-custom-plugin-1.png b/static/doc-img/en-custom-plugin-1.png new file mode 100644 index 0000000..deb2a57 Binary files /dev/null and b/static/doc-img/en-custom-plugin-1.png differ diff --git a/static/doc-img/en-custom-plugin-2.png b/static/doc-img/en-custom-plugin-2.png new file mode 100644 index 0000000..43e21e0 Binary files /dev/null and b/static/doc-img/en-custom-plugin-2.png differ diff --git a/static/doc-img/en-getting-started-playground-1.png b/static/doc-img/en-getting-started-playground-1.png new file mode 100644 index 0000000..7d7e47f Binary files /dev/null and b/static/doc-img/en-getting-started-playground-1.png differ diff --git a/static/doc-img/en-getting-started-playground-2.png b/static/doc-img/en-getting-started-playground-2.png new file mode 100644 index 0000000..066024c Binary files /dev/null and b/static/doc-img/en-getting-started-playground-2.png differ diff --git a/static/doc-img/en-getting-started-playground-3.png b/static/doc-img/en-getting-started-playground-3.png new file mode 100644 index 0000000..927396b Binary files /dev/null and b/static/doc-img/en-getting-started-playground-3.png differ diff --git a/static/doc-img/en-getting-started-playground-4.png b/static/doc-img/en-getting-started-playground-4.png new file mode 100644 index 0000000..7a31aa4 Binary files /dev/null and b/static/doc-img/en-getting-started-playground-4.png differ diff --git a/static/doc-img/en-getting-started-playground-5.png b/static/doc-img/en-getting-started-playground-5.png new file mode 100644 index 0000000..3fee552 Binary files /dev/null and b/static/doc-img/en-getting-started-playground-5.png differ diff --git a/static/doc-img/en-getting-started-playground-6.png b/static/doc-img/en-getting-started-playground-6.png new file mode 100644 index 0000000..4c3ab1c Binary files /dev/null and b/static/doc-img/en-getting-started-playground-6.png differ diff --git a/static/doc-img/en-getting-started-playground-7.png b/static/doc-img/en-getting-started-playground-7.png new file mode 100644 index 0000000..d0ab6dc Binary files /dev/null and b/static/doc-img/en-getting-started-playground-7.png differ diff --git a/static/doc-img/en-getting-started-playground-8.png b/static/doc-img/en-getting-started-playground-8.png new file mode 100644 index 0000000..614f9ef Binary files /dev/null and b/static/doc-img/en-getting-started-playground-8.png differ diff --git a/static/doc-img/en-plugin-mod-1.png b/static/doc-img/en-plugin-mod-1.png new file mode 100644 index 0000000..45c18b1 Binary files /dev/null and b/static/doc-img/en-plugin-mod-1.png differ diff --git a/static/doc-img/en-plugin-mod-2.png b/static/doc-img/en-plugin-mod-2.png new file mode 100644 index 0000000..09e3543 Binary files /dev/null and b/static/doc-img/en-plugin-mod-2.png differ diff --git a/static/doc-img/en-plugin-mod-3.png b/static/doc-img/en-plugin-mod-3.png new file mode 100644 index 0000000..9053e6c Binary files /dev/null and b/static/doc-img/en-plugin-mod-3.png differ diff --git a/static/doc-img/en-plugin-mod-4.png b/static/doc-img/en-plugin-mod-4.png new file mode 100644 index 0000000..ecbd00f Binary files /dev/null and b/static/doc-img/en-plugin-mod-4.png differ diff --git a/static/doc-img/en-plugin-mod-5.png b/static/doc-img/en-plugin-mod-5.png new file mode 100644 index 0000000..323cfdd Binary files /dev/null and b/static/doc-img/en-plugin-mod-5.png differ diff --git a/static/doc-img/en-plugin-mod-6.png b/static/doc-img/en-plugin-mod-6.png new file mode 100644 index 0000000..0910369 Binary files /dev/null and b/static/doc-img/en-plugin-mod-6.png differ diff --git a/static/doc-img/en-plugin-mod-7.png b/static/doc-img/en-plugin-mod-7.png new file mode 100644 index 0000000..d3ba663 Binary files /dev/null and b/static/doc-img/en-plugin-mod-7.png differ diff --git a/static/doc-img/en-plugin-mod-8.png b/static/doc-img/en-plugin-mod-8.png new file mode 100644 index 0000000..08e192f Binary files /dev/null and b/static/doc-img/en-plugin-mod-8.png differ diff --git a/static/doc-img/en-resource-providers-1.png b/static/doc-img/en-resource-providers-1.png new file mode 100644 index 0000000..c1993c1 Binary files /dev/null and b/static/doc-img/en-resource-providers-1.png differ diff --git a/static/doc-img/en-resource-providers-2.png b/static/doc-img/en-resource-providers-2.png new file mode 100644 index 0000000..29331fe Binary files /dev/null and b/static/doc-img/en-resource-providers-2.png differ diff --git a/static/doc-img/en-scripting-concepts-1.png b/static/doc-img/en-scripting-concepts-1.png new file mode 100644 index 0000000..15ea65c Binary files /dev/null and b/static/doc-img/en-scripting-concepts-1.png differ diff --git a/static/doc-img/en-scripting-concepts-2.png b/static/doc-img/en-scripting-concepts-2.png new file mode 100644 index 0000000..0b53e9a Binary files /dev/null and b/static/doc-img/en-scripting-concepts-2.png differ diff --git a/static/doc-img/en-scripting-concepts-3.png b/static/doc-img/en-scripting-concepts-3.png new file mode 100644 index 0000000..f23313f Binary files /dev/null and b/static/doc-img/en-scripting-concepts-3.png differ diff --git a/static/doc-img/en-scripting-concepts-4.png b/static/doc-img/en-scripting-concepts-4.png new file mode 100644 index 0000000..f4bd6dc Binary files /dev/null and b/static/doc-img/en-scripting-concepts-4.png differ diff --git a/static/doc-img/en-scripting-concepts-5.png b/static/doc-img/en-scripting-concepts-5.png new file mode 100644 index 0000000..6a247c5 Binary files /dev/null and b/static/doc-img/en-scripting-concepts-5.png differ diff --git a/static/doc-img/en-scripting-overview.png b/static/doc-img/en-scripting-overview.png new file mode 100644 index 0000000..465be7f Binary files /dev/null and b/static/doc-img/en-scripting-overview.png differ diff --git a/static/doc-img/en-structured-data-1.png b/static/doc-img/en-structured-data-1.png new file mode 100644 index 0000000..0e2b810 Binary files /dev/null and b/static/doc-img/en-structured-data-1.png differ diff --git a/static/doc-img/en-structured-data-2.png b/static/doc-img/en-structured-data-2.png new file mode 100644 index 0000000..f8a7e00 Binary files /dev/null and b/static/doc-img/en-structured-data-2.png differ diff --git a/static/doc-img/en-structured-data-3.png b/static/doc-img/en-structured-data-3.png new file mode 100644 index 0000000..2b757ce Binary files /dev/null and b/static/doc-img/en-structured-data-3.png differ diff --git a/static/doc-img/en-structured-data-4.png b/static/doc-img/en-structured-data-4.png new file mode 100644 index 0000000..a8d8a2d Binary files /dev/null and b/static/doc-img/en-structured-data-4.png differ diff --git a/static/doc-img/en-structured-data-5.png b/static/doc-img/en-structured-data-5.png new file mode 100644 index 0000000..afa56ab Binary files /dev/null and b/static/doc-img/en-structured-data-5.png differ diff --git a/static/scripts/Playground.csproj b/static/scripts/Playground.csproj new file mode 100644 index 0000000..6df721a --- /dev/null +++ b/static/scripts/Playground.csproj @@ -0,0 +1,137 @@ + + + + Library + + net5.0 + 9.0 + + + + + ../../Managed/Assembly-CSharp.dll + + + ../../Managed/UnityEngine.dll + + + ../../Managed/UnityEngine.CoreModule.dll + + + ../../Managed/UnityEngine.AnimationModule.dll + + + ../../Managed/UnityEngine.AudioModule.dll + + + ../../Managed/UnityEngine.InputModule.dll + + + ../../Managed/UnityEngine.PhysicsModule.dll + + + ../../Managed/UnityEngine.TextRenderingModule.dll + + + ../../Managed/UnityEngine.UI.dll + + + ../../Managed/UnityEngine.UIModule.dll + + + ../../Managed/UnityEngine.XRModule.dll + + + ../../Managed/UnityEngine.UnityWebRequestModule.dll + + + ../../Managed/UnityEngine.UnityWebRequestAudioModule.dll + + + ../../Managed/UnityEngine.UnityWebRequestTextureModule.dll + + + ../../Managed/UnityEngine.UnityWebRequestWWWModule.dll + + + ../../Managed/Unity.Collections.dll + + + ../../Managed/Unity.InputSystem.dll + + + ../../Managed/Unity.Jobs.dll + + + ../../Managed/Unity.Mathematics.dll + + + ../../Managed/Unity.TextMeshPro.dll + + + ../../Managed/Cinemachine.dll + + + ../../Managed/Warudo.Core.dll + + + ../../Managed/Warudo.Plugins.Core.dll + + + ../../Managed/UniTask.dll + + + ../../Managed/UniTask.Linq.dll + + + ../../Managed/DOTween.dll + + + ../../Managed/RootMotion.dll + + + ../../Managed/websocket-sharp.dll + + + ../../Managed/Newtonsoft.Json.dll + + + ../../Managed/MessagePack.dll + + + ../../Managed/Animancer.dll + + + ../../Managed/ALINE.dll + + + ../../Managed/uOSC.Runtime.dll + + + ../../Managed/VRM.dll + + + ../../Managed/UniGLTF.dll + + + ../../Managed/UniHumanoid.dll + + + ../../Managed/VRM10.dll + + + ../../Managed/DynamicBone.dll + + + ../../Managed/MagicaCloth.dll + + + ../../Managed/MagicaClothV2.dll + + +