Fix Atlas gen with simplification

Only track lower and upper bounds in world space, and speak in terms of world space and chunks
This commit is contained in:
Marc Di Luzio 2020-07-04 22:34:28 +01:00
parent dbe944bb4e
commit 8b83672dcc
5 changed files with 240 additions and 121 deletions

View file

@ -38,11 +38,11 @@ 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 atlas in chunks // LowerBound is the origin of the bottom left corner of the current chunks in world space (current chunks cover >= this value)
CurrentSize vector.Vector `json:"currentSize"` LowerBound vector.Vector `json:"lowerBound"`
// WorldOrigin represents the location of the [0,0] world space point in terms of the allotted current chunks // UpperBound is the top left corner of the current chunks (curent chunks cover < this value)
WorldOrigin vector.Vector `json:"worldOrigin"` UpperBound vector.Vector `json:"upperBound"`
// ChunkSize is the x/y dimensions of each square chunk // ChunkSize is the x/y dimensions of each square chunk
ChunkSize int `json:"chunksize"` ChunkSize int `json:"chunksize"`
@ -52,10 +52,10 @@ type Atlas struct {
func NewAtlas(chunkSize int) Atlas { func NewAtlas(chunkSize int) Atlas {
// Start up with one chunk // Start up with one chunk
a := Atlas{ a := Atlas{
ChunkSize: chunkSize, ChunkSize: chunkSize,
Chunks: make([]Chunk, 1), Chunks: make([]Chunk, 1),
CurrentSize: vector.Vector{X: 1, Y: 1}, LowerBound: vector.Vector{X: 0, Y: 0},
WorldOrigin: vector.Vector{X: 0, Y: 0}, UpperBound: vector.Vector{X: chunkSize, Y: chunkSize},
} }
// Initialise the first chunk // Initialise the first chunk
a.Chunks[0].populate(chunkSize) a.Chunks[0].populate(chunkSize)
@ -162,129 +162,88 @@ func (a *Atlas) worldSpaceToChunkLocal(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)}
} }
// worldSpaceToChunk gets the current chunk ID for a position in the world // worldSpaceToChunkID gets the current chunk ID for a position in the world
func (a *Atlas) worldSpaceToChunk(v vector.Vector) int { func (a *Atlas) worldSpaceToChunkIndex(v vector.Vector) int {
// First convert to chunk space // Shift the vector by our current min
chunkSpace := a.worldSpaceToChunkSpace(v) v = v.Added(a.LowerBound.Negated())
// Then return the ID // Divide by the current size and floor, to get chunk-scaled vector from the lower bound
return a.chunkSpaceToChunk(chunkSpace) v = v.DividedFloor(a.ChunkSize)
// Calculate the width
width := a.UpperBound.X - a.LowerBound.X
widthInChunks := width / a.ChunkSize
// Along the corridor and up the stairs
return (v.Y * widthInChunks) + v.X
} }
// worldSpaceToChunkSpace converts from world space to chunk space // chunkOriginInWorldSpace returns the origin of the chunk in world space
func (a *Atlas) worldSpaceToChunkSpace(v vector.Vector) vector.Vector {
// Remove the chunk local part
chunkOrigin := v.Added(a.worldSpaceToChunkLocal(v).Negated())
// Convert to chunk space coordinate
chunkSpaceOrigin := chunkOrigin.Divided(a.ChunkSize)
// Shift it by our current chunk origin
chunkIndexOrigin := chunkSpaceOrigin.Added(a.WorldOrigin)
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.WorldOrigin.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.WorldOrigin.Negated())
}
// chunkOriginInWorldSpace gets the chunk origin for a given chunk index
func (a *Atlas) chunkOriginInWorldSpace(chunk int) vector.Vector { func (a *Atlas) chunkOriginInWorldSpace(chunk int) vector.Vector {
// convert the chunk to chunk space // Calculate the width
chunkSpace := a.chunkToChunkSpace(chunk) width := a.UpperBound.X - a.LowerBound.X
widthInChunks := width / a.ChunkSize
// Convert to world space // Reverse the along the corridor and up the stairs
return a.chunkSpaceToWorldSpace(chunkSpace) v := vector.Vector{
} X: chunk % widthInChunks,
Y: chunk / widthInChunks,
// 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.CurrentSize.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.CurrentSize.Y),
Y: (chunk / a.CurrentSize.X),
} }
// Multiply up to world scale
v = v.Multiplied(a.ChunkSize)
// Shift by the lower bound
return v.Added(a.LowerBound)
} }
func (a *Atlas) getExtents() (min vector.Vector, max vector.Vector) { // getNewBounds gets new lower and upper bounds for the world space given a vector
min = a.WorldOrigin.Negated() func (a *Atlas) getNewBounds(v vector.Vector) (lower vector.Vector, upper vector.Vector) {
max = min.Added(a.CurrentSize) lower = vector.Min(v, a.LowerBound)
upper = vector.Max(v.Added(vector.Vector{X: 1, Y: 1}), a.UpperBound)
lower = vector.Vector{
X: maths.RoundDown(lower.X, a.ChunkSize),
Y: maths.RoundDown(lower.Y, a.ChunkSize),
}
upper = vector.Vector{
X: maths.RoundUp(upper.X, a.ChunkSize),
Y: maths.RoundUp(upper.Y, a.ChunkSize),
}
return return
} }
// worldSpaceToTrunkWithGrow will expand the current atlas for a given world space position if needed // worldSpaceToTrunkWithGrow will expand the current atlas for a given world space position if needed
func (a *Atlas) worldSpaceToChunkWithGrow(v vector.Vector) int { func (a *Atlas) worldSpaceToChunkWithGrow(v vector.Vector) int {
min, max := a.getExtents() // If we're within bounds, just return the current chunk
if v.X >= a.LowerBound.X && v.Y >= a.LowerBound.Y && v.X < a.UpperBound.X && v.Y < a.UpperBound.Y {
// Divide by the chunk size to bring into chunk space return a.worldSpaceToChunkIndex(v)
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)
} }
// Calculate the new origin and the new size // Calculate the new bounds
origin := min lower, upper := a.getNewBounds(v)
size := a.CurrentSize size := upper.Added(lower.Negated())
size = size.Divided(a.ChunkSize)
// If we need to shift the origin back // Create the new empty atlas
originDiff := origin.Added(v.Negated())
if originDiff.X > 0 {
origin.X -= originDiff.X
size.X += originDiff.X
}
if originDiff.Y > 0 {
origin.Y -= originDiff.Y
size.Y += originDiff.Y
}
// If we need to expand the size
maxDiff := v.Added(max.Negated())
if maxDiff.X > 0 {
size.X += maxDiff.X
}
if maxDiff.Y > 0 {
size.Y += maxDiff.Y
}
// Set up the new size and origin
newAtlas := Atlas{ newAtlas := Atlas{
ChunkSize: a.ChunkSize, ChunkSize: a.ChunkSize,
WorldOrigin: origin.Negated(), LowerBound: lower,
CurrentSize: size, UpperBound: upper,
Chunks: make([]Chunk, size.X*size.Y), Chunks: make([]Chunk, size.X*size.Y),
} }
// Copy all old chunks into the new atlas // Copy all old chunks into the new atlas
for chunk, chunkData := range a.Chunks { for chunk, chunkData := range a.Chunks {
// Calculate the new chunk location and copy over the data
newChunk := newAtlas.worldSpaceToChunk(a.chunkOriginInWorldSpace(chunk)) // Calculate the chunk ID in the new atlas
origin := a.chunkOriginInWorldSpace(chunk)
newChunk := newAtlas.worldSpaceToChunkIndex(origin)
// Copy over the old chunk to the new atlas // Copy over the old chunk to the new atlas
newAtlas.Chunks[newChunk] = chunkData newAtlas.Chunks[newChunk] = chunkData
} }
// Copy the new atlas data into this one // Overwrite the old atlas with this one
*a = newAtlas *a = newAtlas
return a.worldSpaceToChunk(v) return a.worldSpaceToChunkIndex(v)
} }

View file

@ -22,43 +22,49 @@ func TestAtlas_toChunk(t *testing.T) {
// Get a tile to spawn the chunks // Get a tile to spawn the chunks
a.QueryPosition(vector.Vector{X: -1, Y: -1}) a.QueryPosition(vector.Vector{X: -1, Y: -1})
a.QueryPosition(vector.Vector{X: 0, Y: 0}) a.QueryPosition(vector.Vector{X: 0, Y: 0})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like: // Chunks should look like:
// 2 | 3 // 2 | 3
// ----- // -----
// 0 | 1 // 0 | 1
chunkID := a.worldSpaceToChunk(vector.Vector{X: 0, Y: 0}) chunkID := a.worldSpaceToChunkIndex(vector.Vector{X: 0, Y: 0})
assert.Equal(t, 3, chunkID) assert.Equal(t, 3, chunkID)
chunkID = a.worldSpaceToChunk(vector.Vector{X: 0, Y: -1}) chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 0, Y: -1})
assert.Equal(t, 1, chunkID) assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunk(vector.Vector{X: -1, Y: -1}) chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 0, chunkID) assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunk(vector.Vector{X: -1, Y: 0}) chunkID = a.worldSpaceToChunkIndex(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.QueryPosition(vector.Vector{X: -2, Y: -2}) a.QueryPosition(vector.Vector{X: -2, Y: -2})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(vector.Vector{X: 1, Y: 1}) a.QueryPosition(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like: // Chunks should look like:
// 2 | 3 // 2 | 3
// ----- // -----
// 0 | 1 // 0 | 1
chunkID = a.worldSpaceToChunk(vector.Vector{X: 1, Y: 1}) chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 3, chunkID) assert.Equal(t, 3, chunkID)
chunkID = a.worldSpaceToChunk(vector.Vector{X: 1, Y: -2}) chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: -2})
assert.Equal(t, 1, chunkID) assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunk(vector.Vector{X: -2, Y: -2}) chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -2, Y: -2})
assert.Equal(t, 0, chunkID) assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunk(vector.Vector{X: -2, Y: 1}) chunkID = a.worldSpaceToChunkIndex(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 a 4x4 grid of chunks
a.QueryPosition(vector.Vector{X: 5, Y: 5}) a.QueryPosition(vector.Vector{X: 3, Y: 3})
a.QueryPosition(vector.Vector{X: -5, Y: -5}) assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(vector.Vector{X: -3, Y: -3})
assert.Equal(t, 4*4, len(a.Chunks))
// Chunks should look like: // Chunks should look like:
// 12| 13|| 14| 15 // 12| 13|| 14| 15
// ---------------- // ----------------
@ -67,14 +73,96 @@ func TestAtlas_toChunk(t *testing.T) {
// 4 | 5 || 6 | 7 // 4 | 5 || 6 | 7
// ---------------- // ----------------
// 0 | 1 || 2 | 3 // 0 | 1 || 2 | 3
chunkID = a.worldSpaceToChunk(vector.Vector{X: 1, Y: 3}) chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: 3})
assert.Equal(t, 14, chunkID) assert.Equal(t, 14, chunkID)
chunkID = a.worldSpaceToChunk(vector.Vector{X: 1, Y: -3}) chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: -3})
assert.Equal(t, 2, chunkID) assert.Equal(t, 2, chunkID)
chunkID = a.worldSpaceToChunk(vector.Vector{X: -1, Y: -1}) chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 5, chunkID) assert.Equal(t, 5, chunkID)
chunkID = a.worldSpaceToChunk(vector.Vector{X: -2, Y: 2}) chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -2, Y: 2})
assert.Equal(t, 13, chunkID) assert.Equal(t, 13, chunkID)
a = NewAtlas(3)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(vector.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// || 2| 3
// -------
// || 0| 1
// =======
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 3, Y: 1})
assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: 4})
assert.Equal(t, 2, chunkID)
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 5, Y: 5})
assert.Equal(t, 3, chunkID)
}
func TestAtlas_toWorld(t *testing.T) {
a := NewAtlas(1)
assert.NotNil(t, a)
// Get a tile to spawn some chunks
a.QueryPosition(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
assert.Equal(t, vector.Vector{X: -1, Y: -1}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, vector.Vector{X: 0, Y: -1}, a.chunkOriginInWorldSpace(1))
a = NewAtlas(2)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(vector.Vector{X: -2, Y: -2})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
assert.Equal(t, vector.Vector{X: -2, Y: -2}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, vector.Vector{X: -2, Y: 0}, a.chunkOriginInWorldSpace(2))
a = NewAtlas(2)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(vector.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(vector.Vector{X: -3, Y: -3})
assert.Equal(t, 4*4, len(a.Chunks))
// Chunks should look like:
// 12| 13|| 14| 15
// ----------------
// 8 | 9 || 10| 11
// ================
// 4 | 5 || 6 | 7
// ----------------
// 0 | 1 || 2 | 3
assert.Equal(t, vector.Vector{X: -4, Y: -4}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, vector.Vector{X: 2, Y: -2}, a.chunkOriginInWorldSpace(7))
a = NewAtlas(3)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(vector.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// || 2| 3
// -------
// || 0| 1
// =======
assert.Equal(t, vector.Vector{X: 0, Y: 0}, a.chunkOriginInWorldSpace(0))
} }
func TestAtlas_GetSetTile(t *testing.T) { func TestAtlas_GetSetTile(t *testing.T) {
@ -137,3 +225,26 @@ func TestAtlas_Grown(t *testing.T) {
tile, _ = a.QueryPosition(vector.Vector{X: 1, Y: -2}) tile, _ = a.QueryPosition(vector.Vector{X: 1, Y: -2})
assert.Equal(t, byte(3), tile) assert.Equal(t, byte(3), tile)
} }
func TestAtlas_GetSetCorrect(t *testing.T) {
// Big stress test to ensure we do actually properly expand for all reasonable values
for i := 1; i <= 4; i++ {
for x := -i * 2; x < i*2; x++ {
for y := -i * 2; y < i*2; y++ {
a := NewAtlas(i)
assert.NotNil(t, a)
assert.Equal(t, 1, len(a.Chunks))
pos := vector.Vector{X: x, Y: y}
a.SetTile(pos, TileRock)
a.SetObject(pos, objects.Object{Type: objects.LargeRock})
tile, obj := a.QueryPosition(pos)
assert.Equal(t, TileRock, Tile(tile))
assert.Equal(t, objects.Object{Type: objects.LargeRock}, obj)
}
}
}
}

View file

@ -39,3 +39,19 @@ func Min(x, y int) int {
} }
return x return x
} }
// RoundUp rounds a value up to the nearest multiple
func RoundUp(toRound int, multiple int) int {
remainder := Pmod(toRound, multiple)
if remainder == 0 {
return toRound
}
return (multiple - remainder) + toRound
}
// RoundDown rounds a value down to the nearest multiple
func RoundDown(toRound int, multiple int) int {
remainder := Pmod(toRound, multiple)
return toRound - remainder
}

View file

@ -29,5 +29,19 @@ func TestMin(t *testing.T) {
assert.Equal(t, 100, Min(100, 500)) assert.Equal(t, 100, Min(100, 500))
assert.Equal(t, -4, Min(-4, 1)) assert.Equal(t, -4, Min(-4, 1))
assert.Equal(t, -4, Min(-4, -2)) assert.Equal(t, -4, Min(-4, -2))
}
func TestRoundUp(t *testing.T) {
assert.Equal(t, 10, RoundUp(10, 5))
assert.Equal(t, 12, RoundUp(10, 4))
assert.Equal(t, -8, RoundUp(-8, 4))
assert.Equal(t, -4, RoundUp(-7, 4))
}
func TestRoundDown(t *testing.T) {
assert.Equal(t, 10, RoundDown(10, 5))
assert.Equal(t, 8, RoundDown(10, 4))
assert.Equal(t, -8, RoundDown(-8, 4))
assert.Equal(t, -8, RoundDown(-7, 4))
} }

View file

@ -50,6 +50,25 @@ func (v Vector) Divided(val int) Vector {
return Vector{v.X / val, v.Y / val} return Vector{v.X / val, v.Y / val}
} }
// DividedFloor returns the vector divided but floors the value regardless
func (v Vector) DividedFloor(val int) Vector {
x := float64(v.X) / float64(val)
if x < 0 {
x = math.Floor(x)
} else {
x = math.Floor(x)
}
y := float64(v.Y) / float64(val)
if y < 0 {
y = math.Floor(y)
} else {
y = math.Floor(y)
}
return Vector{X: int(x), Y: int(y)}
}
// Abs returns an absolute version of the vector // Abs returns an absolute version of the 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)}