剧本方法(method())让你的扩展暴露一组可在剧本里调用的逻辑。创作者在剧本里通过「调用扩展方法」action block 选中你的方法、填好参数,剧本执行到那里时引擎就会调用你定义的 run。
声明方法
在 Extension 子类上用 static 方法名 = method({ ... }) 声明:
import { Extension, extension, method } from "@avg-studio/sdk";
@extension({ id: "rewards", label: "奖励系统" })
export class RewardSystem extends Extension {
static giveGold = method({
title: "发放金币",
description: "给玩家加金币并弹出提示",
schema: {
amount: { type: "number", label: "金币数", default: 100 },
},
run(ctx, params) {
const cur = ctx.variables.get<number>("gold") ?? 0;
ctx.variables.set("gold", cur + params.amount);
},
});
}
一个类可以声明多个方法。声明好后,创作者在剧本里按 Tab 选「调用扩展方法」,就能在选择器里找到「奖励系统 → 发放金币」并填参数。
method() 字段
| 字段 | 必填 | 说明 |
|---|
title | 是 | 方法选择器里的显示名 |
description | 否 | 简要说明 |
id | 否 | kebab-case 方法 id,缺省时从属性名 kebabize 推导(addItem → "add-item") |
schema | 否 | 参数定义,Studio 自动生成填参表单 |
run | 是 | 执行体,剧本走到这里时调用 |
runImmediately | 否 | 立即生效版本,详见下文 |
skip | 否 | 玩家快进时的简化行为,详见下文 |
参数 schema
schema 定义方法接受的参数。Studio 根据 schema 在「调用扩展方法」action block 的属性面板里自动生成对应的编辑控件:
| 类型 | 编辑器控件 | 字段配置 |
|---|
string | 文本输入框 | 可配 multiline default required suggestions |
number | 数字输入框 | 可配 min max step default required |
boolean | 开关 | 可配 default required |
enum | 下拉选择 | options: [{ label, value }] 必填,可配 default |
asset | 素材选择器 | 通过 assetType 限定:"image" "audio" "video" "any" |
character | 角色选择器 | 选项来自项目里的角色 |
characterPortrait | 立绘选择器 | 值为角色的立绘 ID;可配 characterField 关联同 schema 里的角色字段并筛选立绘 |
scene | 场景选择器 | 选项来自项目里的场景 |
fragment | Fragment 选择器 | 跳转目标用 |
variable | 变量选择器 | 选已声明的变量 |
uiExtension | UI 扩展选择器 | 选已注册的扩展模块 |
这套 schema 类型只用在 method().schema。不要和 static settings = settings((s) => ({ ... })) 的 builder API 混淆——那是另一套 fluent 接口,类型集也不同(详见 设置 Schema)。
static playEffect = method({
title: "播放特效",
schema: {
targetCharacter: {
type: "character",
label: "目标角色",
required: true,
},
effectType: {
type: "enum",
label: "效果类型",
options: [
{ label: "闪烁", value: "blink" },
{ label: "抖动", value: "shake" },
{ label: "渐隐", value: "fade" },
],
default: "shake",
},
backgroundImage: {
type: "asset",
label: "可选背景图",
assetType: "image",
},
},
run(ctx, params) {
// params.targetCharacter: string
// params.effectType: "blink" | "shake" | "fade"
// params.backgroundImage: string
},
});
enum 字段的 params 类型会被推导成选项值的字面量联合,其他字段(含 asset、character、scene 等)的 params 类型都是 string。
文本参数候选值
单行 string 参数可以声明 suggestions,让检查器显示「可自由输入 + 候选下拉」控件。同一扩展中使用相同 key 的参数会共享候选池,Studio 会从当前项目剧本里收集这些参数已经填写过的值。
static sendMessage = method({
title: "发送消息",
schema: {
contact: {
type: "string",
label: "联系人",
required: true,
suggestions: {
key: "contact",
includeCharacterNames: true,
},
},
content: {
type: "string",
label: "内容",
multiline: true,
},
},
run(ctx, params) {
// params.contact 仍是普通 string
},
});
| 字段 | 说明 |
|---|
key | 扩展内的候选池名称。多个方法使用相同 key 时会复用彼此的历史值 |
includeCharacterNames | 设为 true 后,把项目角色名一并加入候选 |
候选值只改善编辑体验,不限制输入内容,也不改变运行时参数类型。多行文本不建议声明候选值。
| 方法 | 何时被调用 |
|---|
run | 剧本正常播放到该方法。可以是 async,引擎会等它返回再走下一个块 |
runImmediately | 该方法的副作用需要”立即”发生、不等待动画 |
skip | 玩家按住 Ctrl 快进剧本时调用。一般写”放结果但不放动画”的简化版 |
runImmediately 和 skip 都是可选的,不提供时引擎会 fallback 到 run。
static fadeToBlack = method({
title: "渐黑",
schema: { duration: { type: "number", label: "时长(ms)", default: 800 } },
// 正常播放:跑完淡入动画
async run(ctx, params) {
await ctx.curtain.fadeIn({ duration: params.duration });
},
// 玩家快进:直接黑屏,不等动画
skip(ctx) {
ctx.curtain.show();
},
});
在 run 里访问 this.save
method() 的 run 回调里,this 类型是 ExtensionBase——TypeScript 没办法把方法定义和外层类的 saveSchema 关联起来,this.save 拿到的是 EmptySaveAPI,任何 key 都报错。
正确做法是显式声明存档形状然后收窄:
import type { SaveAPI } from "@avg-studio/sdk";
type InventorySaveMap = {
items: readonly string[];
gold: number;
};
@extension({ id: "inventory", label: "背包" })
export class Inventory extends Extension {
static saveSchema = defineSave({
items: { type: "list", persistence: "slot", default: [] as string[] },
gold: { type: "number", persistence: "slot", default: 0 },
});
static addItem = method({
title: "增加物品",
schema: { itemId: { type: "string", label: "物品 ID", required: true } },
run(_ctx, params) {
const save = this.save as unknown as SaveAPI<InventorySaveMap>;
save.set("items", [...save.get("items"), params.itemId]);
},
});
static addGold = method({
title: "增加金币",
schema: { amount: { type: "number", label: "数量", default: 100 } },
run(_ctx, params) {
const save = this.save as unknown as SaveAPI<InventorySaveMap>;
save.set("gold", save.get("gold") + params.amount);
},
});
}
引擎内置的 gallery-screen 就用这个模式。详见 存档 Schema。
方法跟界面联动
方法改了状态、界面要实时刷新——首选 React Hook 路径,不要绕 ctx.subscribe:
// 界面里订阅存档/变量
function GalleryComponent() {
const ctx = useExtensionContext();
const [items] = ctx.variables.useValue("gold");
// gold 变化时组件自动重渲染
return <div>金币:{items}</div>;
}
ctx.variables.useValue 和 this.save.useValue 在底层都订阅了对应的 zustand store,比手动 ctx.subscribe("variable:changed", ...) 更地道。命令式 subscribe 主要给非 React 上下文(比如 static onRegister)用,并且它的 handler 签名是 () => void——不接收参数,需要数据时主动 ctx.variables.get() 读。
方法都是 static——它们附在类上,但每次调用引擎会 new 一个新实例来跑,this 上的临时字段不持久。要持久状态只能放 this.save(随存档)或 ctx.variables(剧本变量)。