Warudo Mod 开发不完全指北

立音喵
立音喵
2023年08月07日


Warudo 有 Mod 系统,却没有 Mod 教程,这怎么行?

当然本文也只是记录我所知道的,最最简单的 Mod 教程,在等待官方文档的时间中,先用这个当作代餐吧。

Mod 模式

Warudo 支持2种 Mod 代码插入模式,使用 Unity 的完整 Plugins 和使用 Playground 的当场编译。

Playground 模式

Playground 模式的优势在于即时编译,只需要在 Warudo 的数据目录中找到 Playground 文件夹,在里面新建任何 .cs 后缀的 C# 文件即可触发编译,任何修改都可以在运行时被装入,非常方便。并且在 Playground 模式下,可以使用 C# 10 的语法特性,对于一些操作来说会更加方便。

同时在使用 Playground 时,无需声明插件本身和定义资源及节点,即写即用。

但在 Playground 中,无法获得语法提示,由于 Playground 的动态注册特性,删除 Playground 中的节点或资源需要重新加载 Warudo 才能删除。

Unity Mod

Unity Mod 相对来说就是更加 “正统” 的方法了。Warudo 支持通过内置的 uMod 提供较为完整的 Mod 开发支持。并且在开发中拥有完整的代码提示,可以打包为 .warudo 格式的文件并发布到创意工坊供他人使用。这种 Mod 模式也可以为 Mod 添加 i18n 语言文件,支持多语言翻译。

但由于 Unity 版本的限制,开发中只能使用 C# 09 的语法。并且每次修改后必须通过 Build Mod 将代码打包为 .warudo 文件并将其复制进 Warudo 数据目录下的 Plugins 文件夹中,Warudo 会在运行时即时重载。

一般来说,我自己则会同时开着两边,在 Unity Mod 里写好一部分之后,将代码复制到 Playground 中测试,避免一遍又一遍的编译整个 Mod (Unity Editor 太卡啦!)。

Mod 基础

准备

既然要写 Mod 了,那就要准备好 Unity Hub 和 Unity 账号。到 这里 下载安装好 Unity Hub。

注意啦!注意啦!(敲桌子)不要安装最新版本的 Unity Editor!如果你只是给 Warudo 写 Mod 的话,不要安装最新版本的 Unity Editor!Unity 即使小版本之间也不保证兼容性

打开 Warudo 文档页面 的英文版本(截至此文发稿时,中文文档页面尚未更新最新 SDK)。

下载 Warudo SDK 0.10.0 Mod Project.zip 工程模板,并使用 Unity Editor 的打开功能,打开这个模板工程。在打开过程中,Unity Editor 应当会询问你是否需要安装 Unity 2021.3.18f1 版本(截止本文发稿时,Warudo 使用此版本的 Unity),此时选择是,让其自动安装正确的版本。

安装后,就可以顺利打开工程模板了。首次打开模板工程可能需要非常非常长的时间,保持网络畅通,等待约5分钟即可完成加载(当然网络太慢可能更久啦)。

示例文件

Warudo 官方提供了示例文件,在这里

如果你懒得看,后面我也会逐个进行介绍。

当然,最好的办法是加入 Warudo 的 Discord 服务器,里面有 Warudo 的开发者和各路大佬可以解答问题。

开始编写 Mod

打开示例工程后,你应该看到一个空零零的场景。

找到菜单栏上的 Warudo 菜单,选择 New Mod

Unity-1

这会打开一个创建界面,在 Mod Name 框中输入你想要给你的 Mod 起什么好名字。比如这里,我们来开发 SpawnArea,一个在 Warudo 世界中框选一个位置,作为“观众席” 的功能,这样我们就可以进行更好的观众互动啦!这个 Mod 也会传到创意工坊哦~

Unity-2

这里会在 Assets 文件夹下创建名为 SpawnArea 的文件夹,这就是 Mod 的根文件夹,所有在此文件夹中的代码都将视作 Mod 代码。

国际化语言包

我选择先告诉你这件事,因为这可以避免你在写完整个 Mod 之后再去找如何支持语言翻译。

Assets/(你的 Mod 名称) 文件夹中,新建一个名为 Localizations 的文件夹,这个文件夹用来放置语言文件,在这个文件夹中的 json 文件将会作为语言文件被加载到 Warudo 中。

创建两个 json 文件,分别命名为 (你的 Mod 名称).json(你的 Mod 名称).zh_CN.json。如这里我将其命名为 SpawnArea.jsonSpawnArea.zh_CN.json

在这个文件中,应当有如下内容

{
    "en": { }
}

或者

{
    "zh_CN": { }
}

而在 en 或者 zh_CN 对象的子级上,使用 "语言键": "翻译值" 来提供语言翻译,如

{
    "en": { 
      "SPAWN_AREA_PLUGIN_NAME": "SpawnArea"
    }
}

{
    "zh_CN": { 
      "SPAWN_AREA_PLUGIN_NAME": "刷新区域(观众区域)"
    }
}

当你在编写 Warudo Mod 时,任何传入字符串的地方传入对应的 语言键 就会自动被 Warudo 替换为对应语言的文本。

Plugin

作为一个 Mod 首先就是要创建 Mod 的主入口。在 Mod 的主文件夹中创建 SpawnArea.cs 文件,双击打开编辑器。

VS-1

这里面默认创建的代码对于 Mod 来说并没有什么用,来先清除它们,然后放入以下代码。

using Warudo.Core.Attributes;
using Warudo.Core.Plugins;

namespace liyin
{
    [PluginType(
        Id = "liyin.spawnarea.plugin",
        Name = "SPAWN_AREA_PLUGIN_NAME",
        Description = "SPAWN_AREA_PLUGIN_DESCRIPTION",
        Author = "LiYin",
        Version = "1.0"
    )]
    public class SpawnAreaPlugin : Plugin
    {
        
    }
}

首先,创建一个自己的 namespace 避免发生冲突,然后给你的 Mod 起一个不会冲突的 Id,比如 你的用户名.Mod名.plugin,然后写上 Mod 的名称和说明,以及作者信息,版本等。

一会儿我们还会回到这里,在 PluginType 中添加内容来注册我们后续编写的资源和节点。

Asset

我们来认识第一种 Mod 能力 —— Asset 资源。

Asset 也就是 Warudo 控制面板上对应 “角色” “场景” “摄像机” 的内容。

创建 Asset 将会让你的 Mod 内容作为资源出现在资源列表中。

这里我们创建一个叫做 SpawnAreaAsset 的资源,用于选中观众的刷新位置。

using Warudo.Core.Attributes;
using Warudo.Core.Scenes;

namespace liyin
{
    [AssetType(
        Id = "liyin.spawnarea.spawnareaasset",
        Category = "CATEGORY_SPAWNAREA",
        Title = "SPAWN_AREA_ASSET_TITLE_AREA"
    )]
    public class SpawnAreaAsset: Asset
    {

    }
}

这里与 Plugin 很类似,我们创建一个继承于 Asset 的类,并将其设置为 AssetType,这可以告诉 Warudo 这是一个什么样的资源。

好了,首先我们为这个节点创建一些基础的内容,让用户可以填写的内容。

首先作为一个可以被开关的节点,我们需要一个开关,并且为了可以框选区域,我们也需要一个输入位置,旋转,大小的“变换”节点。

实际上 Warudo 对一些类型和其数组提供了自动生成的界面,这里作为开关,使用 bool 类型,而作为变换,使用 Warudo 内置的 TransformData 类型。

那么我们可以看下面这一段代码。

namespace liyin
{
    [AssetType(
        Id = "liyin.spawnarea.spawnareaasset",
        Category = "CATEGORY_SPAWNAREA",
        Title = "SPAWN_AREA_ASSET_TITLE_AREA"
    )]
    public class SpawnAreaAsset: Asset
    {
        enum SpawnPolicy {
            [Label("SPAWNAREA_POLICY_MOST_INACTIVE")]
            REMOVE_MOST_INACTIVE,
            [Label("SPAWNAREA_POLICY_OLDEST")]
            REMOVE_OLDEST,
            [Label("SPAWNAREA_POLICY_DENIED_NEW")]
            DENIED_NEW
        }

        [DataInput]
        [Label("ENABLED")]
        bool Enabled = false;

        [DataInput]
        [Label("TRANSFORM")]
        TransformData Transform;

        private bool EditingArea = true;

        [Trigger]
        [Label("SPAWNAREA_SHOW_AREA")]
        [HiddenIf(nameof(HideStartEditingArea))]
        private void StartEditingCurve() => this.EditingArea = true;

        [Trigger]
        [Label("SPAWNAREA_HIDE_AREA")]
        [HiddenIf(nameof(HideStopEditingArea))]
        private void StopEditingArea() => this.EditingArea = false;
        private bool HideStartEditingArea() => this.EditingArea;
        private bool HideStopEditingArea() => !this.EditingArea;

        [Section("SPAWNAREA_SETTING")]
        [DataInput]
        [Label("SPAWNAREA_MAXIMUM_SPAWN_ASSET")]
        [IntegerSlider(1, 5000, 1)]
        int Maximum = 1;

        [DataInput]
        [Label("SPAWNAREA_POLICY")]
        SpawnPolicy Policy = SpawnPolicy.REMOVE_MOST_INACTIVE;
    }
}

作为要让用户输入的内容,Warudo Mod SDK 提供了非常方便的 Type 定义,对于支持的类型,只需要在对应变量上面加上 [DataInput] 即可显示对应的输入框。目前已知的输入支持有 TransformData / Vector3 / int / float / bool / string / enum 以及所有自定义 Asset。

对于整数类型,还可以额外使用 [IntegerSlider(最小值,最大值,步长)] 来定义一个限制拉杆,同样的对于浮点数类型可以使用 [FloatSlider(最小值,最大值,步长)]

而布尔型则会自动生成为开关。

还可以使用 [Trigger] 来标明一个 void 类型、无入参的函数。这会让 Warudo 生成一个按钮。

[HiddenIf] 则提供了按需切换对应变量的控件显示和隐藏的功能。

[Label("字符串")] 对变量提供了说明,如果一个变量不包含 Label 则会自动使用它的变量名。

Warudo-1

作为一个 Asset 它具有 OnCreateOnUpdate 方法。OnCreate 方法在这个 Asset 被创建时调用,而 OnUpdate 在 Unity 的绘图生命周期中进行更新。(不要在 OnUpdate 中使用重量级操作!)

我们想要当点击显示区域时在输出上绘制一个区域,那么我们可以这么做。

public override void OnUpdate()
{
    base.OnUpdate();
    if (Enabled && !this.Active)
    {
        this.SetActive(true);
    }
    else if (!Enabled && this.Active)
    {
        this.SetActive(false);
    }
    if (this.EditingArea && !this.IsSelected) // 先判断自己是不是处于显示区域模式或者被选中了,如果没有选中但启动了区域显示,则自动关闭它
        this.EditingArea = false;
    if (!this.EditingArea) // 如果没有在显示 结束
        return;
    CommandBuilder builder = DrawingManager.GetBuilder(true);
    builder.cameraTargets = new Camera[1]
    {
        Context.PluginManager.GetPlugin<CorePlugin>().MainCamera
    };
    Color colorPointX = new Color(1.0f, 0.0f, 0.0f);
    Color colorPointY = new Color(0.0f, 1.0f, 0.0f);
    Color colorPointZ = new Color(0.0f, 0.0f, 1.0f);
    Color colorArea = new Color(0.2f, 0.2f, 0.8f);
    // 绘制中心的坐标显示线
    {
        Vector3 x = new Vector3(0.1f + 0.1f * Transform.Scale.x, 0, 0);
        x = Transform.RotationQuaternion * x;
        x = x + Transform.Position;
        builder.Arrow(Transform.Position, x, colorPointX);

        Vector3 y = new Vector3(0, 0.1f + 0.1f * Transform.Scale.y, 0);
        y = Transform.RotationQuaternion * y;
        y = y + Transform.Position;
        builder.Arrow(Transform.Position, y, colorPointY);

        Vector3 z = new Vector3(0,0, 0.1f + 0.1f * Transform.Scale.z);
        z = Transform.RotationQuaternion * z;
        z = z + Transform.Position;
        builder.Arrow(Transform.Position, z, colorPointZ);
    }
    // 绘制立方体
    {
        builder.WireBox(Transform.Position, Transform.RotationQuaternion, Transform.Scale, colorArea);
    }
    builder.Dispose();
}

Warudo-2

OnCreate 则可以创建一些有用的监听。

protected override void OnCreate()
{
    base.OnCreate();
    Watch(nameof(Enabled), () => {
        if (!Enabled)
        {
            ClearAllSpawned(); // 当切换开关时,如果是关闭状态删除所有创建的资源(因为这些资源不受 Warudo 自动管理)
        }
    });
}

使用 Watch 可以轻松的在一个变量被改变时获得回调,这里则是在关闭 SpawnArea 的总开关时,清除所有生成的资源。

Node

只有资源肯定是不够的,作为 Warudo 最强大的功能之一,蓝图系统也是必不可少的。

使用 Node 类可以创建新的蓝图节点,让 Warudo 的功能更进一步。

这里我也创建一个节点,用于在 SpawnArea 中添加一个角色。

[NodeType(
    Id = "liyin.SpawnArea.SpawnAreaNodeSpawnNode",
    Category = "CATEGORY_SPAWNAREA",
    Title = "SPAWN_AREA_NODE_SPAWN_TITLE"
)]
public class SpawnAreaSpawnNode : Node
{
    [DataInput]
    private SpawnAreaAsset Asset;

    [DataInput]
    [Label("SOURCE")]
    [AutoComplete("GetSources", true, "")]
    public string Source;
    async UniTask<AutoCompleteList> GetSources() => Context.ResourceManager.ProvideResources("Character").ToAutoCompleteList();

    [FlowOutput]
    public Continuation Exit;
    public string DefaultIdleAnimation = "character-animation://resources/Animations/AGIA/01_Idles/AGIA_Idle_generic_01";

    [DataInput]
    [Label("UID")]
    public string UID;

    private SpawnedCharacter _SpawnedCharacter;

    [DataOutput]
    [Label("SPAWNAREA_SPAWNED_CHARACTER")]
    public SpawnedCharacter spawnedCharacter() {
        return _SpawnedCharacter;
    }

    [FlowInput]
    public Continuation Enter() {
      string uid = UID.Trim();
        if (Asset != null && UID != null && uid != "" && Asset.Active) {
            try
            {
                GameObject gameObject = Context.ResourceManager.ResolveResourceUri<GameObject>(Source);
                spawnedCharacter.UID = uid;
                spawnedCharacter.gameObject = gameObject;
                spawnedCharacter.animancer = gameObject.AddComponent<AnimancerComponent>();
                AnimationClip clip = Context.ResourceManager.ResolveResourceUri<AnimationClip>(DefaultIdleAnimation);
                AnimancerState state = gameObject.GetComponent<AnimancerComponent>().Layers[0].Play(clip, 0.6f);
                Asset.CharacterAssets.Add(uid, spawnedCharacter);
                _SpawnedCharacter = spawnedCharacter;
                return Exit;
            }
            catch (Exception ex)
            {
                if (Context.Service != null)
                    Context.Service.PromptMessage("AN_ERROR_HAS_OCCURRED", ex.ToString(), false);
                return null;
            }
        }
        return null;
    }
}

这个蓝图节点较为全面的包含了一个节点的写法。

Warudo Mod SDK 提供了非常一致性的 Mod API,同样使用 [DataInput] 就可以和 Asset 一样添加输入,而作为蓝图节点,[DataOutput] 数据输出也必不可少。

作为节点化编程系统,控制流则是使用 [FlowInput][FlowOutput] 定义,在 [FlowInput] 所定义的入口中返回一个由 [FlowOutput] 修饰的出口,这样会自动的调用后续控制流。

如果你想要手动触发控制流则可以使用 InvokeFlow(Exit); 这种方式调用后续控制流。

当然 OnCreateOnUpdate 同样可以在 Node 中使用。

Warudo-3

注册到 Mod 上

现在,到了回到 Plugin 上

[PluginType(
    Id = "liyin.SpawnArea.Plugin",
    Name = "SPAWN_AREA_PLUGIN_NAME",
    Description = "SPAWN_AREA_PLUGIN_DESCRIPTION",
    Author = "LiYin",
    Version = "1.0",
    NodeTypes = new[] { typeof(SpawnAreaSpawnNode) },
    AssetTypes = new[] { typeof(SpawnAreaAsset) }
)]

在 PluginType 上对应添加 Asset 和 Node 的列表,即可完成注册。

其他杂项

ResourceManager

在 Warudo 中,资源使用 URI 进行标记,ResourceManager 可以解析这些资源 URI 并返回对应的结果。

T type = Context.ResourceManager.ResolveResourceUri<T>(SourceUri);

OpenedScene

Context.OpenedScene 可以对当前场景进行一些操作,如 Context.OpenedScene.AddAsset<T> 可以创建一个新的资源,相类似的还有 AddGraph 等方法操作蓝图。

要注意,当你编辑之后需要调用 Service 服务中的 Context.Service.BroadcastOpenedScene() 进行通知 Warudo 刷新界面。

Service

Service 中含有一些方便的显示方法。

如调用提示框的方法 Context.Service.PromptMessage("ERROR", "MESSAGE"); 和类似的调用询问框的方法。

导航到目标资源的方法 Context.Service.NavigateToAsset(asset.Id);

通知 Warudo 控制台更新界面的 Context.Service.BroadcastOpenedScene();

打包

回到 Unity Editor 界面上,在菜单上选择 Warudo - Build Mod 即可完成 .warudo 文件的打包。

结语

在这片短文写完的时候,SpawnArea 还没有上线,而且大概率是因为我还没有写完,它所需要的功能比我想象的更多。但希望这篇短文能让你开始 Warudo 的 Mod 之旅,开放性的 Mod 让 Warudo 变得越来越强大。

评论区
Made with ♥ by LiYin
Yin Theme V2