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
Chunks []Chunk `json:"chunks"`
// CurrentSize is the current width/height of the atlas in chunks
CurrentSize vector.Vector `json:"currentSize"`
// LowerBound is the origin of the bottom left corner of the current chunks in world space (current chunks cover >= this value)
LowerBound vector.Vector `json:"lowerBound"`
// WorldOrigin represents the location of the [0,0] world space point in terms of the allotted current chunks
WorldOrigin vector.Vector `json:"worldOrigin"`
// UpperBound is the top left corner of the current chunks (curent chunks cover < this value)
UpperBound vector.Vector `json:"upperBound"`
// ChunkSize is the x/y dimensions of each square chunk
ChunkSize int `json:"chunksize"`
@ -54,8 +54,8 @@ func NewAtlas(chunkSize int) Atlas {
a := Atlas{
ChunkSize: chunkSize,
Chunks: make([]Chunk, 1),
CurrentSize: vector.Vector{X: 1, Y: 1},
WorldOrigin: vector.Vector{X: 0, Y: 0},
LowerBound: vector.Vector{X: 0, Y: 0},
UpperBound: vector.Vector{X: chunkSize, Y: chunkSize},
}
// Initialise the first chunk
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)}
}
// worldSpaceToChunk gets the current chunk ID for a position in the world
func (a *Atlas) worldSpaceToChunk(v vector.Vector) int {
// First convert to chunk space
chunkSpace := a.worldSpaceToChunkSpace(v)
// worldSpaceToChunkID gets the current chunk ID for a position in the world
func (a *Atlas) worldSpaceToChunkIndex(v vector.Vector) int {
// Shift the vector by our current min
v = v.Added(a.LowerBound.Negated())
// Then return the ID
return a.chunkSpaceToChunk(chunkSpace)
// Divide by the current size and floor, to get chunk-scaled vector from the lower bound
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
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
// chunkOriginInWorldSpace returns the origin of the chunk in world space
func (a *Atlas) chunkOriginInWorldSpace(chunk int) vector.Vector {
// convert the chunk to chunk space
chunkSpace := a.chunkToChunkSpace(chunk)
// Calculate the width
width := a.UpperBound.X - a.LowerBound.X
widthInChunks := width / a.ChunkSize
// 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.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),
// Reverse the along the corridor and up the stairs
v := vector.Vector{
X: chunk % widthInChunks,
Y: chunk / widthInChunks,
}
// 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) {
min = a.WorldOrigin.Negated()
max = min.Added(a.CurrentSize)
// getNewBounds gets new lower and upper bounds for the world space given a vector
func (a *Atlas) getNewBounds(v vector.Vector) (lower vector.Vector, upper vector.Vector) {
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
}
// 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)
// 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 {
return a.worldSpaceToChunkIndex(v)
}
// Calculate the new origin and the new size
origin := min
size := a.CurrentSize
// Calculate the new bounds
lower, upper := a.getNewBounds(v)
size := upper.Added(lower.Negated())
size = size.Divided(a.ChunkSize)
// If we need to shift the origin back
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
// Create the new empty atlas
newAtlas := Atlas{
ChunkSize: a.ChunkSize,
WorldOrigin: origin.Negated(),
CurrentSize: size,
LowerBound: lower,
UpperBound: upper,
Chunks: make([]Chunk, size.X*size.Y),
}
// Copy all old chunks into the new atlas
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
newAtlas.Chunks[newChunk] = chunkData
}
// Copy the new atlas data into this one
// Overwrite the old atlas with this one
*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
a.QueryPosition(vector.Vector{X: -1, Y: -1})
a.QueryPosition(vector.Vector{X: 0, Y: 0})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 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)
chunkID = a.worldSpaceToChunk(vector.Vector{X: 0, Y: -1})
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 0, Y: -1})
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)
chunkID = a.worldSpaceToChunk(vector.Vector{X: -1, Y: 0})
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -1, Y: 0})
assert.Equal(t, 2, chunkID)
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
chunkID = a.worldSpaceToChunk(vector.Vector{X: 1, Y: 1})
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: 1})
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)
chunkID = a.worldSpaceToChunk(vector.Vector{X: -2, Y: -2})
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -2, Y: -2})
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)
a = NewAtlas(2)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(vector.Vector{X: 5, Y: 5})
a.QueryPosition(vector.Vector{X: -5, Y: -5})
// 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
// ----------------
@ -67,14 +73,96 @@ func TestAtlas_toChunk(t *testing.T) {
// 4 | 5 || 6 | 7
// ----------------
// 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)
chunkID = a.worldSpaceToChunk(vector.Vector{X: 1, Y: -3})
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: 1, Y: -3})
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)
chunkID = a.worldSpaceToChunk(vector.Vector{X: -2, Y: 2})
chunkID = a.worldSpaceToChunkIndex(vector.Vector{X: -2, Y: 2})
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) {
@ -137,3 +225,26 @@ func TestAtlas_Grown(t *testing.T) {
tile, _ = a.QueryPosition(vector.Vector{X: 1, Y: -2})
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
}
// 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, -4, Min(-4, 1))
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}
}
// 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
func (v Vector) Abs() Vector {
return Vector{maths.Abs(v.X), maths.Abs(v.Y)}