| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900 |
- 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
- }
|