LPAI 独立网页游戏 AI 社区指南

页面与交互

渲染布局与 Canvas

用只读响应式页面和完整 Canvas renderer API 构建场景。

页面与交互 · AI 路径

HTML 负责页面,Canvas 负责连续绘制

渲染布局同步返回一份响应式页面;Canvas renderer 增加精灵、镜头、粒子、碰撞和连续输入。两者只消费状态快照,玩家存档仍只能由 Worker 动作结算。

# 选择页面技术

需求 负责人
状态栏、卡片、列表、表单、弹窗、响应式排版 渲染布局 HTML/CSS
精灵、连续移动、命中检测、局部高频图形 Canvas renderer
三维模型、透视相机、灯光、3D 动画与拾取 type=3d Three.js Canvas renderer
点击、输入、付费或插件动作 已存在的控件 binding
属性、对象、存档、AI 或联网结算 Worker 控件/事件脚本
Canvas 外的临时覆盖层 客户端特效脚本

不要为了一个普通按钮使用 Canvas,也不要为了移动精灵逐帧重建整页 HTML。

# 渲染布局契约

脚本同步返回 HTML 字符串。它可以读取允许的 player、objects 和 tools 代理,但不能修改状态,也不能使用 async、await、Promise、DOM、网络或定时器。

return `
  <main lp-key="camp-root" class="min-h-dvh bg-slate-950 p-4 text-white">
    <h1>` + tools.tr("冒险营地") + `</h1>
    <p>` + player.getString("当前提示") + `</p>
    <button lp-control="explore-control-id" lp-key="explore-action">
      ` + tools.tr("开始探索") + `
    </button>
  </main>
`;

玩家输入、AI 输出和联网文本都是不可信展示数据,应转义或安全绑定,不能直接插入可执行 HTML。

# 绑定协议

属性 作用
lp-control 绑定已有控件及其 Worker 规则
lp-repeat-control 展开对象/插件重复行并保留实例上下文
lp-for 从 JSON 数组或非负整数展开模板,最多 100 行;支持 {{entry.id}} 和 {{index}}
lp-if 按条件挂载或卸载节点及其绑定,支持 ! 取反
lp-args 在控件事件根节点传递 JSON 参数,脚本通过 tools.kwargs.params 读取
lp-events 声明控件事件绑定
lp-model 绑定数值属性或可写动态字符串
lp-key 更新时保留稳定节点身份
lp-canvas 挂载已配置的 Canvas renderer

binding 使用稳定资源 ID,节点使用稳定业务 key。随机 key 会破坏输入、焦点和 renderer 连续性。

重复按钮可共用一个控件:

<button lp-for='entry in [{"id":"attack","label":"攻击"},{"id":"defend","label":"防御"}]'
  lp-control="动作控件ID" lp-events="js_g" lp-args='{"action":"{{entry.id}}"}'>{{entry.label}}</button>

控件的 js_g 脚本读取 tools.kwargs.params.action。lp-for 不执行 JS 表达式;数组可以由布局脚本同步生成,动态数据可用返回 JSON 的占位符。数值和对象数据直接插入 HTML 属性前须正确转义。

动态字符串脚本可以同步返回 JSON.stringify([{ id: 'attack', label: '攻击' }]),然后将数据源写为 lp-for="entry in <|动态字符串ID|>",或等价的 lp-for="entry in player.getString('动态字符串ID')"。引擎在展开时读取当前字符串并解析 JSON,玩家数据变化后更新列表;这里不执行任意 JS 表达式。不要在布局脚本中把 player.getString(...) 的结果直接拼入 HTML 属性:数据中的引号可能破坏标记结构。

真实游戏的价格、倍率等配置放在表格,玩家专属数值放在属性中。动态字符串脚本只读取表格并组织 JSON,不要在脚本中重复写数值。例如 const table = player.getTable('动作配置表ID') 后,从 table.rowNames 取稳定 ID,用 table.getData(id, '名称') 和 table.getData(id, '消耗') 组装行。点击后的控件脚本应按 ID 重新从表格读取价格,而非信任 HTML 参数。

状态变化时会重新计算并比较列表数据,但不会整个重建 DOM:只有同一位置内容发生变化的行会重建,其余行继续复用。列表最多 100 行;调整排序可能使多个位置变化,建议保持 ID 和顺序稳定,避免用高频状态每帧改动列表。

条件渲染与输入框可组合使用:

<section lp-if="<{面板开关属性ID}>">
  <input type="number" lp-model="数量属性ID">
  <textarea lp-model="备注字符串ID"></textarea>
</section>

lp-if 可用属性 <{属性ID}>、动态字符串 <|条件ID|> 或循环行的 {{entry.visible}},前缀 ! 取反;空值、0、false、null、undefined、NaN 为假。复杂条件让动态字符串脚本返回 'true' 或 'false',不执行任意 JS 表达式。隐藏会卸载子树,重显时重新挂载:输入焦点和未提交草稿可能丢失;若只需视觉隐藏,请改用动态 style。

lp-model 当前只支持 input[type=number] 绑定普通数值属性,以及 textarea 绑定开启 editable 且没有脚本的动态字符串;文本 input、select、checkbox 不支持。输入发送给 Worker 校验写入,再以权威状态同步回表单。用于生成列表 JSON 的脚本动态字符串不能同时当作可编辑字段;需要编辑时另建可编辑源字符串。置于对象列表内时,输入会继承当前对象上下文。

# 挂载 Canvas renderer

在场景 Canvas 配置中创建唯一 renderer ID,并给宿主明确的响应式尺寸:

<canvas
  lp-canvas="world"
  lp-key="world-canvas"
  class="block h-[70dvh] w-full"
></canvas>

renderer 脚本获得 player、objects 和 tools.canvas,并且必须同步返回 frame 函数或生命周期对象:

回调 执行时机 输入
mount(ctx) Pixi 和宿主准备完成后一次 初始快照
update(ctx) 运行时快照改变时 新的冻结快照
frame(ctx) 每个活动动画帧 deltaMs 与 elapsedMs
bindingResult(result) Canvas binding 请求结算后 request/binding/accepted/reason
dispose(ctx) renderer 移除、场景切换或替换 最终清理上下文

回调不能是 async,也不能返回 Promise。允许的资源 Promise 用 .then/.catch 启动;高级 API 还应维护 disposed 标记。

# 状态快照 API

生命周期 ctx 与 tools.canvas 都提供冻结状态:

值 内容
ctx.revision / canvas.state.revision 当前快照修订号
ctx.state / canvas.state.snapshot { properties, dynamicStrings },按稳定 ID 索引
ctx.objects / canvas.objects.snapshot 按类/实例组织的对象快照
ctx.scene / canvas.scene id、逻辑 width、height、dpr
ctx.input / canvas.input 指针和键盘快照
ctx.deltaMs / ctx.elapsedMs 截断后的帧间隔与运行时间

快照只读。本地动画不能证明 Worker 动作成功,应等待下一份状态快照或 bindingResult。

# 完整 canvas API

本节绘制/碰撞/资源示例为缺省 type=2d 的 Pixi 接口。type=3d 复用生命周期、输入、绑定和源码模块,原生场景及绘制接口见 Three.js 三维 Canvas。两种引擎分别按需导入;实际发布分包和网络下载仍需按目标构建验收。

# 绘制

方法 作用
canvas.draw.sprite(key, options) 创建/更新已配置图片、帧或动画
canvas.draw.rect(key, options) 创建/更新矩形或圆角矩形
canvas.draw.text(key, options) 创建/更新 Pixi 文本
canvas.draw.bounds(key) 返回 { x, y, width, height } 或 undefined
canvas.draw.remove(key) 删除并销毁一个托管节点,返回 boolean
canvas.draw.clear() 删除全部托管节点,只在整体替换/销毁时使用

三个 draw 方法都是 upsert,必须复用稳定且唯一的 key。通用 options 包括 x、y、width、height、scale、scaleX、scaleY、rotation、alpha、visible、zIndex、bindings、objectId 和可序列化 payload。

  • Sprite 额外支持 asset、frame、animation、anchorX、anchorY。
  • Rect 额外支持 color、fillAlpha、stroke、strokeAlpha、lineWidth、radius。
  • Text 必须传 text;Pixi 字体字段放在 style,可传 anchorX / anchorY。
const canvas = tools.canvas;
let x = 80;

return {
  frame(ctx) {
    if (canvas.input.isKeyDown("ArrowRight")) {
      x += 180 * ctx.deltaMs / 1000;
    }
    if (canvas.input.wasKeyPressed("Space")) {
      canvas.emitBinding("jump", { x });
    }
    canvas.draw.rect("player", {
      x, y: 120, width: 32, height: 32,
      radius: 16, color: "#22d3ee"
    });
  },
  dispose() {
    canvas.draw.clear();
  }
};

# 资源

方法 返回
canvas.assets.resolve(assetId, objectId?) { id, source, state }
canvas.assets.state(assetId) idle、loading、ready 或 error
canvas.assets.preload(assetId, objectId?) Promise<boolean>

资源必须先存在于 renderer 配置中,不能编造 URL。preload 是上下文中返回 Promise 的例外,但生命周期仍需同步返回:不要 await,通过 state/update 在资源就绪后绘制。

# 输入

canvas.input.pointer 包含 x、y、down、pressed、released、pointerType、button、buttons、movementX、movementY、wheelX 和 wheelY。

canvas.input.keys、canvas.input.pressedKeys 和 canvas.input.releasedKeys 使用 KeyboardEvent.code。Canvas 获得焦点后才接收键盘输入。

方法 含义
canvas.input.isKeyDown(code) / canvas.input.wasKeyPressed(code) / canvas.input.wasKeyReleased(code) 持续按下、本帧按下、本帧释放
canvas.input.isButtonDown(button?) / canvas.input.wasButtonPressed(button?) / canvas.input.wasButtonReleased(button?) 指针按钮状态,默认 button 为 0

# 碰撞与边界

方法 返回
canvas.collision.intersects(leftKey, rightKey) 两个托管节点边界是否重叠
canvas.collision.contains(key, x, y) 点是否在托管节点内部
canvas.collision.clamp(key, bounds?) 把节点移入边界,返回是否成功

clamp 默认边界是 Canvas 逻辑区域。碰撞使用当前轴对齐渲染边界,不是自定义物理形状。

# 动作绑定

canvas.emitBinding(name, payload?, objectId?) 立即返回请求 ID。name 必须映射已配置的 action binding,其中使用真实控件 ID 和 js_g、js_press、js_release、js_move 之一。

托管节点可声明 bindings: { click, press, release, move },同时携带 payload 和 objectId。move binding 应配置节流,不能逐帧向 Worker 发动作;Worker 脚本从 tools.kwargs.payload 读取可序列化参数。

bindingResult 表示传输/执行链是否接受请求,不表示业务一定成功。权威成功结果仍来自后续状态快照。

# 模块

canvas.modules.load(key, source) 同步返回作者源码导出的函数或对象。同一 key/source 每个 renderer session 只执行一次;源码变化时旧模块先清理再替换;session 销毁会调用模块导出的 dispose(ctx)。

模块源码只能来自作者维护的只读动态字符串或静态表格。禁止执行玩家输入、动态表格、弱联网结果或运行时 AI 输出。

# 高级 PixiJS

canvas.pixi 开放 PixiJS 8 模块命名空间,canvas.app 开放宿主拥有的 Application。高级 renderer 可创建 Container、Graphics、Sprite、Text、滤镜、mask、shader、mesh、粒子和 ticker 回调。

不能创建/销毁宿主 Application、替换 renderer、销毁 app.ticker 或把 app.canvas 移到其他 DOM。canvas.draw 不管理自行创建的对象;必须在 dispose 中移除监听并销毁资源。

# 预算与失败

运行时会限制托管节点、纹理内存/并发、模块数量/深度/key 长度和模块源码总量。连续三次 frame 报错会暂停 frame 程序并显示 Canvas 诊断。复用稳定节点、预加载有限资源、避免每帧全量扫描,并优先修复第一条诊断。

# 纯 AI 任务合同

向编辑器 AI 提供场景、页面区域、数据 ID、控件/action binding、renderer ID、资源清单、目标宽度、视觉参考、清理规则和验收步骤。要求它保留 Worker 规则,只替换表现层。

验收首帧、状态更新、键盘/触摸焦点、尺寸/DPR、资源失败、binding 拒绝、重复输入、对象定位、场景清理和窄屏无重叠。

下一步:tools 运行时 API、客户端视觉特效和指针脚本。