ARTICLE · 1190202
YYKACN FlownNav 使用文档(手把手详细版)
适用:Cocos Creator 3.8(3.x 通用)· 2D 项目一句话:让成百上千个单位自动绕过障碍、一起涌向目标,并且互相不重叠。内核:Rust 编译的 Wasm(69KB,无外部依赖);Wasm 不可用时自动降级为纯 TS,仍能跑。
0. 先搞清楚它能做什么、不能做什么
能(本插件的目标场景)
大量单位涌向同一个(或少数几个)目标:割草、塔防、肉鸽、潮水怪、SLG 小兵。
单位绕过静态障碍(墙、缺口)。
单位之间、单位与障碍之间互相让路,不会叠成一坨。
目标随时变(例如点击地板改变集结点)。
不能(请勿神话,否则会怪插件不好用)
每个单位各自不同的目标、点对点寻路 → 请用 A* / NavMesh。
3D / 体积导航 → 本插件是 2D 平面。
1. 两个核心概念(务必先看,90% 的"不会用"卡在这里)
① 流场(Flow Field)+ ORCA 是什么?
流场:一张覆盖整张地图的"箭头表",每个格子都指向通往目标的方向。单位只需要查自己脚下那格的箭头往哪走 —— 所以"一大群单位奔同一个目标"时,算一次就够,极省性能。
ORCA(RVO2):单位与单位、单位与障碍之间的局部互避,让它们靠近时不重叠、不卡死。
两者互相独立:可以只用流场(关掉互避),也可以组合使用(默认组合)。
② 网格 vs 世界坐标(最容易搞混!)
插件内部是一张格子网格,你填的参数有的是「格子数」,有的是「世界坐标」。记住这个公式:
地图世界宽度 = width × cellSize
地图世界高度 = height × cellSize参数 | 单位 | 说明 |
|---|---|---|
| 格子数(整数) | 不是世界长度!网格有多少格 |
| 世界单位 / 格 | 一格代表多大 |
| 世界坐标 | 网格左下角的世界坐标 |
| 世界坐标 | 终点位置 |
| 世界坐标 | 矩形障碍(左下角 + 宽高) |
| 格子数 | 障碍向外"长胖"几格,给单位留身位 |
举例:
width: 60, height: 44, cellSize: 0.5→ 地图世界尺寸为 30 × 22。这与示例 Demo 里的wrapWidth=30, wrapHeight=22, cellSize=0.5完全对应。
2. 安装(推荐用编辑器面板一键装,最省事)
2.1 安装扩展文件夹
把本插件的 extension 文件夹复制到你的项目 extensions/ 目录下。建议把文件夹命名为 yyka-flownav(与 package.json 里的 name 字段一致,最稳妥、便于排查问题):
{你的项目}/
└─ extensions/
└─ yyka-flownav/ ← 即本插件的 extension 文件夹
├─ package.json
├─ main.js
├─ i18n/
└─ dist/
├─ panels/ ← 「流场烘焙」面板
├─ wasm/ ← 计算内核
└─ asset-provider/ ← .flow 资源导入/加载然后 重启 Cocos Creator。
2.2 打开烘焙面板
顶部菜单栏 → YYKA插件 → 流场避障插件,即可打开「流场烘焙」面板(可拖到任意位置停靠)。
2.3 第一步:点「一键安装 / 更新」
面板里第一块绿框就是安装区,必须先点这个按钮,插件其余功能才会解锁。它会把运行时代码和内核写进你的项目(目录固定,覆盖安装不会产生重复脚本):
assets/
├─ FlownNav/ ← 运行时代码(TS 组件)
│ ├─ FlowNav.ts ← 一行式门面(最常用入口)
│ ├─ FlowFieldManager.ts ← 全局管理器(单例,跑仿真)
│ ├─ FlowFieldAgent.ts ← 单位组件
│ ├─ NavObstacle.ts ← 障碍组件
│ ├─ DebugView.ts ← 调试可视化
│ ├─ FlowFieldAsset.ts ← .flow 二进制解析
│ ├─ WasmRuntime.ts ← Wasm 封装
│ ├─ FlowFieldFallback.ts ← 纯 TS 降级实现
│ ├─ TileMapNavHelper.ts ← 代价场辅助
│ ├─ loadWasm.ts ← 内核加载
│ └─ index.ts
└─ resources/
└─ FlownNav/
└─ wasm/
└─ flownav_core.wasm ← 计算内核(运行时从这里加载)安装完成后,面板下方会显示可复制的「一行式用法」,并解锁 ③ 烘焙、④ 示例 Demo。
重要:Wasm 内核固定从
assets/resources/FlownNav/wasm/flownav_core.wasm加载。若你手动拷贝运行时(不走面板),务必把flownav_core.wasm一起放到这个路径,否则会降级到纯 TS(能跑但慢很多)。运行后看控制台:打印mode=0表示走 Wasm,mode=1表示降级。
3. 开始使用(三种方式,按需选)
方式 | 入口 | 适合场景 | 代码量 |
|---|---|---|---|
A. 一行式 |
| 刚上手、想最快跑通 | 1 段 |
B. 分步式 |
| 要先加载资源、后建图 | 2~3 行 |
C. 底层管理器 |
| 深度控制参数 / 障碍来自 TileMap | 数行 |
动态目标(点哪走哪)→
start()/build()静态地图(性能最好)→ 预先烘焙
.flow,运行时set()
方式 A:一行式(最快跑通,推荐先跑这个)
在 assets/scripts/ 下新建 MyGame.ts(相对路径按你实际放置位置调整):
import { _decorator, Component, Node } from 'cc';
import { FlowNav } from '../FlownNav/FlowNav';
import { FlowFieldAgent } from '../FlownNav/FlowFieldAgent';
const { ccclass } = _decorator;
@ccclass('MyGame')
export class MyGame extends Component {
async onLoad() {
// 一步完成:加载内核 + 建图 + 算流场
await FlowNav.start({
width: 60, height: 44, // 格子数:60 × 44 格
cellSize: 0.5, // 每格 0.5 世界单位 → 地图 30 × 22
originX: 0, originY: 0, // 网格左下角世界坐标
goal: { x: 24, y: 11 }, // 终点(世界坐标)
blocked: [{ x: 8, y: 10, w: 1, h: 6 }], // 障碍(世界坐标矩形)
inflate: 1, // 障碍膨胀 1 格
});
// 生成 200 个单位:也可以在编辑器里直接给节点挂 FlowFieldAgent 组件(等价)
for (let i = 0; i < 200; i++) {
const n = new Node('unit-' + i);
n.setPosition(2 + (i % 20) * 1.2, 2 + Math.floor(i / 20) * 1.2);
this.node.addChild(n);
const a = n.addComponent(FlowFieldAgent);
a.radius = 0.4; // 避障半径
a.maxSpeed = 3.0; // 最大速度
}
}
}把这个组件挂到场景里任意节点(需要有 Camera 能看到 2D 内容),运行预览就能看到小球群绕墙涌向终点。
FlowNav.start(config) 参数
参数 | 类型 | 必填 | 单位 | 含义 |
|---|---|---|---|---|
| number | 是 | 格子数 | 横向格子数 |
| number | 是 | 格子数 | 纵向格子数 |
| number | 是 | 世界单位/格 | 格子尺寸,越小越精细、越费算力 |
| number | 否 | 世界坐标 | 网格左下角,默认 0,0 |
|
| 是 | 世界坐标 | 终点 |
|
| 否 | 世界坐标 | 矩形障碍列表 |
| number | 否 | 格子数 | 障碍膨胀,适配单位大小 |
| boolean | 否 | — | 重建流场时是否保留现有单位(改目标时设 true) |
方式 B:预烘焙 .flow(静态地图,性能最好)
静态关卡推荐:离线烘焙一次,运行时零 BFS 开销。步骤:
打开面板 ③「烘焙 .flow」,填好网格/原点/目标/障碍,点「烘焙并保存 .flow」。
产物会保存到
assets/flow/<文件名>.flow。要运行时加载,需把
.flow放到assets/resources/下(例如assets/resources/flow/map_flow.flow),资源系统才能找到它。运行时加载:
import { resources } from 'cc';
import { FlowNav } from '../FlownNav/FlowNav';
import { FlowFieldAsset } from '../FlownNav/FlowFieldAsset';
const world = await FlowNav.init(); // 1) 加载内核(可重复调用)
// 方式一:走资源系统加载(依赖插件注册的 .flow 加载器)
resources.load('flow/map_flow', (err, asset) => {
if (!err) world.set(asset as FlowFieldAsset);
});
// 方式二(更保险,任何 3.x 都能用):自己读字节再解析
// const bytes = /* 拿到 .flow 的 Uint8Array */;
// world.set(FlowFieldAsset.loadFromBytes(bytes));
.flow是自定义二进制资源,不同 Creator 小版本的自定义加载器 API 略有差异。若"方式一"在你的版本里没生效,直接用FlowFieldAsset.loadFromBytes(Uint8Array)(完全等价),或干脆用「动态构建」跑。
方式 C:底层管理器(深度控制)
需要精细调参、或障碍来自 TileMap / 自定义来源时,直接用管理器:
import { FlowFieldManager } from '../FlownNav/FlowFieldManager';
import { FlowFieldAgent } from '../FlownNav/FlowFieldAgent';
async function boot() {
// 1) 单例 + 内核(自动创建持久节点,可重复调用)
const mgr = await FlowFieldManager.setup();
// 2) ORCA 参数(都有默认值,按需调)
mgr.neighborDistance = 3; // 邻居搜索半径
mgr.maxNeighbors = 16; // 每个单位最多考虑几个邻居
mgr.timeHorizon = 1.5; // 避障时间视界(越小越激进)
mgr.simAsEveryNthFrame = 1; // 仿真降频:1=每帧,2=每 2 帧…(ORCA 不需 60Hz)
// 3a) 动态流场(世界坐标矩形障碍)
mgr.buildWorldFlow({
width: 60, height: 44, cellSize: 0.5,
goal: { x: 24, y: 11 },
blocked: [{ x: 8, y: 10, w: 1, h: 6 }],
inflate: 1,
});
// 3b) 或直接用代价场数组构建
// const cost = new Float32Array(w * h); // 0=可走, 1=阻挡
// mgr.buildDynamicFlow(w, h, cellSize, originX, originY, cost, goalCellX, goalCellY, keepAgents);
// 3c) 或加载已烘焙资源
// mgr.loadFlow(flowAsset);
// 4) 挂 Agent
for (let i = 0; i < 200; i++) {
const n = new Node(`agent-${i}`);
n.setPosition(2 + (i % 20) * 1.2, 2 + Math.floor(i / 20) * 1.2);
this.node.addChild(n);
const a = n.addComponent(FlowFieldAgent);
a.radius = 0.4;
a.maxSpeed = 3.0;
a.avoidanceEnabled = true;
}
}门面 FlowNav 还暴露底层对象,便于混用:
FlowNav.mgr; // 当前 FlowFieldManager
FlowNav.flow; // 当前生效的流场 FlowFieldAsset(可能为 null)4. 编辑器烘焙面板逐项说明
面板 ③「烘焙 .flow」各字段:
字段 | 单位 | 含义 | 示例 |
|---|---|---|---|
宽 (cells) | 格子数 | 横向格子数 | 40 |
高 (cells) | 格子数 | 纵向格子数 | 30 |
格子尺寸 | 世界单位/格 | 一格多大 | 0.5 |
原点 X / Y | 世界坐标 | 网格左下角;可随选中节点自动推算 | 0 |
目标 X / Y | 世界坐标 | 终点;烘焙时自动换算成格子 | 20 / 15 |
膨胀半径 | 格子数 | 障碍向外长胖几格,防止贴墙卡死 | 1 |
阻塞区 JSON | 世界坐标矩形数组 |
|
|
文件名 | — | 保存为 |
|
点「烘焙并保存 .flow」后,状态栏会显示 已完成 → db://assets/flow/xxx.flow (宽x高, N 字节)。

5. 组件速查
FlowFieldAgent(挂在每个会动的单位上)
属性 | 默认 | 含义 |
|---|---|---|
| 0.4 | 单位半径,越大越容易让路 |
| 3.0 | 最大移动速度(世界单位/秒) |
| 1.0 | 期望速度倍率(跟随流场的强弱) |
| true | 是否参与 ORCA 互避(false = 纯跟随流场) |
| true | 是否参与仿真;到达后可设 |
常用方法:
agent.markArrived()(置active=false)、agent.detach()(从管理器注销)。只要节点上有这个组件,管理器就会每帧自动驱动它的位置,你不需要自己写移动逻辑。
NavObstacle(障碍标记,可选)
标记一块阻挡区域,用于:运行时重建流场时把该区域算作阻挡、以及被 DebugView 可视化。
属性 | 默认 | 含义 |
|---|---|---|
| true | 静态墙 / 动态障碍 |
| (0,0) | 区域中心相对节点的偏移 |
| 0.5 | 矩形半宽 / 半高(世界单位) |
| 0 | >0 时用圆形(半径)代替矩形 |
注意:已烘焙的
.flow不会因为场景里的 NavObstacle 自动变化。NavObstacle 主要用于运行时重建 + 调试可视化;也可以用TileMapNavHelper.buildCostFromObstacles(...)把一组 NavObstacle 转成代价场后再重建流场。
DebugView(调试可视化,可选)
给场景里任意节点挂上即可(会自动补一个 Graphics)。图层画在单位下方,不遮挡。
属性 | 默认 | 含义 |
|---|---|---|
| true | 显示网格 |
| true | 显示流场方向箭头 |
| true | 显示阻挡格(红色) |
| true | 显示单位半径圈 |
| 20 | 每几帧重画一次 |
| 1.0 | 箭头大小/疏密(调小=更小更密) |
TileMapNavHelper(工具类,可选)
import { TileMapNavHelper } from '../FlownNav/TileMapNavHelper';
// 把一组 NavObstacle 转成代价场(0=可走, 1=阻挡;W/H 为格子数)
const cost = TileMapNavHelper.buildCostFromObstacles(w, h, cellSize, originX, originY, obstacles);
// 再把障碍膨胀若干格
const inflated = TileMapNavHelper.inflate(cost, w, h, 1);6. 实体注册机制(Agent 池 / 槽位)
本插件没有单独的 ECS 实体表,"实体"就是挂了 FlowFieldAgent 组件的节点。注册是自动的,你不需要手写注册表。
6.1 注册:加组件即注册
FlowFieldAgent.onLoad() 会做两件事:
若管理器单例不存在,自动创建持久节点
FlowFieldManager(也可提前手动FlowFieldManager.createPersistentNode());调用
manager.register(this)领取一个 slot(槽位)。
register() 会优先回收空闲槽(_freeSlots),没有空闲才追加到内部 _agents 数组并扩容 SoA buffer —— 所以单位反复增删不会让池无限膨胀。管理器内部用 SoA(_pos / _vel / _rad / _speed 等 TypedArray)按槽位存数据,每帧一次性批量送进 Wasm,这正是性能关键。
6.2 注销:销毁即注销
节点销毁 →
FlowFieldAgent.onDestroy()自动detach();也可手动提前注销:
agent.detach()(安全,可重复调用);注销只是把槽位标记为空并回收,不会立刻缩容。
6.3 槽位相关的 API
方法 / 属性 | 作用 |
|---|---|
| 当前槽位序号( |
| 从管理器注销,回收槽位 |
| 置 |
是否参与仿真由 isActive() 判定:active && enabled && node.activeInHierarchy。所以到达 / 静止 / 离屏的单位把 active 置 false,即可不动它们、只保留位置(省算力)。
import { Node } from 'cc';
import { FlowFieldAgent } from '../FlownNav/FlowFieldAgent';
// 生成即注册(onLoad 自动领槽位)
const n = new Node('unit');
this.node.addChild(n);
const a = n.addComponent(FlowFieldAgent);
a.radius = 0.4;
a.maxSpeed = 3.0;
// 到达终点后休眠:保留槽位、不再参与仿真
a.markArrived();
// 彻底移除并回收槽位
a.detach();
n.destroy();6.4 注意事项
重建流场默认会清空池:
FlowNav.start()/build()/buildWorldFlow()默认resetPool(),旧单位会失效(需重新挂组件)。只想改目标、保留现有单位时,传keepAgents: true(Demo 里点击改集结点就是这么做的)。不要自己管理槽位,也不要绕过组件注册;批量生成直接
node.addComponent(FlowFieldAgent)即可。池容量是
agentCapacity(默认 2048),可FlowNav.init(capacity)调大,超限会自动扩容。实现细节见 FlowFieldAgent.ts 与 FlowFieldManager.ts 的
register()/unregister()。
7. 跑示例 Demo(最快看到效果)
面板 ④ 点「附带安装示例 Demo」。它会写入:
assets/demos/GrassCutterDemo.ts、TowerDefenseDemo.ts、DemoWorld.tsassets/flow/sample.flow(示例地图)新建一个 2D 场景,放一个空节点,挂上
GrassCutterDemo(割草,500 单位)或TowerDefenseDemo(塔防,400 单位)。运行预览:一大批小球绕墙涌向绿色基地,并互相避让。
点击画面任意位置即可改变集结点,流场实时重建、单位立刻改道。
想关掉调试叠加层,把组件上的
showDebug取消勾选。
8. 常见问题 FAQ
菜单里找不到插件?确认扩展已放在
{项目}/extensions/下(建议文件夹名与package.json的name一致,即yyka-flownav),然后重启 Creator。面板只有一块安装区,下面功能都不显示?这是设计如此:先点「一键安装 / 更新」装好运行时代码,其余功能才会解锁。
安装后 Assets 面板没立刻出现
FlownNav/?右键该目录「刷新资源库」,或最小化/最大化编辑器窗口触发重扫。报错
FlowNav.start is not a function?项目里的运行时代码是旧版。回到面板点一次「重新安装 / 更新」,然后重启预览。单位不动 / 原地卡住?① 检查
goal是否落在障碍格子内(终点在墙里会导致无解);② 检查originX/originY与cellSize是否和你的实际坐标对得上;③ 终点设太靠地图边缘时,适当加大width/height。单位叠在一起 / 穿模?给单位加大
radius、调高maxNeighbors;或调大neighborDistance。贴墙抖动 / 卡墙角?烘焙时加大「膨胀半径」;运行时调小
maxSpeed/speedScale,或减小timeHorizon。看不到箭头?① 确认场景里有节点挂了
DebugView且showArrows打开;② 确认已有生效的流场(先start()或set());③ 箭头太大/太密就调小arrowScale。性能差 / 卡顿?见第 9 节。先确认
mode=0(走 Wasm 未降级)。Invalid typed array length崩溃?通常是反复onLoad/ 重复建图导致 Agent 池膨胀。重建流场改目标时用keepAgents: true;不再使用的单位调用agent.detach()。
9. 性能建议
静态地图就预烘焙
.flow,运行时零 BFS。ORCA 降频:
simAsEveryNthFrame = 2~3(20~30Hz 对割草/塔防足够)。休眠:到达 / 静止 / 离开视口的单位把
active置false。cellSize别太小:够用即可,越小格子越多、计算越多。maxNeighbors别盲目调大:开销与maxNeighbors × 活跃单位数成正比。务必保留 Wasm:确认
assets/resources/FlownNav/wasm/flownav_core.wasm存在,mode=0。参考基准(Node 原生 wasm):400 单位 × 60 帧 ≈ 20ms,约 120 万 agent-step/s。
更细的参数取舍见 参数调优.md。
10. 明确边界
面向大量单位涌向少数目标的割草 / 塔防 / 肉鸽 / 小型 SLG;不是点对点、每个单位目标各自独立的通用导航解决方案 —— 那类需求请用 A* / NavMesh。















































































