Make Atlas grow in X and Y dimensions independently

Fixes exponential growth
This commit is contained in:
Marc Di Luzio 2020-06-27 16:03:53 +01:00
parent b116cdf291
commit 9bb91920c9
3 changed files with 163 additions and 80 deletions

View file

@ -38,32 +38,40 @@ type Atlas struct {
// This is intentionally not a 2D array so it can be expanded in all directions // This is intentionally not a 2D array so it can be expanded in all directions
Chunks []Chunk `json:"chunks"` Chunks []Chunk `json:"chunks"`
// CurrentSize is the current width/height of the given atlas // CurrentSizeInChunks is the current width/height of the atlas in chunks
CurrentSize int `json:"currentSize"` CurrentSizeInChunks vector.Vector `json:"currentSizeInChunks"`
// ChunkSize is the dimensions of each chunk // WorldOriginInChunkSpace represents the location of [0,0] in chunk space
WorldOriginInChunkSpace vector.Vector `json:"worldOriginInChunkSpace"`
// ChunkSize is the x/y dimensions of each square chunk
ChunkSize int `json:"chunksize"` ChunkSize int `json:"chunksize"`
} }
// NewAtlas creates a new empty atlas // NewAtlas creates a new empty atlas
func NewAtlas(chunkSize int) Atlas { func NewAtlas(chunkSize int) Atlas {
return Atlas{ // Start up with one chunk
CurrentSize: 0, a := Atlas{
Chunks: nil,
ChunkSize: chunkSize, ChunkSize: chunkSize,
Chunks: make([]Chunk, 1),
CurrentSizeInChunks: vector.Vector{X: 1, Y: 1},
WorldOriginInChunkSpace: vector.Vector{X: 0, Y: 0},
} }
// Initialise the first chunk
a.Chunks[0].SpawnContent(chunkSize)
return a
} }
// SetTile sets an individual tile's kind // SetTile sets an individual tile's kind
func (a *Atlas) SetTile(v vector.Vector, tile byte) { func (a *Atlas) SetTile(v vector.Vector, tile byte) {
// Get the chunk, expand, and spawn it if needed // Get the chunk
c := a.toChunkWithGrow(v) c := a.worldSpaceToChunkWithGrow(v)
chunk := a.Chunks[c] chunk := a.Chunks[c]
if chunk.Tiles == nil { if chunk.Tiles == nil {
chunk.SpawnContent(a.ChunkSize) chunk.SpawnContent(a.ChunkSize)
} }
local := a.toChunkLocal(v) local := a.worldSpaceToChunkLocal(v)
tileId := local.X + local.Y*a.ChunkSize tileId := local.X + local.Y*a.ChunkSize
// Sanity check // Sanity check
@ -78,14 +86,14 @@ func (a *Atlas) SetTile(v vector.Vector, tile byte) {
// GetTile will return an individual tile // GetTile will return an individual tile
func (a *Atlas) GetTile(v vector.Vector) byte { func (a *Atlas) GetTile(v vector.Vector) byte {
// Get the chunk, expand, and spawn it if needed // Get the chunk
c := a.toChunkWithGrow(v) c := a.worldSpaceToChunkWithGrow(v)
chunk := a.Chunks[c] chunk := a.Chunks[c]
if chunk.Tiles == nil { if chunk.Tiles == nil {
chunk.SpawnContent(a.ChunkSize) chunk.SpawnContent(a.ChunkSize)
} }
local := a.toChunkLocal(v) local := a.worldSpaceToChunkLocal(v)
tileId := local.X + local.Y*a.ChunkSize tileId := local.X + local.Y*a.ChunkSize
// Sanity check // Sanity check
@ -96,68 +104,134 @@ func (a *Atlas) GetTile(v vector.Vector) byte {
return chunk.Tiles[tileId] return chunk.Tiles[tileId]
} }
// toChunkWithGrow will expand the atlas for a given tile, returns the new chunk // worldSpaceToChunkLocal gets a chunk local coordinate for a tile
func (a *Atlas) toChunkWithGrow(v vector.Vector) int { func (a *Atlas) worldSpaceToChunkLocal(v vector.Vector) vector.Vector {
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
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)} return vector.Vector{X: maths.Pmod(v.X, a.ChunkSize), Y: maths.Pmod(v.Y, a.ChunkSize)}
} }
// GetChunkID gets the current chunk ID for a position in the world // worldSpaceToChunk gets the current chunk ID for a position in the world
func (a *Atlas) toChunk(v vector.Vector) int { func (a *Atlas) worldSpaceToChunk(v vector.Vector) int {
local := a.toChunkLocal(v) // First convert to chunk space
// Get the chunk origin itself chunkSpace := a.worldSpaceToChunkSpace(v)
origin := v.Added(local.Negated())
// Divided it by the number of chunks // Then return the ID
origin = origin.Divided(a.ChunkSize) return a.chunkSpaceToChunk(chunkSpace)
// Shift it by our size (our origin is in the middle)
origin = origin.Added(vector.Vector{X: a.CurrentSize / 2, Y: a.CurrentSize / 2})
// Get the ID based on the final values
return (a.CurrentSize * origin.Y) + origin.X
} }
// chunkOrigin gets the chunk origin for a given chunk index // worldSpaceToChunkSpace converts from world space to chunk space
func (a *Atlas) chunkOrigin(chunk int) vector.Vector { func (a *Atlas) worldSpaceToChunkSpace(v vector.Vector) vector.Vector {
v := vector.Vector{ // Remove the chunk local part
X: maths.Pmod(chunk, a.CurrentSize) - (a.CurrentSize / 2), chunkOrigin := v.Added(a.worldSpaceToChunkLocal(v).Negated())
Y: (chunk / a.CurrentSize) - (a.CurrentSize / 2), // Convert to chunk space coordinate
chunkSpaceOrigin := chunkOrigin.Divided(a.ChunkSize)
// Shift it by our current chunk origin
chunkIndexOrigin := chunkSpaceOrigin.Added(a.WorldOriginInChunkSpace)
return chunkIndexOrigin
}
// chunkSpaceToWorldSpace vonverts from chunk space to world space
func (a *Atlas) chunkSpaceToWorldSpace(v vector.Vector) vector.Vector {
// Shift it by the current chunk origin
shifted := v.Added(a.WorldOriginInChunkSpace.Negated())
// Multiply out by chunk size
return shifted.Multiplied(a.ChunkSize)
}
// chunkOriginInChunkSpace Gets the chunk origin in chunk space
func (a *Atlas) chunkOriginInChunkSpace(chunk int) vector.Vector {
// convert the chunk to chunk space
chunkOrigin := a.chunkToChunkSpace(chunk)
// Shift it by the current chunk origin
return chunkOrigin.Added(a.WorldOriginInChunkSpace.Negated())
}
// chunkOriginInWorldSpace gets the chunk origin for a given chunk index
func (a *Atlas) chunkOriginInWorldSpace(chunk int) vector.Vector {
// convert the chunk to chunk space
chunkSpace := a.chunkToChunkSpace(chunk)
// Convert to world space
return a.chunkSpaceToWorldSpace(chunkSpace)
}
// chunkSpaceToChunk converts from chunk space to the chunk
func (a *Atlas) chunkSpaceToChunk(v vector.Vector) int {
// Along the coridor and up the stair
return (v.Y * a.CurrentSizeInChunks.X) + v.X
}
// chunkToChunkSpace returns the chunk space coord for the chunk
func (a *Atlas) chunkToChunkSpace(chunk int) vector.Vector {
return vector.Vector{
X: maths.Pmod(chunk, a.CurrentSizeInChunks.Y),
Y: (chunk / a.CurrentSizeInChunks.X),
}
}
func (a *Atlas) getExtents() (min vector.Vector, max vector.Vector) {
min = a.WorldOriginInChunkSpace.Negated()
max = min.Added(a.CurrentSizeInChunks)
return
}
// worldSpaceToTrunkWithGrow will expand the current atlas for a given world space position if needed
func (a *Atlas) worldSpaceToChunkWithGrow(v vector.Vector) int {
min, max := a.getExtents()
// Divide by the chunk size to bring into chunk space
v = v.Divided(a.ChunkSize)
// Check we're within the current extents and bail early
if v.X >= min.X && v.Y >= min.Y && v.X < max.X && v.Y < max.Y {
return a.worldSpaceToChunk(v)
} }
return v.Multiplied(a.ChunkSize) // Calculate the new origin and the new size
} origin := min
size := a.CurrentSizeInChunks
// grow will expand the current atlas in all directions by one chunk // If we need to shift the origin back
func (a *Atlas) grow() error { originDiff := origin.Added(v.Negated())
// Create a new atlas if originDiff.X > 0 {
newAtlas := NewAtlas(a.ChunkSize) origin.X -= originDiff.X
size.X += originDiff.X
}
if originDiff.Y > 0 {
origin.Y -= originDiff.Y
size.Y += originDiff.Y
}
// Expand by one on each axis // If we need to expand the size
newAtlas.CurrentSize = a.CurrentSize + 2 maxDiff := v.Added(max.Negated())
if maxDiff.X > 0 {
size.X += maxDiff.X
}
if maxDiff.Y > 0 {
size.Y += maxDiff.Y
}
// Allocate the new atlas chunks // Set up the new size and origin
// These chunks will have nil tile slices newAtlas := Atlas{
newAtlas.Chunks = make([]Chunk, newAtlas.CurrentSize*newAtlas.CurrentSize) ChunkSize: a.ChunkSize,
WorldOriginInChunkSpace: origin.Negated(),
CurrentSizeInChunks: size,
Chunks: make([]Chunk, size.X*size.Y),
}
// Copy all old chunks into the new atlas // Copy all old chunks into the new atlas
for index, chunk := range a.Chunks { for chunk, chunkData := range a.Chunks {
// Calculate the new chunk location and copy over the data // Calculate the new chunk location and copy over the data
newAtlas.Chunks[newAtlas.toChunk(a.chunkOrigin(index))] = chunk newChunk := newAtlas.worldSpaceToChunk(a.chunkOriginInWorldSpace(chunk))
// Copy over the old chunk to the new atlas
newAtlas.Chunks[newChunk] = chunkData
} }
// Copy the new atlas data into this one // Copy the new atlas data into this one
*a = newAtlas *a = newAtlas
// Return the new atlas return a.worldSpaceToChunk(v)
return nil
} }

View file

@ -11,54 +11,53 @@ func TestAtlas_NewAtlas(t *testing.T) {
a := NewAtlas(1) a := NewAtlas(1)
assert.NotNil(t, a) assert.NotNil(t, a)
assert.Equal(t, 1, a.ChunkSize) assert.Equal(t, 1, a.ChunkSize)
assert.Equal(t, 0, len(a.Chunks)) // Should start empty assert.Equal(t, 1, len(a.Chunks)) // Should start empty
} }
func TestAtlas_toChunk(t *testing.T) { func TestAtlas_toChunk(t *testing.T) {
a := NewAtlas(1) a := NewAtlas(1)
assert.NotNil(t, a) assert.NotNil(t, a)
// 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 // Get a tile to spawn the chunks
a.GetTile(vector.Vector{}) a.GetTile(vector.Vector{X: -1, Y: -1})
a.GetTile(vector.Vector{X: 0, Y: 0})
// Chunks should look like: // Chunks should look like:
// 2 | 3 // 2 | 3
// ----- // -----
// 0 | 1 // 0 | 1
chunkID = a.toChunk(vector.Vector{X: 0, Y: 0}) chunkID := a.worldSpaceToChunk(vector.Vector{X: 0, Y: 0})
assert.Equal(t, 3, chunkID) assert.Equal(t, 3, chunkID)
chunkID = a.toChunk(vector.Vector{X: 0, Y: -1}) chunkID = a.worldSpaceToChunk(vector.Vector{X: 0, Y: -1})
assert.Equal(t, 1, chunkID) assert.Equal(t, 1, chunkID)
chunkID = a.toChunk(vector.Vector{X: -1, Y: -1}) chunkID = a.worldSpaceToChunk(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 0, chunkID) assert.Equal(t, 0, chunkID)
chunkID = a.toChunk(vector.Vector{X: -1, Y: 0}) chunkID = a.worldSpaceToChunk(vector.Vector{X: -1, Y: 0})
assert.Equal(t, 2, chunkID) assert.Equal(t, 2, chunkID)
a = NewAtlas(2) a = NewAtlas(2)
assert.NotNil(t, a) assert.NotNil(t, a)
// Get a tile to spawn the chunks // Get a tile to spawn the chunks
a.GetTile(vector.Vector{}) a.GetTile(vector.Vector{X: -2, Y: -2})
a.GetTile(vector.Vector{X: 1, Y: 1})
// Chunks should look like: // Chunks should look like:
// 2 | 3 // 2 | 3
// ----- // -----
// 0 | 1 // 0 | 1
chunkID = a.toChunk(vector.Vector{X: 1, Y: 1}) chunkID = a.worldSpaceToChunk(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 3, chunkID) assert.Equal(t, 3, chunkID)
chunkID = a.toChunk(vector.Vector{X: 1, Y: -2}) chunkID = a.worldSpaceToChunk(vector.Vector{X: 1, Y: -2})
assert.Equal(t, 1, chunkID) assert.Equal(t, 1, chunkID)
chunkID = a.toChunk(vector.Vector{X: -2, Y: -2}) chunkID = a.worldSpaceToChunk(vector.Vector{X: -2, Y: -2})
assert.Equal(t, 0, chunkID) assert.Equal(t, 0, chunkID)
chunkID = a.toChunk(vector.Vector{X: -2, Y: 1}) chunkID = a.worldSpaceToChunk(vector.Vector{X: -2, Y: 1})
assert.Equal(t, 2, chunkID) assert.Equal(t, 2, chunkID)
a = NewAtlas(2) a = NewAtlas(2)
assert.NotNil(t, a) assert.NotNil(t, a)
// Get a tile to spawn the chunks // Get a tile to spawn the chunks
a.GetTile(vector.Vector{X: 0, Y: 3}) a.GetTile(vector.Vector{X: 5, Y: 5})
a.GetTile(vector.Vector{X: -5, Y: -5})
// Chunks should look like: // Chunks should look like:
// 12| 13|| 14| 15 // 12| 13|| 14| 15
// ---------------- // ----------------
@ -67,13 +66,13 @@ func TestAtlas_toChunk(t *testing.T) {
// 4 | 5 || 6 | 7 // 4 | 5 || 6 | 7
// ---------------- // ----------------
// 0 | 1 || 2 | 3 // 0 | 1 || 2 | 3
chunkID = a.toChunk(vector.Vector{X: 1, Y: 3}) chunkID = a.worldSpaceToChunk(vector.Vector{X: 1, Y: 3})
assert.Equal(t, 14, chunkID) assert.Equal(t, 14, chunkID)
chunkID = a.toChunk(vector.Vector{X: 1, Y: -3}) chunkID = a.worldSpaceToChunk(vector.Vector{X: 1, Y: -3})
assert.Equal(t, 2, chunkID) assert.Equal(t, 2, chunkID)
chunkID = a.toChunk(vector.Vector{X: -1, Y: -1}) chunkID = a.worldSpaceToChunk(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 5, chunkID) assert.Equal(t, 5, chunkID)
chunkID = a.toChunk(vector.Vector{X: -2, Y: 2}) chunkID = a.worldSpaceToChunk(vector.Vector{X: -2, Y: 2})
assert.Equal(t, 13, chunkID) assert.Equal(t, 13, chunkID)
} }
@ -96,7 +95,7 @@ func TestAtlas_Grown(t *testing.T) {
// Start with a small example // Start with a small example
a := NewAtlas(2) a := NewAtlas(2)
assert.NotNil(t, a) assert.NotNil(t, a)
assert.Equal(t, 0, len(a.Chunks)) assert.Equal(t, 1, len(a.Chunks))
// Set a few tiles to values // Set a few tiles to values
a.SetTile(vector.Vector{X: 0, Y: 0}, 1) a.SetTile(vector.Vector{X: 0, Y: 0}, 1)

View file

@ -54,3 +54,13 @@ func (v Vector) Divided(val int) Vector {
func (v Vector) Abs() Vector { func (v Vector) Abs() Vector {
return Vector{maths.Abs(v.X), maths.Abs(v.Y)} return Vector{maths.Abs(v.X), maths.Abs(v.Y)}
} }
// Min returns the minimum values in both vectors
func Min(v1 Vector, v2 Vector) Vector {
return Vector{maths.Min(v1.X, v2.X), maths.Min(v1.Y, v2.Y)}
}
// Min returns the max values in both vectors
func Max(v1 Vector, v2 Vector) Vector {
return Vector{maths.Max(v1.X, v2.X), maths.Max(v1.Y, v2.Y)}
}