夜雨聆风学习资料网

ARTICLE · 1190202

YYKACN FlownNav 使用文档(手把手详细版)

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 / height

格子数(整数)

不是世界长度!网格有多少格

cellSize

世界单位 / 格

一格代表多大

originX / originY

世界坐标

网格左下角的世界坐标

goal.x / goal.y

世界坐标

终点位置

blocked[].x/y/w/h

世界坐标

矩形障碍(左下角 + 宽高)

inflate

格子数

障碍向外"长胖"几格,给单位留身位

举例: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. 一行式

FlowNav.start()

刚上手、想最快跑通

1 段

B. 分步式

FlowNav.init() + build() / set()

要先加载资源、后建图

2~3 行

C. 底层管理器

FlowFieldManager

深度控制参数 / 障碍来自 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) 参数

参数

类型

必填

单位

含义

width

number

是

格子数

横向格子数

height

number

是

格子数

纵向格子数

cellSize

number

是

世界单位/格

格子尺寸,越小越精细、越费算力

originX / originY

number

否

世界坐标

网格左下角,默认 0,0

goal

{x, y}

是

世界坐标

终点

blocked

Array<{x,y,w,h}>

否

世界坐标

矩形障碍列表

inflate

number

否

格子数

障碍膨胀,适配单位大小

keepAgents

boolean

否

—

重建流场时是否保留现有单位(改目标时设 true)


方式 B:预烘焙 .flow(静态地图,性能最好)

静态关卡推荐:离线烘焙一次,运行时零 BFS 开销。步骤:

  1. 打开面板 ③「烘焙 .flow」,填好网格/原点/目标/障碍,点「烘焙并保存 .flow」。

  2. 产物会保存到 assets/flow/<文件名>.flow。

  3. 要运行时加载,需把 .flow 放到 assets/resources/ 下(例如 assets/resources/flow/map_flow.flow),资源系统才能找到它。

  4. 运行时加载:

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

世界坐标矩形数组

[{"x","y","w","h"}],可多块

[{"x":8,"y":12,"w":1,"h":6}]

文件名

—

保存为 assets/flow/<文件名>.flow

map_flow

点「烘焙并保存 .flow」后,状态栏会显示 已完成 → db://assets/flow/xxx.flow (宽x高, N 字节)。


5. 组件速查

FlowFieldAgent(挂在每个会动的单位上)

属性

默认

含义

radius

0.4

单位半径,越大越容易让路

maxSpeed

3.0

最大移动速度(世界单位/秒)

speedScale

1.0

期望速度倍率(跟随流场的强弱)

avoidanceEnabled

true

是否参与 ORCA 互避(false = 纯跟随流场)

active

true

是否参与仿真;到达后可设 false 省钱

  • 常用方法:agent.markArrived()(置 active=false)、agent.detach()(从管理器注销)。

  • 只要节点上有这个组件,管理器就会每帧自动驱动它的位置,你不需要自己写移动逻辑。

NavObstacle(障碍标记,可选)

标记一块阻挡区域,用于:运行时重建流场时把该区域算作阻挡、以及被 DebugView 可视化。

属性

默认

含义

isStatic

true

静态墙 / 动态障碍

blockOffset

(0,0)

区域中心相对节点的偏移

halfWidth / halfHeight

0.5

矩形半宽 / 半高(世界单位)

radius

0

>0 时用圆形(半径)代替矩形

注意:已烘焙的 .flow 不会因为场景里的 NavObstacle 自动变化。NavObstacle 主要用于运行时重建 + 调试可视化;也可以用 TileMapNavHelper.buildCostFromObstacles(...) 把一组 NavObstacle 转成代价场后再重建流场。

DebugView(调试可视化,可选)

给场景里任意节点挂上即可(会自动补一个 Graphics)。图层画在单位下方,不遮挡。

属性

默认

含义

showGrid

true

显示网格

showArrows

true

显示流场方向箭头

showBlocked

true

显示阻挡格(红色)

showAgents

true

显示单位半径圈

refreshEveryFrames

20

每几帧重画一次

arrowScale

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() 会做两件事:

  1. 若管理器单例不存在,自动创建持久节点FlowFieldManager(也可提前手动 FlowFieldManager.createPersistentNode());

  2. 调用 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

方法 / 属性

作用

agent.slot

当前槽位序号(-1 = 未注册)

agent.detach()

从管理器注销,回收槽位

agent.markArrived()

置 active = false:不参与仿真,但保留槽位与位置

是否参与仿真由 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(最快看到效果)

  1. 面板 ④ 点「附带安装示例 Demo」。它会写入:

    • assets/demos/GrassCutterDemo.ts、TowerDefenseDemo.ts、DemoWorld.ts

    • assets/flow/sample.flow(示例地图)

  2. 新建一个 2D 场景,放一个空节点,挂上 GrassCutterDemo(割草,500 单位)或 TowerDefenseDemo(塔防,400 单位)。

  3. 运行预览:一大批小球绕墙涌向绿色基地,并互相避让。

  4. 点击画面任意位置即可改变集结点,流场实时重建、单位立刻改道。

  5. 想关掉调试叠加层,把组件上的 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. 性能建议

  1. 静态地图就预烘焙.flow,运行时零 BFS。

  2. ORCA 降频:simAsEveryNthFrame = 2~3(20~30Hz 对割草/塔防足够)。

  3. 休眠:到达 / 静止 / 离开视口的单位把 active 置 false。

  4. cellSize 别太小:够用即可,越小格子越多、计算越多。

  5. maxNeighbors 别盲目调大:开销与 maxNeighbors × 活跃单位数 成正比。

  6. 务必保留 Wasm:确认 assets/resources/FlownNav/wasm/flownav_core.wasm 存在,mode=0。

  7. 参考基准(Node 原生 wasm):400 单位 × 60 帧 ≈ 20ms,约 120 万 agent-step/s。

更细的参数取舍见 参数调优.md。


10. 明确边界

面向大量单位涌向少数目标的割草 / 塔防 / 肉鸽 / 小型 SLG;不是点对点、每个单位目标各自独立的通用导航解决方案 —— 那类需求请用 A* / NavMesh。

相关学习资料