Convert Atlas to infinite lazy growth
The atlas will now expand as needed for any query, but only initialise the chunk tile memory when requested While this may still be a pre-mature optimisation, it does simplify some code and ensures that our memory footprint stays small, for the most part
This commit is contained in:
parent
2556c0d049
commit
b116cdf291
6 changed files with 186 additions and 337 deletions
|
@ -1,7 +1,6 @@
|
|||
package atlas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
|
||||
|
@ -16,114 +15,98 @@ type Chunk struct {
|
|||
Tiles []byte `json:"tiles"`
|
||||
}
|
||||
|
||||
// SpawnContent will create a chunk and fill it with spawned tiles
|
||||
func (c *Chunk) SpawnContent(size int) {
|
||||
c.Tiles = make([]byte, size*size)
|
||||
for i := 0; i < len(c.Tiles); i++ {
|
||||
c.Tiles[i] = objects.Empty
|
||||
}
|
||||
|
||||
// For now, fill it randomly with objects
|
||||
for i := range c.Tiles {
|
||||
if rand.Intn(16) == 0 {
|
||||
c.Tiles[i] = objects.LargeRock
|
||||
} else if rand.Intn(32) == 0 {
|
||||
c.Tiles[i] = objects.SmallRock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Atlas represents a grid of Chunks
|
||||
type Atlas struct {
|
||||
// Chunks represents all chunks in the world
|
||||
// This is intentionally not a 2D array so it can be expanded in all directions
|
||||
Chunks []Chunk `json:"chunks"`
|
||||
|
||||
// size is the current width/height of the given atlas
|
||||
Size int `json:"size"`
|
||||
// CurrentSize is the current width/height of the given atlas
|
||||
CurrentSize int `json:"currentSize"`
|
||||
|
||||
// ChunkSize is the dimensions of each chunk
|
||||
ChunkSize int `json:"chunksize"`
|
||||
}
|
||||
|
||||
// NewAtlas creates a new empty atlas
|
||||
func NewAtlas(size, chunkSize int) Atlas {
|
||||
if size%2 != 0 {
|
||||
log.Fatal("atlas size must always be even")
|
||||
func NewAtlas(chunkSize int) Atlas {
|
||||
return Atlas{
|
||||
CurrentSize: 0,
|
||||
Chunks: nil,
|
||||
ChunkSize: chunkSize,
|
||||
}
|
||||
|
||||
a := Atlas{
|
||||
Size: size,
|
||||
Chunks: make([]Chunk, size*size),
|
||||
ChunkSize: chunkSize,
|
||||
}
|
||||
|
||||
// Initialise all the chunks
|
||||
for i := range a.Chunks {
|
||||
tiles := make([]byte, chunkSize*chunkSize)
|
||||
for i := 0; i < len(tiles); i++ {
|
||||
tiles[i] = objects.Empty
|
||||
}
|
||||
a.Chunks[i] = Chunk{
|
||||
Tiles: tiles,
|
||||
}
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SpawnRocks peppers the world with rocks
|
||||
func (a *Atlas) SpawnRocks() error {
|
||||
extent := a.ChunkSize * (a.Size / 2)
|
||||
|
||||
// Pepper the current world with rocks
|
||||
for i := -extent; i < extent; i++ {
|
||||
for j := -extent; j < extent; j++ {
|
||||
if rand.Intn(16) == 0 {
|
||||
if err := a.SetTile(vector.Vector{X: i, Y: j}, objects.SmallRock); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SpawnWalls spawns the around the world
|
||||
func (a *Atlas) SpawnWalls() error {
|
||||
extent := a.ChunkSize * (a.Size / 2)
|
||||
|
||||
// Surround the atlas in walls
|
||||
for i := -extent; i < extent; i++ {
|
||||
|
||||
if err := a.SetTile(vector.Vector{X: i, Y: extent - 1}, objects.LargeRock); err != nil { // N
|
||||
return err
|
||||
} else if err := a.SetTile(vector.Vector{X: extent - 1, Y: i}, objects.LargeRock); err != nil { // E
|
||||
return err
|
||||
} else if err := a.SetTile(vector.Vector{X: i, Y: -extent}, objects.LargeRock); err != nil { // S
|
||||
return err
|
||||
} else if err := a.SetTile(vector.Vector{X: -extent, Y: i}, objects.LargeRock); err != nil { // W
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTile sets an individual tile's kind
|
||||
func (a *Atlas) SetTile(v vector.Vector, tile byte) error {
|
||||
chunk := a.toChunk(v)
|
||||
if chunk >= len(a.Chunks) {
|
||||
return fmt.Errorf("location outside of allocated atlas")
|
||||
func (a *Atlas) SetTile(v vector.Vector, tile byte) {
|
||||
// Get the chunk, expand, and spawn it if needed
|
||||
c := a.toChunkWithGrow(v)
|
||||
chunk := a.Chunks[c]
|
||||
if chunk.Tiles == nil {
|
||||
chunk.SpawnContent(a.ChunkSize)
|
||||
}
|
||||
|
||||
local := a.toChunkLocal(v)
|
||||
tileId := local.X + local.Y*a.ChunkSize
|
||||
if tileId >= len(a.Chunks[chunk].Tiles) {
|
||||
return fmt.Errorf("location outside of allocated chunk")
|
||||
|
||||
// Sanity check
|
||||
if tileId >= len(chunk.Tiles) || tileId < 0 {
|
||||
log.Fatalf("Local tileID is not in valid chunk, somehow, this means something is very wrong")
|
||||
}
|
||||
a.Chunks[chunk].Tiles[tileId] = tile
|
||||
return nil
|
||||
|
||||
// Set the chunk back
|
||||
chunk.Tiles[tileId] = tile
|
||||
a.Chunks[c] = chunk
|
||||
}
|
||||
|
||||
// GetTile will return an individual tile
|
||||
func (a *Atlas) GetTile(v vector.Vector) (byte, error) {
|
||||
chunk := a.toChunk(v)
|
||||
if chunk >= len(a.Chunks) {
|
||||
return 0, fmt.Errorf("location outside of allocated atlas")
|
||||
func (a *Atlas) GetTile(v vector.Vector) byte {
|
||||
// Get the chunk, expand, and spawn it if needed
|
||||
c := a.toChunkWithGrow(v)
|
||||
chunk := a.Chunks[c]
|
||||
if chunk.Tiles == nil {
|
||||
chunk.SpawnContent(a.ChunkSize)
|
||||
}
|
||||
|
||||
local := a.toChunkLocal(v)
|
||||
tileId := local.X + local.Y*a.ChunkSize
|
||||
if tileId >= len(a.Chunks[chunk].Tiles) {
|
||||
return 0, fmt.Errorf("location outside of allocated chunk")
|
||||
|
||||
// Sanity check
|
||||
if tileId >= len(chunk.Tiles) || tileId < 0 {
|
||||
log.Fatalf("Local tileID is not in valid chunk, somehow, this means something is very wrong")
|
||||
}
|
||||
|
||||
return a.Chunks[chunk].Tiles[tileId], nil
|
||||
return chunk.Tiles[tileId]
|
||||
}
|
||||
|
||||
// toChunkWithGrow will expand the atlas for a given tile, returns the new chunk
|
||||
func (a *Atlas) toChunkWithGrow(v vector.Vector) int {
|
||||
for {
|
||||
// Get the chunk, and grow looping until we have a valid chunk
|
||||
chunk := a.toChunk(v)
|
||||
if chunk >= len(a.Chunks) || chunk < 0 {
|
||||
a.grow()
|
||||
} else {
|
||||
return chunk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toChunkLocal gets a chunk local coordinate for a tile
|
||||
|
@ -131,7 +114,7 @@ func (a *Atlas) toChunkLocal(v vector.Vector) vector.Vector {
|
|||
return vector.Vector{X: maths.Pmod(v.X, a.ChunkSize), Y: maths.Pmod(v.Y, a.ChunkSize)}
|
||||
}
|
||||
|
||||
// GetChunkID gets the chunk ID for a position in the world
|
||||
// GetChunkID gets the current chunk ID for a position in the world
|
||||
func (a *Atlas) toChunk(v vector.Vector) int {
|
||||
local := a.toChunkLocal(v)
|
||||
// Get the chunk origin itself
|
||||
|
@ -139,50 +122,34 @@ func (a *Atlas) toChunk(v vector.Vector) int {
|
|||
// Divided it by the number of chunks
|
||||
origin = origin.Divided(a.ChunkSize)
|
||||
// Shift it by our size (our origin is in the middle)
|
||||
origin = origin.Added(vector.Vector{X: a.Size / 2, Y: a.Size / 2})
|
||||
origin = origin.Added(vector.Vector{X: a.CurrentSize / 2, Y: a.CurrentSize / 2})
|
||||
// Get the ID based on the final values
|
||||
return (a.Size * origin.Y) + origin.X
|
||||
return (a.CurrentSize * origin.Y) + origin.X
|
||||
}
|
||||
|
||||
// chunkOrigin gets the chunk origin for a given chunk index
|
||||
func (a *Atlas) chunkOrigin(chunk int) vector.Vector {
|
||||
v := vector.Vector{
|
||||
X: maths.Pmod(chunk, a.Size) - (a.Size / 2),
|
||||
Y: (chunk / a.Size) - (a.Size / 2),
|
||||
X: maths.Pmod(chunk, a.CurrentSize) - (a.CurrentSize / 2),
|
||||
Y: (chunk / a.CurrentSize) - (a.CurrentSize / 2),
|
||||
}
|
||||
|
||||
return v.Multiplied(a.ChunkSize)
|
||||
}
|
||||
|
||||
// GetWorldExtent gets the min and max valid coordinates of world
|
||||
func (a *Atlas) GetWorldExtents() (min, max vector.Vector) {
|
||||
min = vector.Vector{
|
||||
X: -(a.Size / 2) * a.ChunkSize,
|
||||
Y: -(a.Size / 2) * a.ChunkSize,
|
||||
}
|
||||
max = vector.Vector{
|
||||
X: -min.X - 1,
|
||||
Y: -min.Y - 1,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Grow will return a grown copy of the current atlas
|
||||
func (a *Atlas) Grow(size int) error {
|
||||
if size%2 != 0 {
|
||||
return fmt.Errorf("atlas size must always be even")
|
||||
}
|
||||
delta := size - a.Size
|
||||
if delta < 0 {
|
||||
return fmt.Errorf("cannot shrink an atlas")
|
||||
} else if delta == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// grow will expand the current atlas in all directions by one chunk
|
||||
func (a *Atlas) grow() error {
|
||||
// Create a new atlas
|
||||
newAtlas := NewAtlas(size, a.ChunkSize)
|
||||
newAtlas := NewAtlas(a.ChunkSize)
|
||||
|
||||
// Copy old chunks into new chunks
|
||||
// Expand by one on each axis
|
||||
newAtlas.CurrentSize = a.CurrentSize + 2
|
||||
|
||||
// Allocate the new atlas chunks
|
||||
// These chunks will have nil tile slices
|
||||
newAtlas.Chunks = make([]Chunk, newAtlas.CurrentSize*newAtlas.CurrentSize)
|
||||
|
||||
// Copy all old chunks into the new atlas
|
||||
for index, chunk := range a.Chunks {
|
||||
// Calculate the new chunk location and copy over the data
|
||||
newAtlas.Chunks[newAtlas.toChunk(a.chunkOrigin(index))] = chunk
|
||||
|
|
|
@ -3,60 +3,63 @@ package atlas
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mdiluz/rove/pkg/objects"
|
||||
"github.com/mdiluz/rove/pkg/vector"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAtlas_NewAtlas(t *testing.T) {
|
||||
a := NewAtlas(2, 1)
|
||||
a := NewAtlas(1)
|
||||
assert.NotNil(t, a)
|
||||
// Tiles should look like: 2 | 3
|
||||
// -----
|
||||
// 0 | 1
|
||||
assert.Equal(t, 4, len(a.Chunks))
|
||||
|
||||
a = NewAtlas(4, 1)
|
||||
assert.NotNil(t, a)
|
||||
// Tiles should look like: 2 | 3
|
||||
// -----
|
||||
// 0 | 1
|
||||
assert.Equal(t, 16, len(a.Chunks))
|
||||
assert.Equal(t, 1, a.ChunkSize)
|
||||
assert.Equal(t, 0, len(a.Chunks)) // Should start empty
|
||||
}
|
||||
|
||||
func TestAtlas_toChunk(t *testing.T) {
|
||||
a := NewAtlas(2, 1)
|
||||
a := NewAtlas(1)
|
||||
assert.NotNil(t, a)
|
||||
// Tiles should look like: 2 | 3
|
||||
|
||||
// We start empty so we'll look like this
|
||||
chunkID := a.toChunk(vector.Vector{X: 0, Y: 0})
|
||||
assert.Equal(t, 0, chunkID)
|
||||
|
||||
// Get a tile to spawn the chunks
|
||||
a.GetTile(vector.Vector{})
|
||||
|
||||
// Chunks should look like:
|
||||
// 2 | 3
|
||||
// -----
|
||||
// 0 | 1
|
||||
tile := a.toChunk(vector.Vector{X: 0, Y: 0})
|
||||
assert.Equal(t, 3, tile)
|
||||
tile = a.toChunk(vector.Vector{X: 0, Y: -1})
|
||||
assert.Equal(t, 1, tile)
|
||||
tile = a.toChunk(vector.Vector{X: -1, Y: -1})
|
||||
assert.Equal(t, 0, tile)
|
||||
tile = a.toChunk(vector.Vector{X: -1, Y: 0})
|
||||
assert.Equal(t, 2, tile)
|
||||
chunkID = a.toChunk(vector.Vector{X: 0, Y: 0})
|
||||
assert.Equal(t, 3, chunkID)
|
||||
chunkID = a.toChunk(vector.Vector{X: 0, Y: -1})
|
||||
assert.Equal(t, 1, chunkID)
|
||||
chunkID = a.toChunk(vector.Vector{X: -1, Y: -1})
|
||||
assert.Equal(t, 0, chunkID)
|
||||
chunkID = a.toChunk(vector.Vector{X: -1, Y: 0})
|
||||
assert.Equal(t, 2, chunkID)
|
||||
|
||||
a = NewAtlas(2, 2)
|
||||
a = NewAtlas(2)
|
||||
assert.NotNil(t, a)
|
||||
// Tiles should look like:
|
||||
// Get a tile to spawn the chunks
|
||||
a.GetTile(vector.Vector{})
|
||||
// Chunks should look like:
|
||||
// 2 | 3
|
||||
// -----
|
||||
// 0 | 1
|
||||
tile = a.toChunk(vector.Vector{X: 1, Y: 1})
|
||||
assert.Equal(t, 3, tile)
|
||||
tile = a.toChunk(vector.Vector{X: 1, Y: -2})
|
||||
assert.Equal(t, 1, tile)
|
||||
tile = a.toChunk(vector.Vector{X: -2, Y: -2})
|
||||
assert.Equal(t, 0, tile)
|
||||
tile = a.toChunk(vector.Vector{X: -2, Y: 1})
|
||||
assert.Equal(t, 2, tile)
|
||||
chunkID = a.toChunk(vector.Vector{X: 1, Y: 1})
|
||||
assert.Equal(t, 3, chunkID)
|
||||
chunkID = a.toChunk(vector.Vector{X: 1, Y: -2})
|
||||
assert.Equal(t, 1, chunkID)
|
||||
chunkID = a.toChunk(vector.Vector{X: -2, Y: -2})
|
||||
assert.Equal(t, 0, chunkID)
|
||||
chunkID = a.toChunk(vector.Vector{X: -2, Y: 1})
|
||||
assert.Equal(t, 2, chunkID)
|
||||
|
||||
a = NewAtlas(4, 2)
|
||||
a = NewAtlas(2)
|
||||
assert.NotNil(t, a)
|
||||
// Tiles should look like:
|
||||
// Get a tile to spawn the chunks
|
||||
a.GetTile(vector.Vector{X: 0, Y: 3})
|
||||
// Chunks should look like:
|
||||
// 12| 13|| 14| 15
|
||||
// ----------------
|
||||
// 8 | 9 || 10| 11
|
||||
|
@ -64,107 +67,58 @@ func TestAtlas_toChunk(t *testing.T) {
|
|||
// 4 | 5 || 6 | 7
|
||||
// ----------------
|
||||
// 0 | 1 || 2 | 3
|
||||
tile = a.toChunk(vector.Vector{X: 1, Y: 3})
|
||||
assert.Equal(t, 14, tile)
|
||||
tile = a.toChunk(vector.Vector{X: 1, Y: -3})
|
||||
assert.Equal(t, 2, tile)
|
||||
tile = a.toChunk(vector.Vector{X: -1, Y: -1})
|
||||
assert.Equal(t, 5, tile)
|
||||
tile = a.toChunk(vector.Vector{X: -2, Y: 2})
|
||||
assert.Equal(t, 13, tile)
|
||||
chunkID = a.toChunk(vector.Vector{X: 1, Y: 3})
|
||||
assert.Equal(t, 14, chunkID)
|
||||
chunkID = a.toChunk(vector.Vector{X: 1, Y: -3})
|
||||
assert.Equal(t, 2, chunkID)
|
||||
chunkID = a.toChunk(vector.Vector{X: -1, Y: -1})
|
||||
assert.Equal(t, 5, chunkID)
|
||||
chunkID = a.toChunk(vector.Vector{X: -2, Y: 2})
|
||||
assert.Equal(t, 13, chunkID)
|
||||
}
|
||||
|
||||
func TestAtlas_GetSetTile(t *testing.T) {
|
||||
a := NewAtlas(4, 10)
|
||||
a := NewAtlas(10)
|
||||
assert.NotNil(t, a)
|
||||
|
||||
// Set the origin tile to 1 and test it
|
||||
assert.NoError(t, a.SetTile(vector.Vector{X: 0, Y: 0}, 1))
|
||||
tile, err := a.GetTile(vector.Vector{X: 0, Y: 0})
|
||||
assert.NoError(t, err)
|
||||
a.SetTile(vector.Vector{X: 0, Y: 0}, 1)
|
||||
tile := a.GetTile(vector.Vector{X: 0, Y: 0})
|
||||
assert.Equal(t, byte(1), tile)
|
||||
|
||||
// Set another tile to 1 and test it
|
||||
assert.NoError(t, a.SetTile(vector.Vector{X: 5, Y: -2}, 2))
|
||||
tile, err = a.GetTile(vector.Vector{X: 5, Y: -2})
|
||||
assert.NoError(t, err)
|
||||
a.SetTile(vector.Vector{X: 5, Y: -2}, 2)
|
||||
tile = a.GetTile(vector.Vector{X: 5, Y: -2})
|
||||
assert.Equal(t, byte(2), tile)
|
||||
}
|
||||
|
||||
func TestAtlas_Grown(t *testing.T) {
|
||||
// Start with a small example
|
||||
a := NewAtlas(2, 2)
|
||||
a := NewAtlas(2)
|
||||
assert.NotNil(t, a)
|
||||
assert.Equal(t, 4, len(a.Chunks))
|
||||
assert.Equal(t, 0, len(a.Chunks))
|
||||
|
||||
// Set a few tiles to values
|
||||
assert.NoError(t, a.SetTile(vector.Vector{X: 0, Y: 0}, 1))
|
||||
assert.NoError(t, a.SetTile(vector.Vector{X: -1, Y: -1}, 2))
|
||||
assert.NoError(t, a.SetTile(vector.Vector{X: 1, Y: -2}, 3))
|
||||
a.SetTile(vector.Vector{X: 0, Y: 0}, 1)
|
||||
a.SetTile(vector.Vector{X: -1, Y: -1}, 2)
|
||||
a.SetTile(vector.Vector{X: 1, Y: -2}, 3)
|
||||
|
||||
// Grow once to just double it
|
||||
err := a.Grow(4)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 16, len(a.Chunks))
|
||||
|
||||
tile, err := a.GetTile(vector.Vector{X: 0, Y: 0})
|
||||
assert.NoError(t, err)
|
||||
// Check tile values
|
||||
tile := a.GetTile(vector.Vector{X: 0, Y: 0})
|
||||
assert.Equal(t, byte(1), tile)
|
||||
|
||||
tile, err = a.GetTile(vector.Vector{X: -1, Y: -1})
|
||||
assert.NoError(t, err)
|
||||
tile = a.GetTile(vector.Vector{X: -1, Y: -1})
|
||||
assert.Equal(t, byte(2), tile)
|
||||
|
||||
tile, err = a.GetTile(vector.Vector{X: 1, Y: -2})
|
||||
assert.NoError(t, err)
|
||||
tile = a.GetTile(vector.Vector{X: 1, Y: -2})
|
||||
assert.Equal(t, byte(3), tile)
|
||||
|
||||
// Grow it again even bigger
|
||||
err = a.Grow(10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100, len(a.Chunks))
|
||||
|
||||
tile, err = a.GetTile(vector.Vector{X: 0, Y: 0})
|
||||
assert.NoError(t, err)
|
||||
tile = a.GetTile(vector.Vector{X: 0, Y: 0})
|
||||
assert.Equal(t, byte(1), tile)
|
||||
|
||||
tile, err = a.GetTile(vector.Vector{X: -1, Y: -1})
|
||||
assert.NoError(t, err)
|
||||
tile = a.GetTile(vector.Vector{X: -1, Y: -1})
|
||||
assert.Equal(t, byte(2), tile)
|
||||
|
||||
tile, err = a.GetTile(vector.Vector{X: 1, Y: -2})
|
||||
assert.NoError(t, err)
|
||||
tile = a.GetTile(vector.Vector{X: 1, Y: -2})
|
||||
assert.Equal(t, byte(3), tile)
|
||||
}
|
||||
|
||||
func TestAtlas_SpawnWorld(t *testing.T) {
|
||||
// Start with a small example
|
||||
a := NewAtlas(2, 4)
|
||||
assert.NotNil(t, a)
|
||||
assert.Equal(t, 4, len(a.Chunks))
|
||||
assert.NoError(t, a.SpawnWalls())
|
||||
|
||||
for i := -4; i < 4; i++ {
|
||||
tile, err := a.GetTile(vector.Vector{X: i, Y: -4})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, objects.LargeRock, tile)
|
||||
}
|
||||
|
||||
for i := -4; i < 4; i++ {
|
||||
tile, err := a.GetTile(vector.Vector{X: -4, Y: i})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, objects.LargeRock, tile)
|
||||
}
|
||||
|
||||
for i := -4; i < 4; i++ {
|
||||
tile, err := a.GetTile(vector.Vector{X: 3, Y: i})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, objects.LargeRock, tile)
|
||||
}
|
||||
|
||||
for i := -4; i < 4; i++ {
|
||||
tile, err := a.GetTile(vector.Vector{X: i, Y: 3})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, objects.LargeRock, tile)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue