2D multiplayer platformer using Godot Engine
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

167 lines
5.7 KiB

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
## Default shape to return when chunk or layer is not found.
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)
## 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, 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:
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 `int`s) in `Array` to convex shape.
var factor := float(shape.max())
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_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 ]:
for angle in [ 0, 90, 180, 270 ] if rotated else [ 0 ]:
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)
for i in points.size(): 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: 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.
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 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)