剧本方法(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") |
enabledWhen | 否 | 按扩展项目设置控制该方法是否出现在新建候选中 |
schema | 否 | 参数定义,Studio 自动生成填参表单 |
returns | 否 | 返回值契约;声明后可在 If 中作为条件来源,支持 boolean、number、string |
run | 是 | 执行体,剧本走到这里时调用 |
runImmediately | 否 | 立即生效版本,详见下文 |
skip | 否 | 玩家快进时的简化行为,详见下文 |
按项目配置启用方法
方法不是每个项目都需要时,可以先声明一个扩展设置,再用 enabledWhen 引用它:
import { Extension, extension, method, settings } from "@avg-studio/sdk";
@extension({ id: "rewards", label: "奖励系统" })
export class RewardSystem extends Extension {
static settings = settings((s) => ({
enableAchievementReward: s
.boolean("启用成就奖励方法")
.default(false),
}));
static grantAchievementReward = method({
title: "发放成就奖励",
enabledWhen: "enableAchievementReward",
schema: {
achievementId: { type: "string", label: "成就 ID", required: true },
},
run(ctx, params) {
// 发放奖励……
},
});
}
字符串写法读取当前 Extension 子模块里的同名设置。未显式保存配置时,Studio 会使用该设置的 default;切换开关后,方法选择器和 Tab 菜单会立即更新。
需要按枚举等非布尔值启用时,使用对象写法:
enabledWhen: { setting: "rewardMode", equals: "achievement" }
对象还支持 scope: "extension",用于读取扩展级设置。setting 包含 . 时会被视为扩展内的完整设置路径,可引用其他子模块,例如:
enabledWhen: { setting: "common.enableAdvancedMethods" }
enabledWhen 只控制这个方法是否能被新选择。已经写入剧本的方法仍然保留参数 Schema、可以编辑并正常执行;关闭开关不会让已有剧本断链。
参数 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。
声明返回值
方法需要参与剧情条件判断时,用 returns 显式声明返回类型,并在 run 中返回对应的值:
static hasItem = method({
title: "是否持有道具",
description: "检查玩家是否至少持有一个指定道具",
returns: { type: "boolean", label: "是否持有" },
schema: {
itemName: { type: "string", label: "道具名称", required: true },
},
run(ctx, params) {
const items = ctx.variables.get<string[]>("inventoryItems") ?? [];
return items.includes(params.itemName);
},
});
支持的类型如下:
returns.type | If 中的编辑方式 |
|---|
boolean | 选择「结果为真」或「结果为假」 |
number | 使用等于、大于、小于等数字算子 |
string | 使用等于、包含、开头是等文本算子 |
声明了返回值的方法会在方法列表中显示「返回 · 布尔 / 数字 / 文本」标记。只有这类方法会出现在 If 的扩展方法候选中;没有 returns 的动作方法不会混入条件列表。
If 与普通「调用扩展方法」使用完全相同的参数 Schema 和变量解引用规则。普通调用在右侧检查器中内联填写参数;If 为避免条件卡过长,会弹出参数窗口。两处保存的是同一种参数数据。
运行时返回值必须与 returns.type 一致。类型不匹配、方法缺失、扩展停用或方法抛错时,If 会把该条条件安全地视为不满足,并在控制台记录警告。
文本参数候选值
单行 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。
对于有返回值的方法,runImmediately 和 skip 若存在,也应返回相同类型和语义的结果。普通「调用扩展方法」Block 会忽略返回值;If 会读取返回值完成比较。
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(剧本变量)。