mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-12-26 12:06:24 -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:
parent
a4efd41383
commit
1011b2f030
@ -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) {
|
||||
|
148
d2core/d2map/range_searcher.go
Normal file
148
d2core/d2map/range_searcher.go
Normal 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
|
||||
}
|
||||
}
|
124
d2core/d2map/range_searcher_test.go
Normal file
124
d2core/d2map/range_searcher_test.go
Normal 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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user