Compare commits

...

5 Commits

Author SHA1 Message Date
copygirl 80e4008ac6 Improve GeneratorSimple's terrain shape 7 days ago
copygirl cb18c2206a Add Chunk.region field 7 days ago
copygirl 9c242ff1d1 Fix HALF_SLOPE shape 7 days ago
copygirl 4b064db838 Fix bug in shape definition 1 week ago
copygirl ad76ef1703 Add BlockRegion helper class 1 week ago
  1. 45
      world/block_region.gd
  2. 1
      world/block_region.gd.uid
  3. 11
      world/chunk.gd
  4. 252
      world/generation/generator_simple.gd
  5. 58
      world/layers/matter.gd
  6. 67
      world/layers/shape.gd

@ -0,0 +1,45 @@
class_name BlockRegion
extends RefCounted
static var LOCAL_CHUNK := BlockRegion.new(Vector2i.ZERO, Vector2i.ONE * (Chunk.SIZE - 1))
var min: Vector2i
var max: Vector2i
func _init(a: Vector2i, b: Vector2i) -> void:
min = a.min(b)
max = a.max(b)
func size() -> Vector2i:
return max - min + Vector2i.ONE
func contains(pos: Vector2i) -> bool:
return (pos.x >= min.x) && (pos.x <= max.x) and (pos.y >= min.y) && (pos.y <= max.y)
func offset(value: Vector2i) -> BlockRegion:
return BlockRegion.new(min + value, max + value)
func extend(amount: int) -> BlockRegion:
var v := Vector2i.ONE * amount
return BlockRegion.new(min - v, max + v)
func array_index(pos: Vector2i) -> int:
assert(contains(pos))
var size := size()
var index := (pos.x - min.x) + (pos.y - min.y) * size.x
return clampi(index, 0, size.x * size.y)
# Custom iterators in GDScript are WEIRD.
func _iter_init(state: Array) -> bool:
state[0] = min
return true
func _iter_next(state: Array) -> bool:
var cur: Vector2i = state[0]
if cur.x < max.x: state[0].x += 1; return true
elif cur.y < max.y: state[0] = Vector2i(min.x, cur.y + 1); return true
else: return false
func _iter_get(state: Variant) -> Vector2i:
return state

@ -0,0 +1 @@
uid://w5fuyrkwf40c

@ -7,6 +7,7 @@ const SIZE := 32 # blocks
signal clean
var chunk_pos: Vector2i
var region: BlockRegion
var dirty := false
var tracked_by: Dictionary[ChunkLoader, Variant] # value is unused
@ -19,6 +20,11 @@ func get_players_tracking() -> Array[Player]:
func _init(chunk_pos: Vector2i) -> void:
self.chunk_pos = chunk_pos
var min := chunk_pos * Chunk.SIZE
var max := (chunk_pos + Vector2i.ONE) * Chunk.SIZE - Vector2i.ONE
region = BlockRegion.new(min, max)
name = "Chunk %s" % chunk_pos
position = (Vector2(chunk_pos) + Vector2.ONE / 2) * (Block.SIZE * SIZE)
@ -76,7 +82,4 @@ static func pos_to_center(chunk_pos: Vector2i) -> Vector2:
## Creates an integer index from a chunk-local block position that can be used to index into an array.
static func array_index(local_pos: Vector2i) -> int:
assert((local_pos.x >= 0) and (local_pos.x < Chunk.SIZE))
assert((local_pos.y >= 0) and (local_pos.y < Chunk.SIZE))
# Using `posmod` ensures `local_pos` is valid, and this returns a valid index.
return posmod(local_pos.x, Chunk.SIZE) + posmod(local_pos.y, Chunk.SIZE) * Chunk.SIZE
return BlockRegion.LOCAL_CHUNK.array_index(local_pos)

@ -4,7 +4,7 @@ extends RefCounted
const NAME := "simple"
var noise := FastNoiseLite.new()
var array := PackedFloat32Array()
var array := PackedByteArray()
var bias_air := 0.0 # Above this, always air
var bias_solid := 64.0 # Below this, always solid
@ -17,44 +17,224 @@ func _init() -> void:
noise.fractal_lacunarity = 2.5
noise.fractal_octaves = 3
array.resize((Chunk.SIZE + 1) * (Chunk.SIZE + 1))
array.resize((Chunk.SIZE + 4) * (Chunk.SIZE + 4))
func generate(world: World, chunk: Chunk) -> void:
const THRESHOLD := 0.5
var region := chunk.region.extend(2)
var i := 0
for y in Chunk.SIZE + 1:
for x in Chunk.SIZE + 1:
var local_pos := Vector2i(x, y)
var block_pos := (chunk.chunk_pos * Chunk.SIZE) + local_pos
var bias := (block_pos.y - bias_air) / (bias_solid - bias_air)
array[i] = noise.get_noise_2dv(Vector2(block_pos)) + bias
i += 1
for pos in region:
var bias := (pos.y - bias_air) / (bias_solid - bias_air)
array[i] = int((noise.get_noise_2dv(Vector2(pos)) + bias) >= THRESHOLD)
i += 1
var matter: Matter.Layer = chunk.get_or_create_layer(Matter)
var shapes: Shape.Layer = chunk.get_or_create_layer(Shape)
for x in Chunk.SIZE:
for y in Chunk.SIZE:
var local_pos := Vector2i(x, y)
var shape: Shape
match _values(local_pos):
[ true, true, true, true ]: shape = Shape.FULL
[ false, true, true, true ]: shape = Shape.Base.SLOPE.variants[0]
[ true, false, true, true ]: shape = Shape.Base.SLOPE.variants[2]
[ true, true, false, true ]: shape = Shape.Base.SLOPE.variants[3]
[ true, true, true, false ]: shape = Shape.Base.SLOPE.variants[1]
[ false, false, true, true ]: shape = Shape.Base.HALF.variants[0]
[ true, false, false, true ]: shape = Shape.Base.HALF.variants[2]
[ true, true, false, false ]: shape = Shape.Base.HALF.variants[3]
[ false, true, true, false ]: shape = Shape.Base.HALF.variants[1]
if shape:
matter.set_at(local_pos, Matter.PLASTIC)
shapes.set_at(local_pos, shape)
func _values(pos: Vector2i) -> Array[bool]:
return [
_v(pos, 0, 0) >= 0.5,
_v(pos, 1, 0) >= 0.5,
_v(pos, 1, 1) >= 0.5,
_v(pos, 0, 1) >= 0.5,
]
func _v(pos: Vector2i, x: float, y: float) -> float:
return array[(pos.x + x) + (pos.y + y) * (Chunk.SIZE + 1)]
for pos in BlockRegion.LOCAL_CHUNK:
var shape: Shape
var a := array
const W := (Chunk.SIZE + 4)
i = (pos.x + 2) + (pos.y + 2) * W
var values: Array[int] = [
a[i-2-W*2], a[i-1-W*2], a[i-W*2], a[i+1-W*2], a[i+2-W*2],
a[i-2-W ], a[i-1-W ], a[i-W ], a[i+1-W ], a[i+2-W ],
a[i-2 ], a[i-1 ], a[i ], a[i+1 ], a[i+2 ],
a[i-2+W ], a[i-1+W ], a[i+W ], a[i+1+W ], a[i+2+W ],
a[i-2+W*2], a[i-1+W*2], a[i+W*2], a[i+1+W*2], a[i+2+W*2],
]
# TODO: Implement custom pattern matching that can handle rotation and mirroring.
# var x := -1
# var pattern: Array[int] = [
# x, x, x, x, x,
# x, x, 0, 0, 0,
# x, 0, 0, 1, 1,
# x, 1, 1, 1, x,
# x, x, x, x, x,
# ]
match values:
# HALF_SLOPE (non-mirrored)
[
_, _, _, _, _,
_, _, 0, 0, 0,
_, 0, 0, 1, 1,
_, 1, 1, 1, _,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE.variants[0]
[
_, 0, 1, _, _,
_, 0, 1, 1, _,
_, 0, 0, 1, _,
_, _, 0, 1, _,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE.variants[1]
[
_, _, _, _, _,
_, 1, 1, 1, _,
1, 1, 0, 0, _,
0, 0, 0, _, _,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE.variants[2]
[
_, _, _, _, _,
_, 1, 0, _, _,
_, 1, 0, 0, _,
_, 1, 1, 0, _,
_, _, 1, 0, _,
]:
shape = Shape.Base.HALF_SLOPE.variants[3]
[
_, _, _, _, _,
_, 0, 0, 0, _,
0, 0, 1, 1, _,
1, 1, 1, _, _,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE_BIG.variants[0]
[
_, _, _, _, _,
_, 0, 1, _, _,
_, 0, 1, 1, _,
_, 0, 0, 1, _,
_, _, 0, 1, _,
]:
shape = Shape.Base.HALF_SLOPE_BIG.variants[1]
[
_, _, _, _, _,
_, _, 1, 1, 1,
_, 1, 1, 0, 0,
_, 0, 0, 0, _,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE_BIG.variants[2]
[
_, 1, 0, _, _,
_, 1, 0, 0, _,
_, 1, 1, 0, _,
_, _, 1, 0, _,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE_BIG.variants[3]
# HALF_SLOPE (mirrored)
[
_, _, _, _, _,
0, 0, 0, _, _,
1, 1, 0, 0, _,
_, 1, 1, 1, _,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE.variants[4]
[
_, _, _, _, _,
_, _, 0, 1, _,
_, 0, 0, 1, _,
_, 0, 1, 1, _,
_, 0, 1, _, _,
]:
shape = Shape.Base.HALF_SLOPE.variants[5]
[
_, _, _, _, _,
_, 1, 1, 1, _,
_, 0, 0, 1, 1,
_, _, 0, 0, 0,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE.variants[6]
[
_, _, 1, 0, _,
_, 1, 1, 0, _,
_, 1, 0, 0, _,
_, 1, 0, _, _,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE.variants[7]
[
_, _, _, _, _,
_, 0, 0, 0, _,
_, 1, 1, 0, 0,
_, _, 1, 1, 1,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE_BIG.variants[4]
[
_, _, 0, 1, _,
_, 0, 0, 1, _,
_, 0, 1, 1, _,
_, 0, 1, _, _,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE_BIG.variants[5]
[
_, _, _, _, _,
1, 1, 1, _, _,
0, 0, 1, 1, _,
_, 0, 0, 0, _,
_, _, _, _, _,
]:
shape = Shape.Base.HALF_SLOPE_BIG.variants[6]
[
_, _, _, _, _,
_, _, 1, 0, _,
_, 1, 1, 0, _,
_, 1, 0, 0, _,
_, 1, 0, _, _,
]:
shape = Shape.Base.HALF_SLOPE_BIG.variants[7]
# SLOPE
[
_, _, _, _, _,
_, _, 0, _, _,
_, 0, 0, 1, _,
_, _, 1, _, _,
_, _, _, _, _,
]:
shape = Shape.Base.SLOPE.variants[0]
[
_, _, _, _, _,
_, _, 1, _, _,
_, 0, 0, 1, _,
_, _, 0, _, _,
_, _, _, _, _,
]:
shape = Shape.Base.SLOPE.variants[1]
[
_, _, _, _, _,
_, _, 1, _, _,
_, 1, 0, 0, _,
_, _, 0, _, _,
_, _, _, _, _,
]:
shape = Shape.Base.SLOPE.variants[2]
[
_, _, _, _, _,
_, _, 0, _, _,
_, 1, 0, 0, _,
_, _, 1, _, _,
_, _, _, _, _,
]:
shape = Shape.Base.SLOPE.variants[3]
# FULL
[
_, _, _, _, _,
_, _, _, _, _,
_, _, 1, _, _,
_, _, _, _, _,
_, _, _, _, _,
]:
shape = Shape.FULL
if shape:
matter.set_at(pos, Matter.PLASTIC)
shapes.set_at(pos, shape)

@ -5,6 +5,7 @@ static var REGISTRY := Registry.new()
static var NONE := Matter.new("none")
static var PLASTIC := Matter.new("plastic")
## Default matter to return when chunk or layer is not found.
static var DEFAULT := NONE
var id : int
@ -41,16 +42,17 @@ class Layer extends MeshInstance2D:
@rpc("reliable")
func set_id_at(pos: Vector2i, id: int) -> void:
var index := Chunk.array_index(pos)
if data[index] != id:
# TODO: Keep track of how many non-air blocks are present, to allow for cleaning empty chunks.
data[index] = id
chunk.dirty = true
if data[index] == id: return
# TODO: Keep track of how many non-air blocks are present, to allow for cleaning empty chunks.
data[index] = id
chunk.dirty = true
if multiplayer.is_server():
# Send change to every player tracking this chunk.
if multiplayer.is_server():
for player in chunk.get_players_tracking():
if player.network.is_local: continue # skip server player
set_id_at.rpc_id(player.network.peer_id, pos, id)
for player in chunk.get_players_tracking():
if player.network.is_local: continue # skip server player
set_id_at.rpc_id(player.network.peer_id, pos, id)
func _ready() -> void:
@ -66,27 +68,25 @@ class Layer extends MeshInstance2D:
var shapes: Shape.Layer = chunk.get_layer_or_null(Shape)
if shapes: # Should exist, but let's sanity check.
for x in Chunk.SIZE:
for y in Chunk.SIZE:
var pos := Vector2i(x, y)
var offset := (Vector2.ONE / 2) + Vector2(pos)
# TODO: Support different matter.
# var matter := get_at(pos)
var shape := shapes.get_at(pos)
if shape.points.is_empty(): continue
any_blocks = true
for i in range(2, shape.points.size()):
var uv0 := shape.uvs[0 ]
var uv1 := shape.uvs[i-1]
var uv2 := shape.uvs[i ]
var p0 := (offset + shape.points[0 ]) * Block.SIZE - CHUNK_HALF_SIZE
var p1 := (offset + shape.points[i-1]) * Block.SIZE - CHUNK_HALF_SIZE
var p2 := (offset + shape.points[i ]) * Block.SIZE - CHUNK_HALF_SIZE
m.surface_set_uv(uv0); m.surface_add_vertex_2d(p0)
m.surface_set_uv(uv1); m.surface_add_vertex_2d(p1)
m.surface_set_uv(uv2); m.surface_add_vertex_2d(p2)
for pos in BlockRegion.LOCAL_CHUNK:
var offset := (Vector2.ONE / 2) + Vector2(pos)
# TODO: Support different matter.
# var matter := get_at(pos)
var shape := shapes.get_at(pos)
if shape.points.is_empty(): continue
any_blocks = true
for i in range(2, shape.points.size()):
var uv0 := shape.uvs[0 ]
var uv1 := shape.uvs[i-1]
var uv2 := shape.uvs[i ]
var p0 := (offset + shape.points[0 ]) * Block.SIZE - CHUNK_HALF_SIZE
var p1 := (offset + shape.points[i-1]) * Block.SIZE - CHUNK_HALF_SIZE
var p2 := (offset + shape.points[i ]) * Block.SIZE - CHUNK_HALF_SIZE
m.surface_set_uv(uv0); m.surface_add_vertex_2d(p0)
m.surface_set_uv(uv1); m.surface_add_vertex_2d(p1)
m.surface_set_uv(uv2); m.surface_add_vertex_2d(p2)
if any_blocks:
m.surface_end()

@ -9,6 +9,7 @@ static func lookup(id: int) -> Shape:
static var EMPTY : Shape
static var FULL : Shape
## Default shape to return when chunk or layer is not found.
static var DEFAULT : Shape
var id : int
@ -34,12 +35,14 @@ func _init(base: Base, mirror: bool, angle: int, shape: Shape2D, points: PackedV
uvs.append((Vector2.ONE / 2) + point)
## Represents a base shape, before it has been rotated and mirrored
## (if applicable) to create a number of variants based on that shape.
class Base:
static var EMPTY := Base.new("empty")
static var FULL := Base.new("full" , _rect())
static var HALF := Base.new("half" , [ 0,1, 2,1, 2,2, 0,2 ], true)
static var SLOPE := Base.new("slope" , [ 0,1, 1,0, 1,1 ], true)
static var HALF_SLOPE := Base.new("half_slope" , [ 0,2, 1,0, 2,2 ], true, true)
static var HALF_SLOPE := Base.new("half_slope" , [ 0,2, 2,1, 2,2 ], true, true)
static var HALF_SLOPE_BIG := Base.new("half_slope_big", [ 0,1, 2,0, 2,2, 0,2 ], true, true)
static func _static_init() -> void:
@ -60,28 +63,25 @@ class Base:
shape = _rotate(shape) # rotate by 90 degrees (swap x and y components)
variants.append(Shape.new(self, false, 0, shape, _points(shape)))
elif shape is Array:
# Convert points (pairs of ints) in array to convex shape.
# Convert points (pairs of `int`s) in `Array` to convex shape.
var factor := float(shape.max())
var points := PackedVector2Array()
var points_orig := PackedVector2Array() # untransformed points
for i in range(0, shape.size(), 2):
var point := Vector2(shape[i] / factor, shape[i + 1] / factor)
points.append(point - (Vector2.ONE / 2)) # ensure shape is centered
points_orig.append(point - (Vector2.ONE / 2)) # ensure shape is centered
# Add variations of the shape (rotated, mirrored), including the standard shape.
for mirror in [ false, true ] if mirrored else [ false ]:
if mirror:
# Mirror (flip) points along the X axis.
points = points.duplicate()
for i in points.size():
points[i] = points[i] * Transform2D.FLIP_X
for angle in [ 0, 90, 180, 270 ] if rotated else [ 0 ]:
if angle != 0:
# Rotate points around the center.
var points := points_orig.duplicate() # transformed points
# Technically we could skip `duplicate()` if not mirrored and not rotated, but eh.
if mirror: # Mirror (flip) points along the X axis.
for i in points.size(): points[i] *= Transform2D.FLIP_X
if angle != 0: # Rotate points around the center.
var transform := Transform2D(deg_to_rad(angle), Vector2.ZERO)
points = points.duplicate()
for i in points.size():
points[i] = points[i] * transform
for i in points.size(): points[i] *= transform
var shape_points: PackedVector2Array
for point in points: shape_points.append(point * Block.SIZE)
@ -130,16 +130,17 @@ class Layer extends StaticBody2D:
@rpc("reliable")
func set_id_at(pos: Vector2i, id: int) -> void:
var index := Chunk.array_index(pos)
if data[index] != id:
# TODO: Keep track of how many non-air blocks are present, to allow for cleaning empty chunks.
data[index] = id
chunk.dirty = true
if data[index] == id: return
# TODO: Keep track of how many non-air blocks are present, to allow for cleaning empty chunks.
data[index] = id
chunk.dirty = true
if multiplayer.is_server():
# Send change to every player tracking this chunk.
if multiplayer.is_server():
for player in chunk.get_players_tracking():
if player.network.is_local: continue # skip server player
set_id_at.rpc_id(player.network.peer_id, pos, id)
for player in chunk.get_players_tracking():
if player.network.is_local: continue # skip server player
set_id_at.rpc_id(player.network.peer_id, pos, id)
func _ready() -> void:
@ -155,14 +156,12 @@ class Layer extends StaticBody2D:
if child is CollisionShape2D:
child.queue_free()
for x in Chunk.SIZE:
for y in Chunk.SIZE:
var pos := Vector2i(x, y)
var shape := get_at(pos)
if shape.shape == null: continue
var shape2d := CollisionShape2D.new()
shape2d.name = "Block %s" % pos
shape2d.position = (Vector2(pos) + (Vector2.ONE / 2)) * Block.SIZE - CHUNK_HALF_SIZE
shape2d.shape = shape.shape
add_child(shape2d)
for pos in BlockRegion.LOCAL_CHUNK:
var shape := get_at(pos)
if shape.shape == null: continue
var shape2d := CollisionShape2D.new()
shape2d.name = "Block %s" % pos
shape2d.position = (Vector2(pos) + (Vector2.ONE / 2)) * Block.SIZE - CHUNK_HALF_SIZE
shape2d.shape = shape.shape
add_child(shape2d)

Loading…
Cancel
Save