Files
a301_game_server/internal/world/spatial_grid.go
2026-02-26 17:52:48 +09:00

180 lines
4.0 KiB
Go

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
}