特效系统
客户端特效系统
特效系统 给控件、对象和页面加上临时视觉效果 `客户端特效系统` 只负责画面上的表现,例如飘字、高亮、抖动、悬浮提示、跟手提示。它不负责改属性,也不负责推进剧情。 # 最常见能做什么 飘字 移入高亮 受击抖动 按钮发光 跟手提示 局部遮罩 # 1. 最常用的 3 个方法 方法 用途 tools.runClie
特效系统
给控件、对象和页面加上临时视觉效果
`客户端特效系统` 只负责画面上的表现,例如飘字、高亮、抖动、悬浮提示、跟手提示。它不负责改属性,也不负责推进剧情。
# 最常见能做什么
- 飘字
- 移入高亮
- 受击抖动
- 按钮发光
- 跟手提示
- 局部遮罩
# 1. 最常用的 3 个方法
| 方法 | 用途 |
|---|---|
tools.runClientEffect(script, options?) |
启动一个画面特效 |
tools.disposeClientEffect(label) |
按 label 结束一个特效 |
tools.clearClientEffects() |
清空当前页面全部特效 |
# 2. 最重要的理解:有外层脚本和内层脚本
你平时在编辑器里写的,是外层脚本:
tools.runClientEffect("...");
真正负责演出的,是传进去的那段字符串脚本:
tools.runClientEffect(`
const current = api.getTarget();
if (!current) return;
`);
这两层不是一回事。
# 外层脚本里常用什么
playerobjectstools
# 内层特效脚本里常用什么
apipayloadtargettargetssessionpointer
# 3. 最简单的启动方式
tools.runClientEffect(`
const current = api.getTarget();
if (!current) return;
current.setStyle({
outline: "2px solid rgba(255,255,255,0.65)"
});
`, {
target: "攻击按钮",
label: "attack-highlight"
});
# 4. options 里最常用的字段
| 字段 | 用途 |
|---|---|
payload |
传数据给特效脚本 |
target |
指定一个目标控件 |
targets |
指定多个目标控件 |
waitMs |
特效结束后额外保留多久 |
label |
给这个特效起一个稳定名字 |
mode |
一次性特效用 replace,保留更新用 update |
# 5. target 可以怎么写
# 直接传控件 ID
tools.runClientEffect(script, {
target: "攻击按钮"
});
# 传对象控件目标
tools.runClientEffect(script, {
target: { itemId: "装备按钮", objectId: "对象ID" }
});
# 传多个目标
tools.runClientEffect(script, {
targets: ["主按钮", "副按钮"]
});
# 6. 最常见的几种效果
# 飘字
tools.runClientEffect(`
api.createText(payload.text || "-10", {
style: {
color: "#fff",
fontSize: "18px",
textShadow: "0 2px 8px rgba(0,0,0,0.45)"
}
}).place({
target: api.getTarget(),
offsetY: -20
});
`, {
payload: { text: "-10" },
target: "攻击按钮",
label: "damage-float"
});
# 高亮
tools.runClientEffect(`
const current = api.getTarget();
if (!current) return;
current.setStyle({
filter: "brightness(1.15)"
});
`, {
target: "主按钮",
label: "btn-highlight"
});
# 清掉效果
tools.disposeClientEffect("btn-highlight");
# 7. 在 replace 和 update 之间选择
mode 决定同一个 label 的特效如何继续:
| 模式 | 行为 | 适合场景 |
|---|---|---|
replace |
默认模式。同 label 的新请求会销毁旧会话。 | 飘字、抖动、闪烁、按顺序播放的动画 |
update |
同 label 的新请求复用原会话和其中的 keyed 节点。 | 跟手提示、拖拽预览、位置或尺寸连续变化 |
update 必须提供稳定且非空的 label。它会一直保留到调用 tools.disposeClientEffect(label)、tools.clearClientEffects()、页面切换或脚本执行失败。
# 8. 用 upsert 保留同一个 DOM 节点
update 特效内部要用稳定 key 调用:
api.upsertNode(key, options);
api.upsertText(key, text, options);
第一次调用创建节点。之后使用相同特效 label 和节点 key,会 patch 同一个 DOM,而不是删除重建。options 可以更新内容、HTML、类名、属性、样式、父节点、位置和 motion。
api.upsertText("tip", payload.text, {
placement: {
x: payload.x,
y: payload.y,
offsetX: 12,
offsetY: -10,
selfX: 0,
selfY: 1
},
motion: {
durationMs: 80,
easing: "linear",
position: true,
size: false
}
});
motion.position 会插值 left/top,motion.size 会插值 width/height。连续提交新目标时,浏览器会把当前 transition 重定向到最新目标,因此补帧不依赖游戏速度。不传 motion 就会立即跳到新值。
# 9. 跟手更新的完整写法
下面的外层脚本可以放在 js_move。每次触发只提交最新的指针位置:
const point = tools.kwargs.pointer;
const label = `pointer-tip-${tools.kwargs.itemID}`;
if (!point) return;
tools.runClientEffect(`
api.upsertText("tip", payload.text, {
style: {
padding: "4px 8px",
borderRadius: "6px",
background: "rgba(15,23,42,0.9)",
color: "#fff",
pointerEvents: "none"
},
placement: {
x: payload.x,
y: payload.y,
offsetX: 12,
offsetY: -10,
selfX: 0,
selfY: 1
},
motion: { durationMs: 80, easing: "linear", position: true }
});
`, {
mode: "update",
label,
payload: { text: "查看详情", x: point.clientX, y: point.clientY }
});
这类 update 脚本不要使用 await api.wait(...)、await node.animate(...) 或自己循环补帧。对应的 js_leave 用相同 label 清理。
# 10. 这类内容通常不放在特效系统里
下面这些内容更常写在普通脚本、事件或 AI 联动里:
- 扣金币
- 改属性
- 创建对象
- 推进主线
- 决定 AI 能不能输入
一个简单判断方法是:
如果把这段效果删掉,真实状态还是对的,只是看起来没那么明显,那它就很适合放在特效系统里。
# 使用时要注意的几件事
runClientEffect的第一个参数必须是一段字符串脚本。- 第二个参数如果传,必须是对象。
- 内层特效脚本只能使用注入的
api、payload、target、targets、runtime、session、pointer;运行时会遮蔽window、document、localStorage、fetch、postMessage等浏览器能力,但不额外拦截普通 JavaScript 写法。 - 一次性效果通常使用默认
replace;高频位置或尺寸更新使用mode: "update"、稳定label、稳定 upsert key,需要平滑时再配置motion。 - 特效系统更适合做视觉反馈,核心状态仍然放在属性、事件或别的脚本里。
# 下一步
如果你准备把声音也做成前端脚本,可以继续看 客户端音效系统。
如果你准备把特效挂到控件移入、移出、移动上,可以继续看 交互特效脚本。