workerman 不提供 aoi 算法,需自行实现空间索引(如网格)优化视野管理;直接 foreach 遍历计算距离为 o(n) 复杂度,千人以上性能急剧下降;应采用九宫格加速查找+二次距离过滤,格子边长推荐 ceil(视野半径/3),并分离 aoi 逻辑至独立 businessworker 进程。

Workerman 本身不提供 AOI 算法,它只是个 PHP 异步网络框架;真正要实现全地图视野管理,得你自己在 Worker 进程里构建空间索引结构,再配合心跳、移动事件和广播逻辑。直接用 Workerman 的 onMessage + 全局数组遍历所有玩家做距离判断,1000 人以上就卡死。
为什么不能直接 foreach 所有玩家算距离?
每次玩家移动或发技能,都对全部在线玩家调用 sqrt(pow(x1-x2,2)+pow(y1-y2,2)),O(n) 复杂度。1000 人时每秒 10 次移动就是 1000×10 = 1 万次浮点运算+内存遍历;到 5000 人时单次移动就要扫 5000 个对象——CPU 持续 80%+,延迟飙升,onMessage 回调开始排队。
常见错误现象:Worker::stop() not called 不是根本问题,真正压垮的是你在 onMessage 里写了 foreach ($this->players as $p) + if (distance 这种代码。
可行做法:
- 把玩家按坐标映射到网格(grid),例如格子大小设为 30(≈ 视野半径),玩家位置
(x,y)→ 格子 ID(floor(x/30), floor(y/30)) - 每个格子用
array或SplFixedArray存该格内玩家引用,避免重复 new 对象 - 玩家 A 移动时,只查它所在格子及其周围 8 个格子(即九宫格),合并后去重再逐个算距离
- 格子 ID 计算必须用整数除法,别用
round()或intval(),否则边界玩家会掉格
怎么让 Workerman 支持格子动态更新?
Workerman 没有内置“地图分区”概念,你得自己维护一个全局的二维哈希表,比如 $this->grid = [],键为 "{$gx}_{$gy}",值为玩家 ID 数组。关键不是存什么,而是谁来触发更新。
容易踩的坑:
- 玩家移动消息没带原始坐标,只传了 delta 值 → 无法重算格子 ID,导致玩家“卡在旧格子”不广播
- 玩家跨格子瞬间,先删后加,但中间有帧没进任何格子 → 视野断连,表现为“人突然消失又出现”
- 用
array_diff算进出格子差异 → PHP 数组 key 重排后 diff 失效,应改用isset($old[$id]) !== isset($new[$id])
实操建议:在 onMessage 中收到移动包后,立即调用 updatePlayerGrid($playerId, $newX, $newY),该函数内部完成旧格移除 + 新格插入 + 差异广播(进入/离开通知)。
广播范围怎么控制在“真视野内”,而不是粗暴九宫格?
九宫格只是加速查找,不是最终结果。它解决的是“找哪些人可能在视野里”,不是“哪些人真在视野里”。格子大小取 30,但玩家实际视野半径可能是 25 —— 那么九宫格里边缘格子的玩家,大概率超距,必须二次过滤。
性能影响明显:
- 格子太小(如 10)→ 九宫格含 9 个格子,但要查 81 个格子才能覆盖半径 25 圆形区域 → 查太多,没意义
- 格子太大(如 50)→ 单格人数爆炸,九宫格查 9 个格子等于查 90% 全服玩家 → 回退成暴力遍历
推荐参数:格子边长 = ceil(视野半径 / 3),这样九宫格正方形外接圆半径 ≈ 视野半径 × 1.06,误差可控,且只需查 9 格。最后用 ($dx*$dx + $dy*$dy) 做无开方距离判断(省 CPU)。
Workerman 下如何避免广播风暴和连接假死?
AOI 同步不是“玩家一动就 push 给所有人”,而是要节流、聚合、异步投递。Workerman 的 ConnectionInterface::send() 是同步阻塞的,如果对 200 个客户端连续 send,哪怕数据只有 20 字节,也会卡住当前 Worker 进程 10ms+,后续消息全部堆积。
真实场景中必须:
- 把同一帧内多个玩家的 AOI 变更(进入/离开/移动)合并成一个批量包,用
json_encode(['updates' => [...]])一次 send - 用
Timer::add()做 20ms 合并窗口,而不是每帧立刻发;移动频繁时自动降频 - 对离线或慢连接客户端,用
$connection->isClosed()主动跳过,别让它拖垮整个 Worker - 别在
onClose里同步执行removePlayerFromGrid()+ 广播离开 —— 应该丢进Timer::add(0, ...)异步处理,防止 onClose 阻塞
最易被忽略的一点:Workerman 默认每个 Worker 进程单线程,所有 AOI 计算、格子更新、广播都在同一个循环里跑。一旦某个玩家触发了异常大的 AOI 更新(比如在出生点挤了 300 人),整个进程就卡住,所有连接暂停响应 —— 这不是代码 bug,是架构没做隔离。真要上量,得把 AOI 逻辑拆到独立的 BusinessWorker 进程,用 GatewayWorker 负责收发,两者通过 TextProtocol 或 FrameProtocol 通信。











