class_name Shape # FIXME: Potential incompatibility between different game versions that have different shapes. static var REGISTRY: Array[Shape] static func lookup(id: int) -> Shape: return REGISTRY[id] if (id >= 0) and (id < REGISTRY.size()) else EMPTY # Initialized in `Base._static_init`. static var EMPTY : Shape static var FULL : Shape static var DEFAULT : Shape var id : int var base : Base var mirror : bool var angle : int var shape : Shape2D var points : PackedVector2Array var uvs : PackedVector2Array func _init(base: Base, mirror: bool, angle: int, shape: Shape2D, points: PackedVector2Array) -> void: id = REGISTRY.size() REGISTRY.append(self) self.base = base self.mirror = mirror self.angle = angle self.shape = shape self.points = points # TODO: Define UVs properly. for point in points: uvs.append((Vector2.ONE / 2) + point) 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_BIG := Base.new("half_slope_big", [ 0,1, 2,0, 2,2, 0,2 ], true, true) static func _static_init() -> void: Shape.EMPTY = EMPTY.variants[0] Shape.FULL = FULL .variants[0] Shape.DEFAULT = Shape.EMPTY var name : String var variants : Array[Shape] func _init(name: String, shape = null, rotated := false, mirrored := false) -> void: self.name = name if shape == null: variants.append(Shape.new(self, false, 0, null, PackedVector2Array())) elif shape is RectangleShape2D: variants.append(Shape.new(self, false, 0, shape, _points(shape))) if rotated: 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. var factor := float(shape.max()) var points := PackedVector2Array() 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 # 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 transform := Transform2D(deg_to_rad(angle), Vector2.ZERO) points = points.duplicate() for i in points.size(): points[i] = points[i] * transform var shape_points: PackedVector2Array for point in points: shape_points.append(point * Block.SIZE) shape = ConvexPolygonShape2D.new() shape.points = shape_points variants.append(Shape.new(self, mirror, angle, shape, points)) else: breakpoint static func _rect(w := 1.0, h := 1.0) -> RectangleShape2D: var rect := RectangleShape2D.new() rect.size = Vector2(w, h) * Block.SIZE return rect static func _rotate(rect: RectangleShape2D) -> RectangleShape2D: return _rect(rect.size.y, rect.size.x) static func _points(rect: RectangleShape2D) -> PackedVector2Array: var w := rect.size.x / Block.SIZE / 2 var h := rect.size.y / Block.SIZE / 2 return [ Vector2(-w, -h), Vector2(w, -h), Vector2(w, h), Vector2(-w, h) ] class Layer extends StaticBody2D: var data: PackedByteArray var chunk: Chunk: get: return get_parent() func _init() -> void: data.resize(Chunk.SIZE * Chunk.SIZE) static func load(buffer: StreamBuffer) -> Layer: var result := Layer.new() buffer.read_raw_buffer_into(result.data) return result func save(buffer: StreamBuffer) -> void: buffer.write_raw_buffer(data) func get_at(pos: Vector2i) -> Shape: var index := Chunk.array_index(pos) return Shape.lookup(data[index]) func set_at(pos: Vector2i, value: Shape) -> void: set_id_at(pos, value.id) @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 # 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) func _ready() -> void: # TODO: Only update if shapes have been changed. chunk.clean.connect(_update_shapes) _update_shapes() func _update_shapes() -> void: const CHUNK_HALF_SIZE := (Vector2.ONE * Chunk.SIZE * Block.SIZE) / 2 # FIXME: Updating the shape like this is not ideal. for child in get_children(): 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)