package world import ( "a301_game_server/internal/entity" "a301_game_server/pkg/mathutil" ) // cellKey uniquely identifies a grid cell. type cellKey struct { cx, cz int } // GridAOI implements AOI using a spatial grid. Entities in nearby cells are considered visible. type GridAOI struct { cellSize float32 viewRange int cells map[cellKey]map[uint64]entity.Entity entityCell map[uint64]cellKey } func NewGridAOI(cellSize float32, viewRange int) *GridAOI { return &GridAOI{ cellSize: cellSize, viewRange: viewRange, cells: make(map[cellKey]map[uint64]entity.Entity), entityCell: make(map[uint64]cellKey), } } func (g *GridAOI) posToCell(pos mathutil.Vec3) cellKey { cx := int(pos.X / g.cellSize) cz := int(pos.Z / g.cellSize) if pos.X < 0 { cx-- } if pos.Z < 0 { cz-- } return cellKey{cx, cz} } func (g *GridAOI) Add(ent entity.Entity) { cell := g.posToCell(ent.Position()) g.addToCell(cell, ent) g.entityCell[ent.EntityID()] = cell } func (g *GridAOI) Remove(ent entity.Entity) []AOIEvent { eid := ent.EntityID() cell, ok := g.entityCell[eid] if !ok { return nil } nearby := g.getNearbyFromCell(cell, eid) events := make([]AOIEvent, 0, len(nearby)) for _, observer := range nearby { events = append(events, AOIEvent{Observer: observer, Target: ent, Type: AOILeave}) } g.removeFromCell(cell, eid) delete(g.entityCell, eid) return events } func (g *GridAOI) UpdatePosition(ent entity.Entity, oldPos, newPos mathutil.Vec3) []AOIEvent { eid := ent.EntityID() oldCell := g.posToCell(oldPos) newCell := g.posToCell(newPos) if oldCell == newCell { return nil } oldVisible := g.visibleCells(oldCell) newVisible := g.visibleCells(newCell) leaving := cellDifference(oldVisible, newVisible) entering := cellDifference(newVisible, oldVisible) var events []AOIEvent for _, c := range leaving { if cellEntities, ok := g.cells[c]; ok { for _, other := range cellEntities { if other.EntityID() == eid { continue } events = append(events, AOIEvent{Observer: other, Target: ent, Type: AOILeave}, AOIEvent{Observer: ent, Target: other, Type: AOILeave}, ) } } } for _, c := range entering { if cellEntities, ok := g.cells[c]; ok { for _, other := range cellEntities { if other.EntityID() == eid { continue } events = append(events, AOIEvent{Observer: other, Target: ent, Type: AOIEnter}, AOIEvent{Observer: ent, Target: other, Type: AOIEnter}, ) } } } g.removeFromCell(oldCell, eid) g.addToCell(newCell, ent) g.entityCell[eid] = newCell return events } func (g *GridAOI) GetNearby(ent entity.Entity) []entity.Entity { cell, ok := g.entityCell[ent.EntityID()] if !ok { return nil } return g.getNearbyFromCell(cell, ent.EntityID()) } func (g *GridAOI) getNearbyFromCell(cell cellKey, excludeID uint64) []entity.Entity { var result []entity.Entity for _, c := range g.visibleCells(cell) { if cellEntities, ok := g.cells[c]; ok { for _, e := range cellEntities { if e.EntityID() != excludeID { result = append(result, e) } } } } return result } func (g *GridAOI) visibleCells(center cellKey) []cellKey { size := (2*g.viewRange + 1) * (2*g.viewRange + 1) cells := make([]cellKey, 0, size) for dx := -g.viewRange; dx <= g.viewRange; dx++ { for dz := -g.viewRange; dz <= g.viewRange; dz++ { cells = append(cells, cellKey{center.cx + dx, center.cz + dz}) } } return cells } func (g *GridAOI) addToCell(cell cellKey, ent entity.Entity) { if g.cells[cell] == nil { g.cells[cell] = make(map[uint64]entity.Entity) } g.cells[cell][ent.EntityID()] = ent } func (g *GridAOI) removeFromCell(cell cellKey, eid uint64) { if m, ok := g.cells[cell]; ok { delete(m, eid) if len(m) == 0 { delete(g.cells, cell) } } } func cellDifference(a, b []cellKey) []cellKey { set := make(map[cellKey]struct{}, len(b)) for _, c := range b { set[c] = struct{}{} } var diff []cellKey for _, c := range a { if _, ok := set[c]; !ok { diff = append(diff, c) } } return diff }