aoi.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  1. package aoiTower
  2. import (
  3. "f1-game/internal/types"
  4. "math"
  5. clog "github.com/cherry-game/cherry/logger"
  6. )
  7. type (
  8. Config struct {
  9. Width int32 // 地图宽(x)
  10. Height int32 // 地图高(y)
  11. TowerWidth int32 // 灯塔宽(x)
  12. TowerHeight int32 // 灯塔高(y)
  13. TowerPointOffset int32 // 灯塔坐标偏移值(用于扩展或收缩灯塔边界)
  14. TowerViewRange int32 // 观察者所在灯塔周围能看到多少圈灯塔,必须 >= 1
  15. WatcherLeaveDelay int64 // 观察者离开灯塔的延迟时间(秒),用于消除边界抖动
  16. PrintTowerInfo bool // 初始化时,是否打印Tower的信息
  17. }
  18. // AOI灯塔系统
  19. AOI struct {
  20. config Config // 配置信息
  21. towerNumX int32 // x轴的灯塔数量
  22. towerNumY int32 // y轴的灯塔数量
  23. towers map[int32]map[int32]*Tower // 灯塔列表 key:灯塔x, value:{key:灯塔y, value:*Tower}
  24. objects Objects // 所有对象 key:ObjectID, value:*aoi.Object
  25. pendingWatchers map[int64]struct{} // 待处理(延迟离开)的观察者ID集合
  26. OnWatcherEnter OnWatcherEnterCallback // 观察者-[进入时]的回调
  27. OnWatcherMove OnWatcherMoveCallback // 观察者-[移动时]的回调
  28. OnMarkerEnter OnMarkerEnterCallback // 被观察者-[进入时]的回调
  29. OnMarkerMove OnMarkerMoveCallback // 被观察者-[移动时]的回调
  30. OnMarkerUpdate OnMarkerUpdateCallback // 被观察者-[更新时]的回调
  31. OnMarkerLeave OnMarkerLeaveCallback // 被观察者-[退出时]的回调
  32. OnMarkerUpdateID OnMarkerUpdateIDCallback // 被观察者-[更新时]的回调(带UpdateID)
  33. OnTowerExpire OnTowerExpireCallback // 灯塔过期的回调
  34. }
  35. OnWatcherEnterCallback func(watcher *Object, towers Towers)
  36. OnWatcherMoveCallback func(watcher *Object, newTowers Towers, oldTowers Towers)
  37. OnMarkerEnterCallback func(marker *Object, tower *Tower)
  38. OnMarkerMoveCallback func(marker *Object, newTowers *Tower, oldTowers *Tower)
  39. OnMarkerUpdateCallback func(marker *Object, tower *Tower)
  40. OnMarkerLeaveCallback func(marker *Object, tower *Tower)
  41. OnMarkerUpdateIDCallback func(marker *Object, tower *Tower, updateID uint32, funcName string)
  42. OnTowerExpireCallback func(watcher *Object, tower *Tower)
  43. )
  44. // AOI 灯塔 AOI 系统
  45. // 核心概念:
  46. // - Watcher(观察者):玩家,有视野范围,需要了解视野内的被观察者
  47. // - Marker(被观察者):NPC、怪物等,被观察者只属于一个灯塔
  48. // - Tower(灯塔):地图划分的网格单元,管理其范围内的观察者和被观察者
  49. //
  50. // 工作原理:
  51. // 1. 初始化时,根据 TowerWidth 和 TowerHeight 将地图划分为网格
  52. // 2. 观察者进入时,注册到其视野范围内的所有灯塔
  53. // 3. 被观察者进入时,注册到其所属的单个灯塔
  54. // 4. 对象移动时,更新其所属灯塔并触发 AOI 变化回调
  55. //
  56. // New 创建一个新的灯塔形 AOI 系统实例
  57. // 参数:
  58. // - config: AOI 系统配置(包含地图大小、灯塔尺寸、视野范围等)
  59. //
  60. // 返回值:aoi.AOI 接口实现指针
  61. // 用途:初始化一个完整的灯塔 AOI 系统
  62. // 工作流程:
  63. // 1. 配置校验
  64. // 2. 计算灯塔个数
  65. // 3. 初始化所有灯塔对象
  66. //
  67. // 性能:O(towerNumX * towerNumY) 时间复杂度
  68. func New(config Config) *AOI {
  69. towerAOI := &AOI{
  70. config: config,
  71. towers: make(map[int32]map[int32]*Tower),
  72. objects: Objects{},
  73. pendingWatchers: make(map[int64]struct{}),
  74. }
  75. towerAOI.init()
  76. return towerAOI
  77. }
  78. // init 初始化灯塔
  79. // 工作流程:
  80. // 1. 验证配置参数的合法性(地图尺寸、灯塔尺寸、视野范围等)
  81. // 2. 计算灯塔网格的行列数
  82. // 3. 创建所有灯塔对象并初始化二维 map 结构
  83. //
  84. // 关键点:
  85. // - 配置校验确保后续计算不会出现除零等错误
  86. // - 使用向上取整确保灯塔网格能覆盖整个地图
  87. // - 每个灯塔都分配唯一的 ID
  88. func (p *AOI) init() {
  89. // 地图大小校验:宽高都必须 >= 1
  90. if p.config.Width < 1 || p.config.Height < 1 {
  91. clog.Panicf("Width and Height must be >= 1, got %v", p.config.Width, p.config.Height)
  92. }
  93. // TowerWidth 校验:必须 >= 1,且不能超过地图宽度
  94. // 原因:灯塔是网格划分单位,宽度为 0 会导致除以零错误;超过地图宽度无意义
  95. if p.config.TowerWidth < 1 || p.config.TowerWidth > p.config.Width {
  96. clog.Panicf("TowerWidth must be 1 to %d, got %d", p.config.Width, p.config.TowerWidth)
  97. }
  98. // TowerHeight 校验:必须 >= 1,且不能超过地图高度
  99. // 原因:灯塔是网格划分单位,高度为 0 会导致除以零错误;超过地图高度无意义
  100. if p.config.TowerHeight < 1 || p.config.TowerHeight > p.config.Height {
  101. clog.Panicf("TowerHeight must be 1 to %d, got %d", p.config.Height, p.config.TowerHeight)
  102. }
  103. // TowerPointOffset:坐标级别的偏移,用于扩展或收缩灯塔的坐标边界,不能为负
  104. if p.config.TowerPointOffset < 0 {
  105. clog.Panicf("TowerPointOffset must be >= 0, got %d", p.config.TowerPointOffset)
  106. }
  107. // TowerViewRange:灯塔级别的视野范围(以灯塔为单位),必须 >= 1
  108. if p.config.TowerViewRange < 1 {
  109. clog.Panicf("TowerViewRange must be >= 1, got %d", p.config.TowerViewRange)
  110. }
  111. // 计算灯塔网格数量:使用 Ceil 向上取整
  112. // 例如:地图宽=10,灯塔宽=3 => towerNumX = Ceil(10/3) = 4
  113. // 这样可以覆盖整个地图(3,3,3,1)
  114. p.towerNumX = int32(math.Ceil(float64(p.config.Width) / float64(p.config.TowerWidth)))
  115. p.towerNumY = int32(math.Ceil(float64(p.config.Height) / float64(p.config.TowerHeight)))
  116. // 初始化灯塔网络:创建所有灯塔对象
  117. // 灯塔存储在二维 map 中:towers[towerX][towerY] = *Tower
  118. towerID := int32(0)
  119. p.towers = make(map[int32]map[int32]*Tower, p.towerNumX)
  120. for tx := int32(0); tx < p.towerNumX; tx++ {
  121. p.towers[tx] = make(map[int32]*Tower, p.towerNumY)
  122. for ty := int32(0); ty < p.towerNumY; ty++ {
  123. p.towers[tx][ty] = p.newTower(towerID, tx, ty)
  124. towerID++
  125. }
  126. }
  127. }
  128. // GetObject 根据对象ID 获取已进入 AOI 系统的对象
  129. // 参数:
  130. // - objectID: 对象的唯一标识符
  131. //
  132. // 返回值:对象指针,如果不存在则返回 nil
  133. // 用途:查询已进入 AOI 系统的对象
  134. // 性能:O(1) 时间复杂度
  135. func (p *AOI) GetObject(objectID int64) *Object {
  136. return p.objects.Get(objectID)
  137. }
  138. // Enter 对象进入 AOI 系统
  139. // 参数:
  140. // - object: 要进入系统的对象(观察者或被观察者)
  141. //
  142. // 返回值:是否成功进入(true 表示进入成功)
  143. // 用途:初始化对象在 AOI 系统中的位置和属性
  144. // 处理流程:
  145. // 1. 坐标校验 - 确保对象在地图范围内
  146. // 2. 重复检查 - 防止同一对象重复进入
  147. // 3. 处理流程:
  148. // 4. 如果是观察者 - 注册到视野范围内的所有灯塔,触发 OnWatcherEnter 回调
  149. // 5. 如果是被观察者 - 注册到所属灯塔(业务层脏标记标记会在观察者回调中触发)
  150. //
  151. // 性能:O(m) 时间复杂度,m 为视野范围内的灯塔数量
  152. // 注意:观察者和被观察者的处理流程完全不同
  153. func (p *AOI) Enter(object Object) bool {
  154. // 第一步:坐标校验 - 确保对象在地图范围内
  155. if !p.CheckPoint(object.Point.X, object.Point.Y) {
  156. clog.Warnf("[Enter] check point error. %s", object.Info())
  157. return false
  158. }
  159. // 第二步:重复检查 - 防止同一对象重复进入
  160. if p.objects.Contains(object.ObjectID) {
  161. clog.Warnf("[Enter] object has entered. %s", object.Info())
  162. return false
  163. }
  164. // 第三步:处理观察者(Watcher)- 获取视野范围内的所有灯塔
  165. if object.IsWatcher() {
  166. towers := p.GetRangeTowers(object.Point.X, object.Point.Y)
  167. if towers.Len() < 1 {
  168. clog.Warnf("[Enter] get tower fail. %s", object.Info())
  169. return false
  170. }
  171. p.objects.Add(&object)
  172. // 观察者加入到所有可见灯塔的观察者列表
  173. towers.Range(func(tower *Tower) {
  174. tower.AddWatcher(&object)
  175. })
  176. // 触发回调:报告 Watcher 进入
  177. if p.OnWatcherEnter != nil {
  178. p.OnWatcherEnter(&object, towers)
  179. }
  180. }
  181. // 第四步:处理被观察者(Marker)- 被观察者只属于一个灯塔
  182. if object.IsMarker() {
  183. tower := p.GetTowerByPoint(object.Point.X, object.Point.Y)
  184. if tower == nil {
  185. clog.Warnf("[Enter] get tower fail. %s", object.Info())
  186. return false
  187. }
  188. p.objects.Add(&object)
  189. tower.AddMarker(&object)
  190. // 触发回调:通知观察该灯塔的所有观察者有新marker进入
  191. if p.OnMarkerEnter != nil && len(tower.watchers) > 0 {
  192. p.OnMarkerEnter(&object, tower)
  193. }
  194. }
  195. return true
  196. }
  197. // Update 更新被观察者(Marker)的状态
  198. // 参数:
  199. // - object: 要更新的被观察者对象(必须是 IsMarker() 返回 true)
  200. //
  201. // 返回值:是否成功更新(true 表示更新成功)
  202. // 用途:当被观察者的状态变化时调用(如位置更新但未变更灯塔、属性变化等)
  203. // 处理流程:
  204. // 1. 类型检查 - 只有被观察者可以调用此方法
  205. // 2. 获取被观察者所属的灯塔
  206. // 3. 触发 OnMarkerUpdate 回调,通知观察该灯塔的所有观察者有 marker 更新
  207. //
  208. // 性能:O(m) 时间复杂度,m 为灯塔中观察者的数量
  209. // 注意:
  210. // - 此方法用于 marker 在同一灯塔内的状态更新,不改变灯塔关联
  211. // - 如果 marker 需要移动到新灯塔,应使用 Move() 方法
  212. // - 常见场景:部队属性变化、血量变化等需要实时通知观察者的状态更新
  213. func (p *AOI) Update(object Object) bool {
  214. if object.IsWatcher() {
  215. clog.Warnf("[Update]Only the marker object can call updates. %s", object.Info())
  216. return false
  217. }
  218. tower := p.GetTowerByPoint(object.Point.X, object.Point.Y)
  219. if tower == nil {
  220. clog.Warnf("[Update] Get tower fail. %s", object.Info())
  221. return false
  222. }
  223. // 触发回调:通知观察该灯塔的所有观察者有marker更新
  224. if p.OnMarkerUpdate != nil && len(tower.watchers) > 0 {
  225. p.OnMarkerUpdate(&object, tower)
  226. }
  227. return true
  228. }
  229. // UpdateID 更新被观察者(Marker)的状态(带UpdateID)
  230. // 参数:
  231. // - object: 要更新的被观察者对象
  232. // - updateID: 更新操作的唯一标识符
  233. // - funcName: 触发更新的函数名
  234. func (p *AOI) UpdateID(object Object, updateID uint32, funcName string) bool {
  235. if object.IsWatcher() {
  236. clog.Warnf("[UpdateID]Only the marker object can call updates. %s", object.Info())
  237. return false
  238. }
  239. tower := p.GetTowerByPoint(object.Point.X, object.Point.Y)
  240. if tower == nil {
  241. clog.Warnf("[UpdateID] Get tower fail. %s", object.Info())
  242. return false
  243. }
  244. // 触发回调:通知观察该灯塔的所有观察者有marker更新
  245. if p.OnMarkerUpdateID != nil && len(tower.watchers) > 0 {
  246. p.OnMarkerUpdateID(&object, tower, updateID, funcName)
  247. }
  248. return true
  249. }
  250. // 参数:
  251. // - objectID: 对象的唯一标识符
  252. // - x: 新位置的场景坐标 X
  253. // - y: 新位置的场景坐标 Y
  254. // - now: 当前时间戳(秒),用于设置延迟离开的过期时间
  255. //
  256. // 返回值:是否成功移动(是否改变了灯塔)
  257. // 用途:实时更新对象位置并计算 AOI 变化
  258. func (p *AOI) Move(objectID int64, x, y int32, now int64) bool {
  259. // 第一步:获取对象,检查是否存在
  260. object := p.objects.Get(objectID)
  261. if object == nil {
  262. return false
  263. }
  264. // 第二步:检查是否真的有位置变化
  265. if object.Point.X == x && object.Point.Y == y {
  266. return false
  267. }
  268. // 第三步:坐标校验
  269. if !p.CheckPoint(x, y) {
  270. clog.Warnf("[Move] Check point error. %s", object.Info())
  271. return false
  272. }
  273. // 第四步:获取旧灯塔位置
  274. oldTower := p.GetTowerByPoint(object.Point.X, object.Point.Y)
  275. if oldTower == nil {
  276. clog.Warnf("[Move] Old tower not found. %s", object.Info())
  277. return false
  278. }
  279. // 第五步:获取新灯塔位置
  280. newTower := p.GetTowerByPoint(x, y)
  281. if newTower == nil {
  282. clog.Warnf("[Move] New tower not found. %s", object.Info())
  283. return false
  284. }
  285. // 保存旧的xy坐标备用
  286. var (
  287. oldX = object.Point.X
  288. oldY = object.Point.Y
  289. )
  290. // 第六步:更新对象位置
  291. object.SetPoint(x, y)
  292. // 第七步:快速路径 - 如果灯塔没有改变,则不需要同步
  293. // 关键优化:同一灯塔内的移动不会改变 AOI 可见性
  294. if oldTower.id == newTower.id {
  295. return false
  296. }
  297. // 第八步:处理观察者移动 - 计算视野变化并更新灯塔关联
  298. if object.IsWatcher() {
  299. addTowers, delTowers := p.GetDiffTowers(oldX, oldY, x, y)
  300. // 处理新增的灯塔
  301. addTowers.Range(func(tower *Tower) {
  302. // 检查是否处于延迟离开状态(重入)
  303. if _, ok := object.delayLeaveTowers[tower.id]; ok {
  304. // 取消延迟离开
  305. object.RemoveDelayLeaveTower(tower.id)
  306. // 已在视野里,从新增里移除
  307. addTowers.Remove(tower.id)
  308. return // 已经在 tower.watchers 中,无需再次添加
  309. }
  310. // 真正的 Assume Add
  311. tower.AddWatcher(object)
  312. })
  313. // 处理删除的灯塔 (延迟离开)
  314. delTowers.Range(func(tower *Tower) {
  315. // 添加到延迟列表,不立即移除
  316. object.AddDelayLeaveTower(tower.id, now+p.config.WatcherLeaveDelay)
  317. p.pendingWatchers[objectID] = _nilStruct
  318. })
  319. // 触发回调:Watcher视野变化
  320. if p.OnWatcherMove != nil {
  321. p.OnWatcherMove(object, addTowers, delTowers)
  322. }
  323. return true
  324. }
  325. // 第十步:处理被观察者移动
  326. // 被观察者只能在一个灯塔内,需要更新其所属灯塔
  327. if object.IsMarker() {
  328. // 仅有灯塔改变时才进行处理
  329. if oldTower.id != newTower.id {
  330. oldTower.DelMarker(object)
  331. newTower.AddMarker(object)
  332. }
  333. if p.OnMarkerMove != nil && (newTower.watchers.Size() > 0 || oldTower.watchers.Size() > 0) {
  334. p.OnMarkerMove(object, newTower, oldTower)
  335. }
  336. return true
  337. }
  338. return false
  339. }
  340. // Leave 对象离开 AOI 系统
  341. // 参数:
  342. // - objectID: 对象的唯一标识符
  343. //
  344. // 返回值:无
  345. // 用途:当对象退出游戏世界时调用
  346. // 处理流程:
  347. // 1. 查找并删除对象
  348. // 2. 离开观察者 - 从视野范围内的所有灯塔中删除 (忽略延迟,立即清理)
  349. // 3. 离开被观察者 - 直接通知
  350. func (p *AOI) Leave(objectID int64) {
  351. object := p.objects.Get(objectID)
  352. if object == nil {
  353. clog.Debugf("[Leave] objectID = %d not found.", objectID)
  354. return
  355. }
  356. // remove object
  357. p.objects.Remove(objectID)
  358. tower := p.GetTowerByPoint(object.Point.X, object.Point.Y)
  359. if tower == nil {
  360. return
  361. }
  362. // object clean
  363. defer object.Clean()
  364. if object.IsWatcher() {
  365. rangeTowers := p.GetRangeTowers(object.Point.X, object.Point.Y)
  366. rangeTowers.Range(func(tower *Tower) {
  367. tower.DelWatcher(object)
  368. })
  369. // 清理 pending
  370. delete(p.pendingWatchers, objectID)
  371. }
  372. if object.IsMarker() {
  373. // 触发回调:通知观察该灯塔的所有观察者有marker离开
  374. if p.OnMarkerLeave != nil {
  375. p.OnMarkerLeave(object, tower)
  376. }
  377. tower.DelMarker(object)
  378. }
  379. }
  380. // Tick 定时清理过期的延迟观察者
  381. // 参数:
  382. // - now: 当前时间戳(秒)
  383. //
  384. // 性能:O(M),M为有延迟任务的观察者数量
  385. func (p *AOI) Tick(now int64) {
  386. if len(p.pendingWatchers) == 0 {
  387. return
  388. }
  389. for watcherID := range p.pendingWatchers {
  390. watcher := p.objects.Get(watcherID)
  391. if watcher == nil {
  392. delete(p.pendingWatchers, watcherID)
  393. continue
  394. }
  395. // 检查是否有过期任务
  396. delayMap := watcher.GetDelayLeaveTowers()
  397. if len(delayMap) == 0 {
  398. delete(p.pendingWatchers, watcherID)
  399. continue
  400. }
  401. for towerID, expireTime := range delayMap {
  402. if now >= expireTime {
  403. // 过期,执行真正的移除
  404. // 根据 ID 计算灯塔坐标
  405. // ID = tx * towerNumY + ty
  406. tx := towerID / p.towerNumY
  407. ty := towerID % p.towerNumY
  408. if towerMap, ok := p.towers[tx]; ok {
  409. if tower, ok := towerMap[ty]; ok && tower.id == towerID {
  410. if p.OnTowerExpire != nil {
  411. p.OnTowerExpire(watcher, tower)
  412. }
  413. tower.DelWatcher(watcher)
  414. }
  415. }
  416. delete(delayMap, towerID)
  417. }
  418. }
  419. if len(delayMap) == 0 {
  420. delete(p.pendingWatchers, watcherID)
  421. }
  422. }
  423. }
  424. // GetWatcherOwners 获取一个被观察者的所有观察者ownerID
  425. // 参数:
  426. // - objectID: 被观察者的唯一标识符
  427. //
  428. // 返回值:所有观察者的ownerID 集合
  429. // 用途:查询某个被观察者是否被哪些玩家看到
  430. // 处理流程:
  431. // 1. 查找对象并验证存在性
  432. // 2. 确保是被观察者
  433. // 3. 获取其所在灯塔
  434. // 4. 从灯塔的所有观察者中收集其ownerID
  435. //
  436. // 性能:O(m) 时间复杂度
  437. // 注意:ownerID是观察者的所有者ID,不是对象ID
  438. func (p *AOI) GetWatcherOwners(objectID int64) types.Set[int64] {
  439. owners := types.NewSet[int64]()
  440. object := p.objects.Get(objectID)
  441. if object == nil {
  442. return owners
  443. }
  444. if object.IsWatcher() {
  445. clog.Warnf("[GetWatchers] objectID = {%s} is a watcher. get fail.", object.Info())
  446. return owners
  447. }
  448. thisTower := p.GetTowerByPoint(object.Point.X, object.Point.Y)
  449. if thisTower == nil {
  450. clog.Warnf("[GetWatchers] objectID = {%s}, old tower not found. %s", object.Info())
  451. return owners
  452. }
  453. for _, watcher := range thisTower.watchers {
  454. owners.Add(watcher.OwnerID())
  455. }
  456. return owners
  457. }
  458. // newTower 创建一个新的灯塔对象
  459. // 参数:
  460. // - id: 灯塔的唯一标识符
  461. // - tx: 灯塔的网格坐标 X(不是场景坐标)
  462. // - ty: 灯塔的网格坐标 Y(不是场景坐标)
  463. //
  464. // 返回值:指向新创建的 Tower 对象的指针
  465. // 关键逻辑:
  466. // 1. 根据灯塔坐标计算灯塔的场景坐标范围(minX, minY, maxX, maxY)
  467. // 2. 坐标计算考虑 TowerPointOffset 来扩展灯塔边界(用于 AOI 计算)
  468. // 3. 初始化灯塔的观察者和被观察者列表为空
  469. func (p *AOI) newTower(id, tx, ty int32) *Tower {
  470. var (
  471. // 计算灯塔的最小坐标(左上角)
  472. // 公式:灯塔坐标 * 灯塔大小 - 偏移值
  473. // TowerPointOffset 的作用:扩大灯塔边界以覆盖更多的 AOI 范围
  474. minX = tx*p.config.TowerWidth - p.config.TowerPointOffset
  475. minY = ty*p.config.TowerHeight - p.config.TowerPointOffset
  476. // 计算灯塔的最大坐标(右下角)
  477. maxX = minX + p.config.TowerWidth
  478. maxY = minY + p.config.TowerHeight
  479. )
  480. // 调试输出:打印灯塔信息(需要在 Config 中启用 PrintTowerInfo)
  481. if p.config.PrintTowerInfo {
  482. clog.Infof("id = %d, min(X = %d, Y = %d), max(X = %d, Y = %d)", id, minX, minY, maxX, maxY)
  483. }
  484. return &Tower{
  485. id: id,
  486. markers: Objects{}, // 该灯塔内的被观察者列表
  487. watchers: Objects{}, // 观察该灯塔的观察者列表
  488. minX: minX,
  489. minY: minY,
  490. maxX: maxX,
  491. maxY: maxY,
  492. }
  493. }
  494. // CheckPoint 检查场景坐标是否在地图范围内
  495. // 参数:
  496. // - x: 场景坐标 X
  497. // - y: 场景坐标 Y
  498. //
  499. // 返回值:坐标是否合法(true 表示在地图范围内 [0, Width) x [0, Height))
  500. // 关键点:使用 < 而不是 <=,地图的有效范围是 [0, Width)
  501. func (p *AOI) CheckPoint(x, y int32) bool {
  502. return x >= 0 && y >= 0 && x < p.config.Width && y < p.config.Height
  503. }
  504. // InArea 检查点是否在指定的矩形区域内
  505. // 参数:
  506. // - x, y: 检查的场景坐标
  507. // - sx, sy: 区域的起始坐标(包含)
  508. // - ex, ey: 区域的结束坐标(包含)
  509. //
  510. // 返回值:点是否在区域内
  511. // 注意:此方法使用 <= 作为边界判断,包含边界点
  512. func (_ *AOI) InArea(x, y int32, sx, sy, ex, ey int32) bool {
  513. return x >= sx && x <= ex && y >= sy && y <= ey
  514. }
  515. // ToTowerPoint 将场景坐标转换为灯塔网格坐标
  516. // 参数:
  517. // - x: 场景坐标 X
  518. // - y: 场景坐标 Y
  519. //
  520. // 返回值:
  521. // - tx: 灯塔网格坐标 X
  522. // - ty: 灯塔网格坐标 Y
  523. //
  524. // 示例:TowerWidth=3,TowerHeight=3
  525. //
  526. // 场景坐标 (5, 7) => 灯塔坐标 (1, 2)
  527. // 场景坐标 (0, 0) => 灯塔坐标 (0, 0)
  528. // 场景坐标 (3, 3) => 灯塔坐标 (1, 1)
  529. func (p *AOI) ToTowerPoint(x, y int32) (tx int32, ty int32) {
  530. tx = x / p.config.TowerWidth
  531. ty = y / p.config.TowerHeight
  532. return
  533. }
  534. // GetTower 根据灯塔网格坐标获取灯塔对象
  535. // 参数:
  536. // - tx: 灯塔网格坐标 X
  537. // - ty: 灯塔网格坐标 Y
  538. //
  539. // 返回值:灯塔对象指针,如果灯塔不存在则返回 nil
  540. // 性能:O(1) 时间复杂度(map 查询)
  541. func (p *AOI) GetTower(tx, ty int32) *Tower {
  542. if tower, found := p.towers[tx][ty]; found {
  543. return tower
  544. }
  545. return nil
  546. }
  547. // GetTowerByPoint 根据场景坐标获取所属的灯塔对象
  548. // 参数:
  549. // - x: 场景坐标 X
  550. // - y: 场景坐标 Y
  551. //
  552. // 返回值:灯塔对象指针,如果不存在则返回 nil
  553. // 工作流程:场景坐标 -> 灯塔坐标 -> 获取灯塔对象
  554. // 性能:O(1) 时间复杂度
  555. func (p *AOI) GetTowerByPoint(x, y int32) *Tower {
  556. tx, ty := p.ToTowerPoint(x, y)
  557. return p.GetTower(tx, ty)
  558. }
  559. // GetDiffTowers 计算从旧位置到新位置视野范围的变化
  560. // 参数:
  561. // - ox, oy: 旧位置的场景坐标
  562. // - nx, ny: 新位置的场景坐标
  563. //
  564. // 返回值:
  565. // - addTowers: 新增的灯塔(在新位置可见但旧位置不可见)
  566. // - delTowers: 删除的灯塔(在旧位置可见但新位置不可见)
  567. //
  568. // 用途:当玩家移动时,计算需要新增或删除的 AOI 对象
  569. // 性能:O(n) 时间复杂度,n 为灯塔总数
  570. func (p *AOI) GetDiffTowers(ox, oy, nx, ny int32) (Towers, Towers) {
  571. // 获取旧位置的视野范围和新位置的视野范围
  572. oldTowers := p.GetRangeTowers(ox, oy)
  573. newTowers := p.GetRangeTowers(nx, ny)
  574. // 计算新增灯塔:在 newTowers 中但不在 oldTowers 中的灯塔
  575. // 这些灯塔内的被观察者需要下发给观察者
  576. addTowers := make(Towers)
  577. for id, tower := range newTowers {
  578. if !oldTowers.Contains(id) {
  579. addTowers[id] = tower
  580. }
  581. }
  582. // 计算删除灯塔:在 oldTowers 中但不在 newTowers 中的灯塔
  583. // 这些灯塔内的被观察者将不再可见
  584. delTowers := make(Towers)
  585. for id, tower := range oldTowers {
  586. if !newTowers.Contains(id) {
  587. delTowers.Add(tower)
  588. }
  589. }
  590. return addTowers, delTowers
  591. }
  592. // GetRangeTowers 获取指定位置观察者的视野范围内的所有灯塔
  593. // 参数:
  594. // - x: 观察者的场景坐标 X
  595. // - y: 观察者的场景坐标 Y
  596. //
  597. // 返回值:视野范围内的所有灯塔集合
  598. // 工作流程:
  599. // 1. 场景坐标 -> 灯塔坐标
  600. // 2. 灯塔坐标 -> 视野范围(基于 TowerViewRange)
  601. // 3. 遍历范围内的所有灯塔
  602. //
  603. // 性能:O(m) 时间复杂度,m 为视野范围内的灯塔数量(通常为常数)
  604. // 注意:根据 TowerViewRange 的值,视野范围会包含 (2*TowerViewRange+1)² 个灯塔
  605. func (p *AOI) GetRangeTowers(x, y int32) Towers {
  606. // 第一步:将场景坐标转换为灯塔坐标
  607. // 例如:场景点 (5,7),灯塔大小 3x3 => 灯塔坐标 (1,2)
  608. tx, ty := p.ToTowerPoint(x, y)
  609. // 第二步:获取视野范围内的灯塔坐标范围
  610. // TowerViewRange 决定了观察者能看到多少周围的灯塔
  611. // 例如:TowerViewRange=1,从 (1,2) 可以看到 (0-2, 1-3) 9 个灯塔
  612. sx, sy, ex, ey := p.GetRangeTowerPoint(tx, ty)
  613. // 第三步:遍历范围内的所有灯塔并添加到结果集
  614. towers := Towers{}
  615. for x := sx; x <= ex; x++ {
  616. for y := sy; y <= ey; y++ {
  617. if rangeTower := p.GetTower(x, y); rangeTower != nil {
  618. towers.Add(rangeTower)
  619. }
  620. }
  621. }
  622. return towers
  623. }
  624. // GetRangeTowerPoint 计算灯塔坐标的视野范围边界(截断模式)
  625. // 参数:
  626. // - tx: 灯塔网格坐标 X
  627. // - ty: 灯塔网格坐标 Y
  628. //
  629. // 返回值:
  630. // - sx: 视野范围的起始灯塔坐标 X(包含)
  631. // - sy: 视野范围的起始灯塔坐标 Y(包含)
  632. // - ex: 视野范围的结束灯塔坐标 X(包含)
  633. // - ey: 视野范围的结束灯塔坐标 Y(包含)
  634. //
  635. // 计算公式:
  636. //
  637. // 视野范围 = [tx - TowerViewRange, tx + TowerViewRange] x [ty - TowerViewRange, ty + TowerViewRange]
  638. //
  639. // 边界处理策略:截断模式(靠近边界时视野会缩小)
  640. //
  641. // 可见灯塔数量对比:
  642. //
  643. // 中心位置(无边界限制):
  644. // - TowerViewRange=1 时:3×3 = 9 个灯塔
  645. // - TowerViewRange=2 时:5×5 = 25 个灯塔
  646. // 边界位置(观察点在地图边缘):
  647. // - TowerViewRange=1 时:2×2 = 4 个灯塔 ⚠️
  648. // - TowerViewRange=2 时:3×3 = 9 个灯塔 ⚠️
  649. //
  650. // 特点:
  651. // - 靠近地图边界时,视野范围会缩小
  652. // - 边界处玩家看到的灯塔数与中心位置不同,可能影响游戏体验和公平性
  653. //
  654. // 适用场景:
  655. // - 单机游戏
  656. // - 边界差异可接受的场景
  657. // - 边界处有地图边界墙壁等遮挡物
  658. //
  659. // 性能:O(1) 时间复杂度
  660. //
  661. // 对比:GetRangeTowerPointNew 采用平移模式,保持所有位置视野大小一致(推荐用于公平的多人游戏)
  662. func (p *AOI) GetRangeTowerPoint(tx, ty int32) (sx, sy, ex, ey int32) {
  663. // 基于 TowerViewRange 计算视野范围(中心灯塔 ± TowerViewRange)
  664. sx = tx - p.config.TowerViewRange
  665. ex = tx + p.config.TowerViewRange
  666. sy = ty - p.config.TowerViewRange
  667. ey = ty + p.config.TowerViewRange
  668. // 左边界修正:不能小于 0
  669. if sx < 0 {
  670. sx = 0
  671. }
  672. // 上边界修正:不能小于 0
  673. if sy < 0 {
  674. sy = 0
  675. }
  676. // 右边界修正:灯塔数组上界是 towerNumX-1
  677. // 关键:使用 >= 而不是 >,因为 map 的合法 key 范围是 [0, towerNumX-1]
  678. if ex >= p.towerNumX {
  679. ex = p.towerNumX - 1
  680. }
  681. // 下边界修正:灯塔数组上界是 towerNumY-1
  682. if ey >= p.towerNumY {
  683. ey = p.towerNumY - 1
  684. }
  685. return sx, sy, ex, ey
  686. }
  687. // GetRangeTowerPointNew 计算灯塔坐标视野范围,保持视野一致性(平移模式)
  688. // 参数:
  689. // - tx: 灯塔网格坐标 X
  690. // - ty: 灯塔网格坐标 Y
  691. //
  692. // 返回值:
  693. // - sx, sy: 视野范围的起始坐标(包含)
  694. // - ex, ey: 视野范围的结束坐标(包含)
  695. //
  696. // 计算公式:
  697. //
  698. // 完整视野宽度 = 2 * TowerViewRange + 1 个灯塔
  699. //
  700. // 边界处理策略:平移模式(靠近边界时向相反方向平移以保持视野大小)
  701. //
  702. // 可见灯塔数量对比:
  703. //
  704. // 中心位置(无边界限制):
  705. // - TowerViewRange=1 时:3×3 = 9 个灯塔
  706. // - TowerViewRange=2 时:5×5 = 25 个灯塔
  707. // 边界位置(观察点在地图边缘):
  708. // - TowerViewRange=1 时:3×3 = 9 个灯塔 ✅
  709. // - TowerViewRange=2 时:5×5 = 25 个灯塔 ✅
  710. //
  711. // 特点:
  712. // - 无论观察点在地图任何位置,视野大小始终保持一致
  713. // - 边界处玩家看到的灯塔数与中心位置相同
  714. // - 保证游戏的公平性和用户体验一致
  715. //
  716. // 适用场景:
  717. // - SLG 多人游戏(推荐)
  718. // - 需要保证所有玩家视野相同的游戏
  719. // - 自由拖动屏幕观察地图的游戏
  720. //
  721. // 性能:O(1) 时间复杂度
  722. //
  723. // 对比:GetRangeTowerPoint 采用截断模式,靠近边界时视野会缩小(不推荐用于多人游戏)
  724. func (p *AOI) GetRangeTowerPointNew(tx, ty int32) (sx, sy, ex, ey int32) {
  725. // towerViewRange2 = 2 * TowerViewRange,用于计算完整的视野宽度
  726. // 解释:
  727. // - TowerViewRange 是观察者向一个方向能看到的灯塔距离
  728. // - 视野范围 = [tx - TowerViewRange, tx + TowerViewRange]
  729. // - 视野宽度 = TowerViewRange + 1(中心灯塔) + TowerViewRange = 2*TowerViewRange + 1
  730. // - 边界处理时,需要 2*TowerViewRange 来保持视野范围的大小
  731. // 例如:TowerViewRange=2,则 towerViewRange2=4
  732. // - 完整视野宽度 = 4 + 1 = 5 个灯塔 [0, 4] 或 [3, 7] 等
  733. towerViewRange2 := 2 * p.config.TowerViewRange
  734. // X 轴方向的视野范围计算:采用对称的边界平移策略
  735. if tx < p.config.TowerViewRange {
  736. // 观察者靠近左边界:向右平移范围以保持宽度
  737. sx = 0
  738. ex = towerViewRange2
  739. } else if tx >= p.towerNumX-p.config.TowerViewRange {
  740. // 观察者靠近右边界:向左平移范围以保持宽度
  741. sx = p.towerNumX - towerViewRange2 - 1
  742. ex = p.towerNumX - 1
  743. } else {
  744. // 正常情况:以观察者为中心对称扩展
  745. sx = tx - p.config.TowerViewRange
  746. ex = tx + p.config.TowerViewRange
  747. }
  748. // Y 轴方向的视野范围计算(逻辑与 X 轴完全相同)
  749. if ty < p.config.TowerViewRange {
  750. // 观察者靠近上边界:向下平移范围以保持高度
  751. sy = 0
  752. ey = towerViewRange2
  753. } else if ty >= p.towerNumY-p.config.TowerViewRange {
  754. // 观察者靠近下边界:向上平移范围以保持高度
  755. sy = p.towerNumY - towerViewRange2 - 1
  756. ey = p.towerNumY - 1
  757. } else {
  758. // 正常情况:以观察者为中心对称扩展
  759. sy = ty - p.config.TowerViewRange
  760. ey = ty + p.config.TowerViewRange
  761. }
  762. return sx, sy, ex, ey
  763. }
  764. // SetOnWatcherEnter 设置观察者进入时的回调
  765. // 参数:callback 为 func(watcher *aoi.Object, towers Towers) 形式
  766. func (p *AOI) SetOnWatcherEnter(callback OnWatcherEnterCallback) {
  767. p.OnWatcherEnter = callback
  768. }
  769. // SetOnWatcherMove 设置观察者移动时的回调
  770. // 参数:callback 为 func(watcher *aoi.Object, towers Towers) 形式
  771. func (p *AOI) SetOnWatcherMove(callback OnWatcherMoveCallback) {
  772. p.OnWatcherMove = callback
  773. }
  774. // SetOnMarkerEnter 设置被观察者进入时的回调
  775. // 参数:callback 为 func(marker *aoi.Object, tower *Tower) 形式
  776. func (p *AOI) SetOnMarkerEnter(callback OnMarkerEnterCallback) {
  777. p.OnMarkerEnter = callback
  778. }
  779. // SetOnMarkerMove 设置被观察者移动时的回调
  780. // 参数:callback 为 func(marker *aoi.Object, newTower *Tower, oldTower *Tower) 形式
  781. func (p *AOI) SetOnMarkerMove(callback OnMarkerMoveCallback) {
  782. p.OnMarkerMove = callback
  783. }
  784. // SetOnMarkerUpdate 设置被观察者更新时的回调
  785. // 参数:callback 为 func(marker *aoi.Object, tower *Tower) 形式
  786. func (p *AOI) SetOnMarkerUpdate(callback OnMarkerUpdateCallback) {
  787. p.OnMarkerUpdate = callback
  788. }
  789. // SetOnMarkerUpdateID 设置被观察者更新时的回调(带UpdateID)
  790. // 参数:callback 为 func(marker *aoi.Object, tower *Tower, updateID uint32, funcName string) 形式
  791. func (p *AOI) SetOnMarkerUpdateID(callback OnMarkerUpdateIDCallback) {
  792. p.OnMarkerUpdateID = callback
  793. }
  794. // SetOnMarkerLeave 设置被观察者离开时的回调
  795. // 参数:callback 为 func(marker *aoi.Object, tower *Tower) 形式
  796. func (p *AOI) SetOnMarkerLeave(callback OnMarkerLeaveCallback) {
  797. p.OnMarkerLeave = callback
  798. }