1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-12-27 04:26:29 -05:00

Add simple index to query map (#321)

`renderPass2` is looping over every entity for every frame, this updates the method so it only evaluates each entity once.

Adds ability to query by rectangle or circle.
This commit is contained in:
nicholas-eden 2020-02-26 05:40:32 -08:00 committed by GitHub
parent a4efd41383
commit 1011b2f030
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 296 additions and 21 deletions

View File

@ -25,7 +25,7 @@ type MapEngine struct {
debugVisLevel int
regions []*MapRegion
entities []MapEntity
entities MapEntitiesSearcher
viewport *Viewport
camera Camera
}
@ -34,6 +34,7 @@ func CreateMapEngine(gameState *d2gamestate.GameState) *MapEngine {
engine := &MapEngine{
gameState: gameState,
viewport: NewViewport(0, 0, 800, 600),
entities: NewRangeSearcher(),
}
d2term.BindAction("mapdebugvis", "set map debug visualization level", func(level int) {
@ -88,7 +89,7 @@ func (m *MapEngine) WorldToOrtho(x, y float64) (float64, float64) {
func (m *MapEngine) GenerateMap(regionType d2enum.RegionIdType, levelPreset int, fileIndex int) {
region, entities := loadRegion(m.gameState.Seed, 0, 0, regionType, levelPreset, fileIndex)
m.regions = append(m.regions, region)
m.entities = append(m.entities, entities...)
m.entities.Add(entities...)
}
func (m *MapEngine) GenerateAct1Overworld() {
@ -96,16 +97,16 @@ func (m *MapEngine) GenerateAct1Overworld() {
region, entities := loadRegion(m.gameState.Seed, 0, 0, d2enum.RegionAct1Town, 1, -1)
m.regions = append(m.regions, region)
m.entities = append(m.entities, entities...)
m.entities.Add(entities...)
if strings.Contains(region.regionPath, "E1") {
region, entities := loadRegion(m.gameState.Seed, region.tileRect.Width-1, 0, d2enum.RegionAct1Town, 2, -1)
m.AppendRegion(region)
m.entities = append(m.entities, entities...)
m.entities.Add(entities...)
} else if strings.Contains(region.regionPath, "S1") {
region, entities := loadRegion(m.gameState.Seed, 0, region.tileRect.Height-1, d2enum.RegionAct1Town, 3, -1)
m.AppendRegion(region)
m.entities = append(m.entities, entities...)
m.entities.Add(entities...)
}
}
@ -125,7 +126,7 @@ func (m *MapEngine) GetRegionAtTile(x, y int) *MapRegion {
}
func (m *MapEngine) AddEntity(entity MapEntity) {
m.entities = append(m.entities, entity)
m.entities.Add(entity)
}
func (m *MapEngine) RemoveEntity(entity MapEntity) {
@ -133,15 +134,7 @@ func (m *MapEngine) RemoveEntity(entity MapEntity) {
return
}
// In-place filter to remove the given entity.
n := 0
for _, check := range m.entities {
if check != entity {
m.entities[n] = check
n++
}
}
m.entities = m.entities[:n]
m.entities.Remove(entity)
}
func (m *MapEngine) Advance(tickTime float64) {
@ -151,9 +144,11 @@ func (m *MapEngine) Advance(tickTime float64) {
}
}
for _, entity := range m.entities {
for _, entity := range m.entities.All() {
entity.Advance(tickTime)
}
m.entities.Update()
}
func (m *MapEngine) Render(target d2render.Surface) {

View File

@ -0,0 +1,148 @@
package d2map
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"math"
"sort"
)
type MapEntitiesSearcher interface {
// Returns all map entities.
All() []MapEntity
// Add adds an entity to the index and re-sorts.
Add(entities ...MapEntity)
// Remove finds and removes the entity from the index.
Remove(entity MapEntity)
// SearchByRect get entities in a rectangle, results will be sorted top left to bottom right.
// Elements with equal Y will be sorted by X
SearchByRect(rect d2common.Rectangle) []MapEntity
// SearchByRadius get entities in a circle, results will be sorted top left to bottom right.
// Elements with equal Y will be sorted by X
SearchByRadius(originX, originY, radius float64) []MapEntity
// Update re-sorts the index, must be ran after each update.
Update()
}
// rangeSearcher a basic index of entity locations using a slice ordered by Y then X coordinates.
// Eventually this should be probably replaced with a proper spatial index.
type rangeSearcher struct {
entities []MapEntity
}
func NewRangeSearcher() MapEntitiesSearcher {
return &rangeSearcher{
entities: make([]MapEntity, 0, 64),
}
}
func (r *rangeSearcher) All() []MapEntity {
return r.entities
}
func (r *rangeSearcher) Add(entities ...MapEntity) {
r.entities = append(r.entities, entities...)
r.Update()
}
func (r *rangeSearcher) Remove(entity MapEntity) {
if entity == nil {
return
}
// In-place filter to remove the given entity.
n := 0
for _, check := range r.entities {
if check != entity {
r.entities[n] = check
n++
}
}
r.entities = r.entities[:n]
}
func (r *rangeSearcher) SearchByRect(rect d2common.Rectangle) []MapEntity {
left, top, right, bottom := float64(rect.Left), float64(rect.Top), float64(rect.Right()), float64(rect.Bottom())
topIndex := sort.Search(len(r.entities), func(i int) bool {
x, y := r.entities[i].GetPosition()
if y == top {
return x >= left
}
return y >= top
})
matches := make([]MapEntity, 0, 16)
for i := topIndex; i < len(r.entities); i++ {
x, y := r.entities[i].GetPosition()
if y > bottom {
break
}
if x >= left && x <= right {
matches = append(matches, r.entities[i])
}
}
return matches
}
func (r *rangeSearcher) SearchByRadius(originX, originY, radius float64) []MapEntity {
left, right := originX-radius, originX+radius
top, bottom := originY-radius, originY+radius
inRect := r.SearchByRect(d2common.Rectangle{
Left: int(left),
Top: int(top),
Width: int(right - left),
Height: int(bottom - top),
})
// In-place filter to remove entities outside the radius.
n := 0
for _, check := range inRect {
x, y := check.GetPosition()
if distance(originX, originY, x, y) <= radius {
inRect[n] = check
n++
}
}
return inRect[:n]
}
func distance(x1, y1, x2, y2 float64) float64 {
return math.Abs(math.Sqrt(math.Pow(x2-x1, 2) + math.Pow(y2-y1, 2)))
}
// Re-sorts the index after entities have moved.
// Uses bubble sort to target O(n) sort time, in most cases no entities will be swapped.
func (r *rangeSearcher) Update() {
bubbleSort(r.entities, func(i, j int) bool {
ix, iy := r.entities[i].GetPosition()
jx, jy := r.entities[j].GetPosition()
if iy == jy {
return ix < jx
}
return iy < jy
})
}
func bubbleSort(items []MapEntity, less func(i, j int) bool) {
var (
n = len(items)
sorted = false
)
for !sorted {
swapped := false
for i := 0; i < n-1; i++ {
if less(i+1, i) {
items[i+1], items[i] = items[i], items[i+1]
swapped = true
}
}
if !swapped {
sorted = true
}
n = n - 1
}
}

View File

@ -0,0 +1,124 @@
package d2map
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render"
"github.com/stretchr/testify/assert"
"testing"
)
type mockEntity struct {
x float64
y float64
}
func (m *mockEntity) Render(target d2render.Surface) {
panic("implement me")
}
func (m *mockEntity) Advance(tickTime float64) {
panic("implement me")
}
func (m *mockEntity) GetPosition() (float64, float64) {
return m.x, m.y
}
func newMockEntity(x, y float64) MapEntity {
return &mockEntity{
x: x,
y: y,
}
}
func TestRangeSearcher_Add(t *testing.T) {
searcher := &rangeSearcher{
entities: make([]MapEntity, 0, 64),
}
searcher.Add(
newMockEntity(0, 9),
newMockEntity(8, 1),
newMockEntity(1, 8),
newMockEntity(3, 6),
newMockEntity(5, 4),
newMockEntity(6, 3),
newMockEntity(9, 0),
newMockEntity(4, 5),
newMockEntity(2, 7),
newMockEntity(7, 2),
)
for i := 0; i <= 9; i++ {
_, pos := searcher.entities[i].GetPosition()
assert.Equal(t, float64(i), pos)
}
}
func TestRangeSearcher_SearchByRect(t *testing.T) {
searcher := &rangeSearcher{
entities: make([]MapEntity, 0, 64),
}
searcher.Add(
newMockEntity(0, 9),
newMockEntity(8, 1),
newMockEntity(1, 8),
newMockEntity(3, 6),
newMockEntity(5, 4),
newMockEntity(6, 3),
newMockEntity(9, 0),
newMockEntity(4, 5),
newMockEntity(2, 7),
newMockEntity(7, 2),
)
matches := searcher.SearchByRect(d2common.Rectangle{
Left: 3,
Top: 0,
Width: 4,
Height: 9,
})
valsX := make([]float64, 0)
for _, match := range matches {
x, _ := match.GetPosition()
valsX = append(valsX, x)
}
assert.ElementsMatch(t, []float64{3, 4, 5, 6, 7}, valsX)
matches = searcher.SearchByRect(d2common.Rectangle{
Left: 0,
Top: 1,
Width: 9,
Height: 4,
})
valsY := make([]float64, 0)
for _, match := range matches {
_, y := match.GetPosition()
valsY = append(valsY, y)
}
assert.ElementsMatch(t, []float64{1, 2, 3, 4, 5}, valsY)
matches = searcher.SearchByRect(d2common.Rectangle{
Left: 3,
Top: 3,
Width: 2,
Height: 2,
})
valsY = make([]float64, 0)
valsX = make([]float64, 0)
for _, match := range matches {
x, y := match.GetPosition()
valsX = append(valsX, x)
valsY = append(valsY, y)
}
assert.ElementsMatch(t, []float64{4, 5}, valsY)
assert.ElementsMatch(t, []float64{4, 5}, valsX)
}

View File

@ -374,7 +374,10 @@ func (mr *MapRegion) renderPass1(viewport *Viewport, target d2render.Surface) {
}
}
func (mr *MapRegion) renderPass2(entities []MapEntity, viewport *Viewport, target d2render.Surface) {
func (mr *MapRegion) renderPass2(entities MapEntitiesSearcher, viewport *Viewport, target d2render.Surface) {
tileEntities := entities.SearchByRect(mr.tileRect)
nextIndex := 0
for tileY := range mr.ds1.Tiles {
for tileX, tile := range mr.ds1.Tiles[tileY] {
worldX, worldY := mr.getTileWorldPosition(tileX, tileY)
@ -382,12 +385,17 @@ func (mr *MapRegion) renderPass2(entities []MapEntity, viewport *Viewport, targe
viewport.PushTranslationWorld(worldX, worldY)
mr.renderTilePass2(tile, viewport, target)
for _, entity := range entities {
entWorldX, entWorldY := entity.GetPosition()
if entWorldX == worldX && entWorldY == worldY {
for nextIndex < len(tileEntities) {
nextX, nextY := tileEntities[nextIndex].GetPosition()
if nextX == worldX && nextY == worldY {
target.PushTranslation(viewport.GetTranslationScreen())
entity.Render(target)
tileEntities[nextIndex].Render(target)
target.Pop()
nextIndex++
} else if (nextY == worldY && nextX < worldX) || nextY < worldY {
nextIndex++
} else {
break
}
}