自定义命令
命令代表控制场景中发生的事情的单个操作;例如,它可以更改背景、移动角色或加载另一个剧本脚本。剧本脚本 中定义的参数化命令序列实际上控制着游戏流程。您可以在 API 参考 中找到可用的内置命令。在代码中,所有内置脚本命令实现都在 Naninovel.Commands 命名空间下定义。
添加自定义命令
要添加您自己的自定义脚本命令,请创建一个派生自 Command 的新 C# 类并重写 Execute 方法。创建的类将自动被引擎识别,您可以通过类名或别名(如果已分配)从剧本脚本调用该命令。要为 Naninovel 命令分配别名,请将 Alias 特性应用于类。
下面是一个自定义命令的示例,可以在剧本脚本中通过 @HelloWorld 或 @hello 调用,将“你好,世界!”打印到控制台,并且还可以接受可选的 name 参数(例如 @hello name:Felix),以问候提供的名字而不是世界。
using System;
using Naninovel;
using Naninovel.Commands;
using UnityEngine;
[Serializable, Alias("hello")]
public class HelloWorld : Command
{
public StringParameter Name;
public override Awaitable Execute (ExecutionContext ctx)
{
if (Assigned(Name)) Debug.Log($"Hello, {Name}!");
else Debug.Log("你好,世界!");
return Async.Completed;
}
}注意
每当您更改 C# 命令实现时(例如重命名类、添加或删除参数、更改其类型或特性),请记住重新导入剧本脚本资产(右键单击存储脚本的文件夹,然后单击“Reimport”)。这是必要的,因为剧本脚本是在导入时(而不是在运行时)解析和编译的,并且必须与 C# 实现保持同步。
Execute 方法
Execute 是脚本播放器执行命令时调用的异步方法;请将您的命令逻辑放在其中。使用 引擎服务 访问引擎的内置系统。剧本脚本执行将暂停,直到此方法返回的 Awaitable 完成;要使等待成为可选,请像内置命令那样声明一个布尔类型的 Wait 参数,并使用 WaitOrForget 方法包装异步逻辑。
执行上下文
请注意提供给 Execute 方法的 ExecutionContext ctx 参数。在执行 异步操作 时,请务必在每个异步操作后检查 ctx.Token 异步令牌是否有取消和完成请求,并做出相应反应:
AsyncToken.Canceled表示引擎已被销毁或重置。在这两种情况下,使用引擎 API 都不再安全,任何状态修改都会导致未定义的行为。取消时,命令实现应立即抛出AsyncOperationCanceledException,并放弃当前正在执行的所有活动。AsyncToken.Completed表示命令应尽可能快地完成所有活动。例如,如果您正在运行动画,请立即完成它们,无论其预期持续时间如何。这通常发生在玩家激活Continue输入或存档操作开始时。
public override async Awaitable Execute (ExecutionContext ctx)
{
await PerformSomething();
// 引擎可能在上述方法运行时已被销毁;
// 如果是这种情况,以下检查将抛出异常。
ctx.Token.ThrowIfCanceled();
// 检查后继续使用引擎 API 是安全的。
var someUI = Engine.GetService<IUIManager>().GetUI<SomeUI>();
// 如果收到完成请求,请立即淡出 UI。
var fadeDuration = ctx.Token.Completed ? 0 : 5;
await someUI.ChangeVisibility(false, fadeDuration, ctx.Token);
// 上述方法接受异步令牌;此类方法在内部处理
// 取消,因此之后无需再次检查。
}执行上下文的另一个成员是执行命令的脚本轨道实例,可通过 ctx.Track 访问。每当需要控制播放或调用其他需要轨道的引擎 API 时,请使用轨道实例。例如,像这样停止播放:
public override Awaitable Execute (ExecutionContext ctx)
{
ctx.Track.Stop();
return Async.Completed;
}参数类型
要将命令参数公开给剧本脚本,请向命令类添加具有以下支持类型之一的公共字段:
| 字段类型 | 值类型 | 脚本示例 |
|---|---|---|
| StringParameter | String | LoremIpsum, "Lorem ipsum" |
| LocalizableTextParameter | LocalizableText | "Lorem ipsum|#id|" |
| NumberParameter | Single | 10, 0.525, -55.1 |
| BooleanParameter | Boolean | true, false |
| NamedStringParameter | NamedString | Script001.LabelName, .LabelName |
| NamedNumberParameter | NamedNumber | Yuko.5, Kohaku.-10.25 |
| NamedBooleanParameter | NamedBoolean | Misaki.false |
| StringListParameter | List<String> | Lorem,ipsum,"dolor sit amet" |
| NumberListParameter | List<Single> | 10,-1,0.105 |
| BooleanListParameter | List<Boolean> | true,false,true |
| NamedStringListParameter | List<NamedString> | Felix.Happy,Jenna.Confidence |
| NamedNumberListParameter | List<NamedNumber> | Yuko.5,Misaki.-8 |
| NamedBooleanListParameter | List<NamedBoolean> | Misaki.false,Kohaku.true |
参数别名
您还可以将 [Alias] 特性应用于字段,为参数分配别名,以便在剧本脚本中引用该参数时用别名代替字段名称。
[Alias("myParam")]
public StringParameter MyParameter;@cmd myParam: value of 'MyParameter' param主参数
应用 [PrimaryParameter] 特性后,可在不提供参数标识符的情况下指定该参数。每个命令只允许一个主参数。
[PrimaryParameter]
public StringParameter MyPrimaryParameter;@cmd value of the primary param必需参数
要使参数成为必需参数(当剧本脚本中未指定时会导致记录错误),请将 [RequiredParameter] 特性应用于字段。未应用该特性时,参数被视为可选参数。
[RequiredParameter]
public StringParameter MyRequiredParameter;可选参数
当参数不是必需时,它在剧本脚本中可能已赋值,也可能没有;使用 HasValue 属性来测试是否是这种情况。或者,您可以使用 Assigned() 静态方法,该方法接受参数实例,并在提供的参数不为 null 且已赋值时返回 true。
public StringParameter MyOptionalParameter;
...
if (MyOptionalParameter.HasValue) { }
if (Assigned(MyOptionalParameter)) { }可本地化命令
如果命令具有可以本地化的参数(直接呈现给玩家的文本),请实现 Command.ILocalizable 接口以将命令添加到生成的 脚本本地化 文档中,并使用 LocalizableTextParameter 参数类型。
public class PrintText : Command, Command.ILocalizable
{
public LocalizableTextParameter Text;
}可预加载命令
如果命令执行需要加载一些资源,请实现 Command.IPreloadable 接口以在游戏加载时预加载所需的资源。有关更多信息,请参阅 内存管理 指南。
public class PlayAudioClip : Command, Command.IPreloadable
{
public StringParameter ClipPath;
public async Awaitable PreloadResources (ScriptPlaylist playlist)
{
if (!Assigned(ClipPath) || ClipPath.DynamicValue) return;
await Audio.SfxLoader.Load(ClipPath, this);
}
public void ReleaseResources (ScriptPlaylist playlist)
{
if (!Assigned(ClipPath) || ClipPath.DynamicValue) return;
Audio.SfxLoader.Release(ClipPath, this);
}
}注意 ClipPath.DynamicValue 检查:如果仅在执行命令时才知道名称(即参数包含 剧本表达式),我们将无法预加载资源;在这种情况下,应在 Execute 方法内加载资源。
命令示例
您可以在 Naninovel/Runtime/Commands 包文件夹中找到包含所有内置命令实现的 C# 脚本;在实现您自己的自定义命令时,可以随意将它们用作参考。
示例
库存示例 还演示了如何添加自定义命令,用于添加/移除库存系统中的物品。命令实现位于 Scripts/Runtime/Inventory/Commands 目录下。
覆盖内置命令
在某些情况下,覆盖内置 Naninovel 命令可能很有用。例如,您可能希望更改 @print 命令的工作方式而不添加自定义命令,以便更改也会影响 通用文本行(通用行的文本在底层被解析为打印命令)。
要覆盖内置命令,请添加一个自定义命令并应用与该内置命令相同的别名。覆盖命令后重新导入剧本脚本(右键单击存储它们的文件夹,然后单击“Reimport”)以使更改生效。然后,在播放剧本脚本时将自动使用自定义命令而不是内置命令。
下面的示例覆盖了内置 @print 命令,在向玩家显示文本之前,先将文本记录到控制台。
[Serializable, Alias("print")]
public class MyCustomPrintCommand : PrintText
{
public override Awaitable Execute (ExecutionContext ctx)
{
Debug.Log(Text);
return base.Execute(ctx);
}
}提示
命令和参数可以应用各种上下文特性,以便在 IDE 和故事编辑器中提供文档、自动补全和高级诊断。可用特性请参阅 IDE 扩展 指南。