package aoiTower import ( "f1-game/internal/types" "math" clog "github.com/cherry-game/cherry/logger" ) type ( Config struct { Width int32 // 地图宽(x) Height int32 // 地图高(y) TowerWidth int32 // 灯塔宽(x) TowerHeight int32 // 灯塔高(y) TowerPointOffset int32 // 灯塔坐标偏移值(用于扩展或收缩灯塔边界) TowerViewRange int32 // 观察者所在灯塔周围能看到多少圈灯塔,必须 >= 1 WatcherLeaveDelay int64 // 观察者离开灯塔的延迟时间(秒),用于消除边界抖动 PrintTowerInfo bool // 初始化时,是否打印Tower的信息 } // AOI灯塔系统 AOI struct { config Config // 配置信息 towerNumX int32 // x轴的灯塔数量 towerNumY int32 // y轴的灯塔数量 towers map[int32]map[int32]*Tower // 灯塔列表 key:灯塔x, value:{key:灯塔y, value:*Tower} objects Objects // 所有对象 key:ObjectID, value:*aoi.Object pendingWatchers map[int64]struct{} // 待处理(延迟离开)的观察者ID集合 OnWatcherEnter OnWatcherEnterCallback // 观察者-[进入时]的回调 OnWatcherMove OnWatcherMoveCallback // 观察者-[移动时]的回调 OnMarkerEnter OnMarkerEnterCallback // 被观察者-[进入时]的回调 OnMarkerMove OnMarkerMoveCallback // 被观察者-[移动时]的回调 OnMarkerUpdate OnMarkerUpdateCallback // 被观察者-[更新时]的回调 OnMarkerLeave OnMarkerLeaveCallback // 被观察者-[退出时]的回调 OnMarkerUpdateID OnMarkerUpdateIDCallback // 被观察者-[更新时]的回调(带UpdateID) OnTowerExpire OnTowerExpireCallback // 灯塔过期的回调 } OnWatcherEnterCallback func(watcher *Object, towers Towers) OnWatcherMoveCallback func(watcher *Object, newTowers Towers, oldTowers Towers) OnMarkerEnterCallback func(marker *Object, tower *Tower) OnMarkerMoveCallback func(marker *Object, newTowers *Tower, oldTowers *Tower) OnMarkerUpdateCallback func(marker *Object, tower *Tower) OnMarkerLeaveCallback func(marker *Object, tower *Tower) OnMarkerUpdateIDCallback func(marker *Object, tower *Tower, updateID uint32, funcName string) OnTowerExpireCallback func(watcher *Object, tower *Tower) ) // AOI 灯塔 AOI 系统 // 核心概念: // - Watcher(观察者):玩家,有视野范围,需要了解视野内的被观察者 // - Marker(被观察者):NPC、怪物等,被观察者只属于一个灯塔 // - Tower(灯塔):地图划分的网格单元,管理其范围内的观察者和被观察者 // // 工作原理: // 1. 初始化时,根据 TowerWidth 和 TowerHeight 将地图划分为网格 // 2. 观察者进入时,注册到其视野范围内的所有灯塔 // 3. 被观察者进入时,注册到其所属的单个灯塔 // 4. 对象移动时,更新其所属灯塔并触发 AOI 变化回调 // // New 创建一个新的灯塔形 AOI 系统实例 // 参数: // - config: AOI 系统配置(包含地图大小、灯塔尺寸、视野范围等) // // 返回值:aoi.AOI 接口实现指针 // 用途:初始化一个完整的灯塔 AOI 系统 // 工作流程: // 1. 配置校验 // 2. 计算灯塔个数 // 3. 初始化所有灯塔对象 // // 性能:O(towerNumX * towerNumY) 时间复杂度 func New(config Config) *AOI { towerAOI := &AOI{ config: config, towers: make(map[int32]map[int32]*Tower), objects: Objects{}, pendingWatchers: make(map[int64]struct{}), } towerAOI.init() return towerAOI } // init 初始化灯塔 // 工作流程: // 1. 验证配置参数的合法性(地图尺寸、灯塔尺寸、视野范围等) // 2. 计算灯塔网格的行列数 // 3. 创建所有灯塔对象并初始化二维 map 结构 // // 关键点: // - 配置校验确保后续计算不会出现除零等错误 // - 使用向上取整确保灯塔网格能覆盖整个地图 // - 每个灯塔都分配唯一的 ID func (p *AOI) init() { // 地图大小校验:宽高都必须 >= 1 if p.config.Width < 1 || p.config.Height < 1 { clog.Panicf("Width and Height must be >= 1, got %v", p.config.Width, p.config.Height) } // TowerWidth 校验:必须 >= 1,且不能超过地图宽度 // 原因:灯塔是网格划分单位,宽度为 0 会导致除以零错误;超过地图宽度无意义 if p.config.TowerWidth < 1 || p.config.TowerWidth > p.config.Width { clog.Panicf("TowerWidth must be 1 to %d, got %d", p.config.Width, p.config.TowerWidth) } // TowerHeight 校验:必须 >= 1,且不能超过地图高度 // 原因:灯塔是网格划分单位,高度为 0 会导致除以零错误;超过地图高度无意义 if p.config.TowerHeight < 1 || p.config.TowerHeight > p.config.Height { clog.Panicf("TowerHeight must be 1 to %d, got %d", p.config.Height, p.config.TowerHeight) } // TowerPointOffset:坐标级别的偏移,用于扩展或收缩灯塔的坐标边界,不能为负 if p.config.TowerPointOffset < 0 { clog.Panicf("TowerPointOffset must be >= 0, got %d", p.config.TowerPointOffset) } // TowerViewRange:灯塔级别的视野范围(以灯塔为单位),必须 >= 1 if p.config.TowerViewRange < 1 { clog.Panicf("TowerViewRange must be >= 1, got %d", p.config.TowerViewRange) } // 计算灯塔网格数量:使用 Ceil 向上取整 // 例如:地图宽=10,灯塔宽=3 => towerNumX = Ceil(10/3) = 4 // 这样可以覆盖整个地图(3,3,3,1) p.towerNumX = int32(math.Ceil(float64(p.config.Width) / float64(p.config.TowerWidth))) p.towerNumY = int32(math.Ceil(float64(p.config.Height) / float64(p.config.TowerHeight))) // 初始化灯塔网络:创建所有灯塔对象 // 灯塔存储在二维 map 中:towers[towerX][towerY] = *Tower towerID := int32(0) p.towers = make(map[int32]map[int32]*Tower, p.towerNumX) for tx := int32(0); tx < p.towerNumX; tx++ { p.towers[tx] = make(map[int32]*Tower, p.towerNumY) for ty := int32(0); ty < p.towerNumY; ty++ { p.towers[tx][ty] = p.newTower(towerID, tx, ty) towerID++ } } } // GetObject 根据对象ID 获取已进入 AOI 系统的对象 // 参数: // - objectID: 对象的唯一标识符 // // 返回值:对象指针,如果不存在则返回 nil // 用途:查询已进入 AOI 系统的对象 // 性能:O(1) 时间复杂度 func (p *AOI) GetObject(objectID int64) *Object { return p.objects.Get(objectID) } // Enter 对象进入 AOI 系统 // 参数: // - object: 要进入系统的对象(观察者或被观察者) // // 返回值:是否成功进入(true 表示进入成功) // 用途:初始化对象在 AOI 系统中的位置和属性 // 处理流程: // 1. 坐标校验 - 确保对象在地图范围内 // 2. 重复检查 - 防止同一对象重复进入 // 3. 处理流程: // 4. 如果是观察者 - 注册到视野范围内的所有灯塔,触发 OnWatcherEnter 回调 // 5. 如果是被观察者 - 注册到所属灯塔(业务层脏标记标记会在观察者回调中触发) // // 性能:O(m) 时间复杂度,m 为视野范围内的灯塔数量 // 注意:观察者和被观察者的处理流程完全不同 func (p *AOI) Enter(object Object) bool { // 第一步:坐标校验 - 确保对象在地图范围内 if !p.CheckPoint(object.Point.X, object.Point.Y) { clog.Warnf("[Enter] check point error. %s", object.Info()) return false } // 第二步:重复检查 - 防止同一对象重复进入 if p.objects.Contains(object.ObjectID) { clog.Warnf("[Enter] object has entered. %s", object.Info()) return false } // 第三步:处理观察者(Watcher)- 获取视野范围内的所有灯塔 if object.IsWatcher() { towers := p.GetRangeTowers(object.Point.X, object.Point.Y) if towers.Len() < 1 { clog.Warnf("[Enter] get tower fail. %s", object.Info()) return false } p.objects.Add(&object) // 观察者加入到所有可见灯塔的观察者列表 towers.Range(func(tower *Tower) { tower.AddWatcher(&object) }) // 触发回调:报告 Watcher 进入 if p.OnWatcherEnter != nil { p.OnWatcherEnter(&object, towers) } } // 第四步:处理被观察者(Marker)- 被观察者只属于一个灯塔 if object.IsMarker() { tower := p.GetTowerByPoint(object.Point.X, object.Point.Y) if tower == nil { clog.Warnf("[Enter] get tower fail. %s", object.Info()) return false } p.objects.Add(&object) tower.AddMarker(&object) // 触发回调:通知观察该灯塔的所有观察者有新marker进入 if p.OnMarkerEnter != nil && len(tower.watchers) > 0 { p.OnMarkerEnter(&object, tower) } } return true } // Update 更新被观察者(Marker)的状态 // 参数: // - object: 要更新的被观察者对象(必须是 IsMarker() 返回 true) // // 返回值:是否成功更新(true 表示更新成功) // 用途:当被观察者的状态变化时调用(如位置更新但未变更灯塔、属性变化等) // 处理流程: // 1. 类型检查 - 只有被观察者可以调用此方法 // 2. 获取被观察者所属的灯塔 // 3. 触发 OnMarkerUpdate 回调,通知观察该灯塔的所有观察者有 marker 更新 // // 性能:O(m) 时间复杂度,m 为灯塔中观察者的数量 // 注意: // - 此方法用于 marker 在同一灯塔内的状态更新,不改变灯塔关联 // - 如果 marker 需要移动到新灯塔,应使用 Move() 方法 // - 常见场景:部队属性变化、血量变化等需要实时通知观察者的状态更新 func (p *AOI) Update(object Object) bool { if object.IsWatcher() { clog.Warnf("[Update]Only the marker object can call updates. %s", object.Info()) return false } tower := p.GetTowerByPoint(object.Point.X, object.Point.Y) if tower == nil { clog.Warnf("[Update] Get tower fail. %s", object.Info()) return false } // 触发回调:通知观察该灯塔的所有观察者有marker更新 if p.OnMarkerUpdate != nil && len(tower.watchers) > 0 { p.OnMarkerUpdate(&object, tower) } return true } // UpdateID 更新被观察者(Marker)的状态(带UpdateID) // 参数: // - object: 要更新的被观察者对象 // - updateID: 更新操作的唯一标识符 // - funcName: 触发更新的函数名 func (p *AOI) UpdateID(object Object, updateID uint32, funcName string) bool { if object.IsWatcher() { clog.Warnf("[UpdateID]Only the marker object can call updates. %s", object.Info()) return false } tower := p.GetTowerByPoint(object.Point.X, object.Point.Y) if tower == nil { clog.Warnf("[UpdateID] Get tower fail. %s", object.Info()) return false } // 触发回调:通知观察该灯塔的所有观察者有marker更新 if p.OnMarkerUpdateID != nil && len(tower.watchers) > 0 { p.OnMarkerUpdateID(&object, tower, updateID, funcName) } return true } // 参数: // - objectID: 对象的唯一标识符 // - x: 新位置的场景坐标 X // - y: 新位置的场景坐标 Y // - now: 当前时间戳(秒),用于设置延迟离开的过期时间 // // 返回值:是否成功移动(是否改变了灯塔) // 用途:实时更新对象位置并计算 AOI 变化 func (p *AOI) Move(objectID int64, x, y int32, now int64) bool { // 第一步:获取对象,检查是否存在 object := p.objects.Get(objectID) if object == nil { return false } // 第二步:检查是否真的有位置变化 if object.Point.X == x && object.Point.Y == y { return false } // 第三步:坐标校验 if !p.CheckPoint(x, y) { clog.Warnf("[Move] Check point error. %s", object.Info()) return false } // 第四步:获取旧灯塔位置 oldTower := p.GetTowerByPoint(object.Point.X, object.Point.Y) if oldTower == nil { clog.Warnf("[Move] Old tower not found. %s", object.Info()) return false } // 第五步:获取新灯塔位置 newTower := p.GetTowerByPoint(x, y) if newTower == nil { clog.Warnf("[Move] New tower not found. %s", object.Info()) return false } // 保存旧的xy坐标备用 var ( oldX = object.Point.X oldY = object.Point.Y ) // 第六步:更新对象位置 object.SetPoint(x, y) // 第七步:快速路径 - 如果灯塔没有改变,则不需要同步 // 关键优化:同一灯塔内的移动不会改变 AOI 可见性 if oldTower.id == newTower.id { return false } // 第八步:处理观察者移动 - 计算视野变化并更新灯塔关联 if object.IsWatcher() { addTowers, delTowers := p.GetDiffTowers(oldX, oldY, x, y) // 处理新增的灯塔 addTowers.Range(func(tower *Tower) { // 检查是否处于延迟离开状态(重入) if _, ok := object.delayLeaveTowers[tower.id]; ok { // 取消延迟离开 object.RemoveDelayLeaveTower(tower.id) // 已在视野里,从新增里移除 addTowers.Remove(tower.id) return // 已经在 tower.watchers 中,无需再次添加 } // 真正的 Assume Add tower.AddWatcher(object) }) // 处理删除的灯塔 (延迟离开) delTowers.Range(func(tower *Tower) { // 添加到延迟列表,不立即移除 object.AddDelayLeaveTower(tower.id, now+p.config.WatcherLeaveDelay) p.pendingWatchers[objectID] = _nilStruct }) // 触发回调:Watcher视野变化 if p.OnWatcherMove != nil { p.OnWatcherMove(object, addTowers, delTowers) } return true } // 第十步:处理被观察者移动 // 被观察者只能在一个灯塔内,需要更新其所属灯塔 if object.IsMarker() { // 仅有灯塔改变时才进行处理 if oldTower.id != newTower.id { oldTower.DelMarker(object) newTower.AddMarker(object) } if p.OnMarkerMove != nil && (newTower.watchers.Size() > 0 || oldTower.watchers.Size() > 0) { p.OnMarkerMove(object, newTower, oldTower) } return true } return false } // Leave 对象离开 AOI 系统 // 参数: // - objectID: 对象的唯一标识符 // // 返回值:无 // 用途:当对象退出游戏世界时调用 // 处理流程: // 1. 查找并删除对象 // 2. 离开观察者 - 从视野范围内的所有灯塔中删除 (忽略延迟,立即清理) // 3. 离开被观察者 - 直接通知 func (p *AOI) Leave(objectID int64) { object := p.objects.Get(objectID) if object == nil { clog.Debugf("[Leave] objectID = %d not found.", objectID) return } // remove object p.objects.Remove(objectID) tower := p.GetTowerByPoint(object.Point.X, object.Point.Y) if tower == nil { return } // object clean defer object.Clean() if object.IsWatcher() { rangeTowers := p.GetRangeTowers(object.Point.X, object.Point.Y) rangeTowers.Range(func(tower *Tower) { tower.DelWatcher(object) }) // 清理 pending delete(p.pendingWatchers, objectID) } if object.IsMarker() { // 触发回调:通知观察该灯塔的所有观察者有marker离开 if p.OnMarkerLeave != nil { p.OnMarkerLeave(object, tower) } tower.DelMarker(object) } } // Tick 定时清理过期的延迟观察者 // 参数: // - now: 当前时间戳(秒) // // 性能:O(M),M为有延迟任务的观察者数量 func (p *AOI) Tick(now int64) { if len(p.pendingWatchers) == 0 { return } for watcherID := range p.pendingWatchers { watcher := p.objects.Get(watcherID) if watcher == nil { delete(p.pendingWatchers, watcherID) continue } // 检查是否有过期任务 delayMap := watcher.GetDelayLeaveTowers() if len(delayMap) == 0 { delete(p.pendingWatchers, watcherID) continue } for towerID, expireTime := range delayMap { if now >= expireTime { // 过期,执行真正的移除 // 根据 ID 计算灯塔坐标 // ID = tx * towerNumY + ty tx := towerID / p.towerNumY ty := towerID % p.towerNumY if towerMap, ok := p.towers[tx]; ok { if tower, ok := towerMap[ty]; ok && tower.id == towerID { if p.OnTowerExpire != nil { p.OnTowerExpire(watcher, tower) } tower.DelWatcher(watcher) } } delete(delayMap, towerID) } } if len(delayMap) == 0 { delete(p.pendingWatchers, watcherID) } } } // GetWatcherOwners 获取一个被观察者的所有观察者ownerID // 参数: // - objectID: 被观察者的唯一标识符 // // 返回值:所有观察者的ownerID 集合 // 用途:查询某个被观察者是否被哪些玩家看到 // 处理流程: // 1. 查找对象并验证存在性 // 2. 确保是被观察者 // 3. 获取其所在灯塔 // 4. 从灯塔的所有观察者中收集其ownerID // // 性能:O(m) 时间复杂度 // 注意:ownerID是观察者的所有者ID,不是对象ID func (p *AOI) GetWatcherOwners(objectID int64) types.Set[int64] { owners := types.NewSet[int64]() object := p.objects.Get(objectID) if object == nil { return owners } if object.IsWatcher() { clog.Warnf("[GetWatchers] objectID = {%s} is a watcher. get fail.", object.Info()) return owners } thisTower := p.GetTowerByPoint(object.Point.X, object.Point.Y) if thisTower == nil { clog.Warnf("[GetWatchers] objectID = {%s}, old tower not found. %s", object.Info()) return owners } for _, watcher := range thisTower.watchers { owners.Add(watcher.OwnerID()) } return owners } // newTower 创建一个新的灯塔对象 // 参数: // - id: 灯塔的唯一标识符 // - tx: 灯塔的网格坐标 X(不是场景坐标) // - ty: 灯塔的网格坐标 Y(不是场景坐标) // // 返回值:指向新创建的 Tower 对象的指针 // 关键逻辑: // 1. 根据灯塔坐标计算灯塔的场景坐标范围(minX, minY, maxX, maxY) // 2. 坐标计算考虑 TowerPointOffset 来扩展灯塔边界(用于 AOI 计算) // 3. 初始化灯塔的观察者和被观察者列表为空 func (p *AOI) newTower(id, tx, ty int32) *Tower { var ( // 计算灯塔的最小坐标(左上角) // 公式:灯塔坐标 * 灯塔大小 - 偏移值 // TowerPointOffset 的作用:扩大灯塔边界以覆盖更多的 AOI 范围 minX = tx*p.config.TowerWidth - p.config.TowerPointOffset minY = ty*p.config.TowerHeight - p.config.TowerPointOffset // 计算灯塔的最大坐标(右下角) maxX = minX + p.config.TowerWidth maxY = minY + p.config.TowerHeight ) // 调试输出:打印灯塔信息(需要在 Config 中启用 PrintTowerInfo) if p.config.PrintTowerInfo { clog.Infof("id = %d, min(X = %d, Y = %d), max(X = %d, Y = %d)", id, minX, minY, maxX, maxY) } return &Tower{ id: id, markers: Objects{}, // 该灯塔内的被观察者列表 watchers: Objects{}, // 观察该灯塔的观察者列表 minX: minX, minY: minY, maxX: maxX, maxY: maxY, } } // CheckPoint 检查场景坐标是否在地图范围内 // 参数: // - x: 场景坐标 X // - y: 场景坐标 Y // // 返回值:坐标是否合法(true 表示在地图范围内 [0, Width) x [0, Height)) // 关键点:使用 < 而不是 <=,地图的有效范围是 [0, Width) func (p *AOI) CheckPoint(x, y int32) bool { return x >= 0 && y >= 0 && x < p.config.Width && y < p.config.Height } // InArea 检查点是否在指定的矩形区域内 // 参数: // - x, y: 检查的场景坐标 // - sx, sy: 区域的起始坐标(包含) // - ex, ey: 区域的结束坐标(包含) // // 返回值:点是否在区域内 // 注意:此方法使用 <= 作为边界判断,包含边界点 func (_ *AOI) InArea(x, y int32, sx, sy, ex, ey int32) bool { return x >= sx && x <= ex && y >= sy && y <= ey } // ToTowerPoint 将场景坐标转换为灯塔网格坐标 // 参数: // - x: 场景坐标 X // - y: 场景坐标 Y // // 返回值: // - tx: 灯塔网格坐标 X // - ty: 灯塔网格坐标 Y // // 示例:TowerWidth=3,TowerHeight=3 // // 场景坐标 (5, 7) => 灯塔坐标 (1, 2) // 场景坐标 (0, 0) => 灯塔坐标 (0, 0) // 场景坐标 (3, 3) => 灯塔坐标 (1, 1) func (p *AOI) ToTowerPoint(x, y int32) (tx int32, ty int32) { tx = x / p.config.TowerWidth ty = y / p.config.TowerHeight return } // GetTower 根据灯塔网格坐标获取灯塔对象 // 参数: // - tx: 灯塔网格坐标 X // - ty: 灯塔网格坐标 Y // // 返回值:灯塔对象指针,如果灯塔不存在则返回 nil // 性能:O(1) 时间复杂度(map 查询) func (p *AOI) GetTower(tx, ty int32) *Tower { if tower, found := p.towers[tx][ty]; found { return tower } return nil } // GetTowerByPoint 根据场景坐标获取所属的灯塔对象 // 参数: // - x: 场景坐标 X // - y: 场景坐标 Y // // 返回值:灯塔对象指针,如果不存在则返回 nil // 工作流程:场景坐标 -> 灯塔坐标 -> 获取灯塔对象 // 性能:O(1) 时间复杂度 func (p *AOI) GetTowerByPoint(x, y int32) *Tower { tx, ty := p.ToTowerPoint(x, y) return p.GetTower(tx, ty) } // GetDiffTowers 计算从旧位置到新位置视野范围的变化 // 参数: // - ox, oy: 旧位置的场景坐标 // - nx, ny: 新位置的场景坐标 // // 返回值: // - addTowers: 新增的灯塔(在新位置可见但旧位置不可见) // - delTowers: 删除的灯塔(在旧位置可见但新位置不可见) // // 用途:当玩家移动时,计算需要新增或删除的 AOI 对象 // 性能:O(n) 时间复杂度,n 为灯塔总数 func (p *AOI) GetDiffTowers(ox, oy, nx, ny int32) (Towers, Towers) { // 获取旧位置的视野范围和新位置的视野范围 oldTowers := p.GetRangeTowers(ox, oy) newTowers := p.GetRangeTowers(nx, ny) // 计算新增灯塔:在 newTowers 中但不在 oldTowers 中的灯塔 // 这些灯塔内的被观察者需要下发给观察者 addTowers := make(Towers) for id, tower := range newTowers { if !oldTowers.Contains(id) { addTowers[id] = tower } } // 计算删除灯塔:在 oldTowers 中但不在 newTowers 中的灯塔 // 这些灯塔内的被观察者将不再可见 delTowers := make(Towers) for id, tower := range oldTowers { if !newTowers.Contains(id) { delTowers.Add(tower) } } return addTowers, delTowers } // GetRangeTowers 获取指定位置观察者的视野范围内的所有灯塔 // 参数: // - x: 观察者的场景坐标 X // - y: 观察者的场景坐标 Y // // 返回值:视野范围内的所有灯塔集合 // 工作流程: // 1. 场景坐标 -> 灯塔坐标 // 2. 灯塔坐标 -> 视野范围(基于 TowerViewRange) // 3. 遍历范围内的所有灯塔 // // 性能:O(m) 时间复杂度,m 为视野范围内的灯塔数量(通常为常数) // 注意:根据 TowerViewRange 的值,视野范围会包含 (2*TowerViewRange+1)² 个灯塔 func (p *AOI) GetRangeTowers(x, y int32) Towers { // 第一步:将场景坐标转换为灯塔坐标 // 例如:场景点 (5,7),灯塔大小 3x3 => 灯塔坐标 (1,2) tx, ty := p.ToTowerPoint(x, y) // 第二步:获取视野范围内的灯塔坐标范围 // TowerViewRange 决定了观察者能看到多少周围的灯塔 // 例如:TowerViewRange=1,从 (1,2) 可以看到 (0-2, 1-3) 9 个灯塔 sx, sy, ex, ey := p.GetRangeTowerPoint(tx, ty) // 第三步:遍历范围内的所有灯塔并添加到结果集 towers := Towers{} for x := sx; x <= ex; x++ { for y := sy; y <= ey; y++ { if rangeTower := p.GetTower(x, y); rangeTower != nil { towers.Add(rangeTower) } } } return towers } // GetRangeTowerPoint 计算灯塔坐标的视野范围边界(截断模式) // 参数: // - tx: 灯塔网格坐标 X // - ty: 灯塔网格坐标 Y // // 返回值: // - sx: 视野范围的起始灯塔坐标 X(包含) // - sy: 视野范围的起始灯塔坐标 Y(包含) // - ex: 视野范围的结束灯塔坐标 X(包含) // - ey: 视野范围的结束灯塔坐标 Y(包含) // // 计算公式: // // 视野范围 = [tx - TowerViewRange, tx + TowerViewRange] x [ty - TowerViewRange, ty + TowerViewRange] // // 边界处理策略:截断模式(靠近边界时视野会缩小) // // 可见灯塔数量对比: // // 中心位置(无边界限制): // - TowerViewRange=1 时:3×3 = 9 个灯塔 // - TowerViewRange=2 时:5×5 = 25 个灯塔 // 边界位置(观察点在地图边缘): // - TowerViewRange=1 时:2×2 = 4 个灯塔 ⚠️ // - TowerViewRange=2 时:3×3 = 9 个灯塔 ⚠️ // // 特点: // - 靠近地图边界时,视野范围会缩小 // - 边界处玩家看到的灯塔数与中心位置不同,可能影响游戏体验和公平性 // // 适用场景: // - 单机游戏 // - 边界差异可接受的场景 // - 边界处有地图边界墙壁等遮挡物 // // 性能:O(1) 时间复杂度 // // 对比:GetRangeTowerPointNew 采用平移模式,保持所有位置视野大小一致(推荐用于公平的多人游戏) func (p *AOI) GetRangeTowerPoint(tx, ty int32) (sx, sy, ex, ey int32) { // 基于 TowerViewRange 计算视野范围(中心灯塔 ± TowerViewRange) sx = tx - p.config.TowerViewRange ex = tx + p.config.TowerViewRange sy = ty - p.config.TowerViewRange ey = ty + p.config.TowerViewRange // 左边界修正:不能小于 0 if sx < 0 { sx = 0 } // 上边界修正:不能小于 0 if sy < 0 { sy = 0 } // 右边界修正:灯塔数组上界是 towerNumX-1 // 关键:使用 >= 而不是 >,因为 map 的合法 key 范围是 [0, towerNumX-1] if ex >= p.towerNumX { ex = p.towerNumX - 1 } // 下边界修正:灯塔数组上界是 towerNumY-1 if ey >= p.towerNumY { ey = p.towerNumY - 1 } return sx, sy, ex, ey } // GetRangeTowerPointNew 计算灯塔坐标视野范围,保持视野一致性(平移模式) // 参数: // - tx: 灯塔网格坐标 X // - ty: 灯塔网格坐标 Y // // 返回值: // - sx, sy: 视野范围的起始坐标(包含) // - ex, ey: 视野范围的结束坐标(包含) // // 计算公式: // // 完整视野宽度 = 2 * TowerViewRange + 1 个灯塔 // // 边界处理策略:平移模式(靠近边界时向相反方向平移以保持视野大小) // // 可见灯塔数量对比: // // 中心位置(无边界限制): // - TowerViewRange=1 时:3×3 = 9 个灯塔 // - TowerViewRange=2 时:5×5 = 25 个灯塔 // 边界位置(观察点在地图边缘): // - TowerViewRange=1 时:3×3 = 9 个灯塔 ✅ // - TowerViewRange=2 时:5×5 = 25 个灯塔 ✅ // // 特点: // - 无论观察点在地图任何位置,视野大小始终保持一致 // - 边界处玩家看到的灯塔数与中心位置相同 // - 保证游戏的公平性和用户体验一致 // // 适用场景: // - SLG 多人游戏(推荐) // - 需要保证所有玩家视野相同的游戏 // - 自由拖动屏幕观察地图的游戏 // // 性能:O(1) 时间复杂度 // // 对比:GetRangeTowerPoint 采用截断模式,靠近边界时视野会缩小(不推荐用于多人游戏) func (p *AOI) GetRangeTowerPointNew(tx, ty int32) (sx, sy, ex, ey int32) { // towerViewRange2 = 2 * TowerViewRange,用于计算完整的视野宽度 // 解释: // - TowerViewRange 是观察者向一个方向能看到的灯塔距离 // - 视野范围 = [tx - TowerViewRange, tx + TowerViewRange] // - 视野宽度 = TowerViewRange + 1(中心灯塔) + TowerViewRange = 2*TowerViewRange + 1 // - 边界处理时,需要 2*TowerViewRange 来保持视野范围的大小 // 例如:TowerViewRange=2,则 towerViewRange2=4 // - 完整视野宽度 = 4 + 1 = 5 个灯塔 [0, 4] 或 [3, 7] 等 towerViewRange2 := 2 * p.config.TowerViewRange // X 轴方向的视野范围计算:采用对称的边界平移策略 if tx < p.config.TowerViewRange { // 观察者靠近左边界:向右平移范围以保持宽度 sx = 0 ex = towerViewRange2 } else if tx >= p.towerNumX-p.config.TowerViewRange { // 观察者靠近右边界:向左平移范围以保持宽度 sx = p.towerNumX - towerViewRange2 - 1 ex = p.towerNumX - 1 } else { // 正常情况:以观察者为中心对称扩展 sx = tx - p.config.TowerViewRange ex = tx + p.config.TowerViewRange } // Y 轴方向的视野范围计算(逻辑与 X 轴完全相同) if ty < p.config.TowerViewRange { // 观察者靠近上边界:向下平移范围以保持高度 sy = 0 ey = towerViewRange2 } else if ty >= p.towerNumY-p.config.TowerViewRange { // 观察者靠近下边界:向上平移范围以保持高度 sy = p.towerNumY - towerViewRange2 - 1 ey = p.towerNumY - 1 } else { // 正常情况:以观察者为中心对称扩展 sy = ty - p.config.TowerViewRange ey = ty + p.config.TowerViewRange } return sx, sy, ex, ey } // SetOnWatcherEnter 设置观察者进入时的回调 // 参数:callback 为 func(watcher *aoi.Object, towers Towers) 形式 func (p *AOI) SetOnWatcherEnter(callback OnWatcherEnterCallback) { p.OnWatcherEnter = callback } // SetOnWatcherMove 设置观察者移动时的回调 // 参数:callback 为 func(watcher *aoi.Object, towers Towers) 形式 func (p *AOI) SetOnWatcherMove(callback OnWatcherMoveCallback) { p.OnWatcherMove = callback } // SetOnMarkerEnter 设置被观察者进入时的回调 // 参数:callback 为 func(marker *aoi.Object, tower *Tower) 形式 func (p *AOI) SetOnMarkerEnter(callback OnMarkerEnterCallback) { p.OnMarkerEnter = callback } // SetOnMarkerMove 设置被观察者移动时的回调 // 参数:callback 为 func(marker *aoi.Object, newTower *Tower, oldTower *Tower) 形式 func (p *AOI) SetOnMarkerMove(callback OnMarkerMoveCallback) { p.OnMarkerMove = callback } // SetOnMarkerUpdate 设置被观察者更新时的回调 // 参数:callback 为 func(marker *aoi.Object, tower *Tower) 形式 func (p *AOI) SetOnMarkerUpdate(callback OnMarkerUpdateCallback) { p.OnMarkerUpdate = callback } // SetOnMarkerUpdateID 设置被观察者更新时的回调(带UpdateID) // 参数:callback 为 func(marker *aoi.Object, tower *Tower, updateID uint32, funcName string) 形式 func (p *AOI) SetOnMarkerUpdateID(callback OnMarkerUpdateIDCallback) { p.OnMarkerUpdateID = callback } // SetOnMarkerLeave 设置被观察者离开时的回调 // 参数:callback 为 func(marker *aoi.Object, tower *Tower) 形式 func (p *AOI) SetOnMarkerLeave(callback OnMarkerLeaveCallback) { p.OnMarkerLeave = callback }