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:
Marc Di Luzio 2020-06-27 14:48:21 +01:00
parent 2556c0d049
commit b116cdf291
6 changed files with 186 additions and 337 deletions

View file

@ -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