extends Node3D @export_category("Level Generator") @export var width: int = 40 @export var height: int = 40 @export var min_dim: int = 5 @export_category("Geometry Generator") ## Geometry generation config @export var wall_thickness: float = 1 var min_room_size: int = min_dim * min_dim enum Direction {LEFT, RIGHT} enum Tile {FLOOR, WALL} class BSPNode: var axis: int var min_dims: Vector2i var max_dims: Vector2i var left: BSPNode var right: BSPNode func _init(p_axis: int, p_min_dims: Vector2i, p_max_dims: Vector2i, p_left: BSPNode, p_right: BSPNode): axis = p_axis min_dims = p_min_dims max_dims = p_max_dims left = p_left right = p_right func generate_level(axis: int, min_space: Vector2i, max_space: Vector2i, depth: int = 0) -> BSPNode: var dims = max_space - min_space var new_axis = (axis + 1) % 2 # 10% we stop here and just create a big room if (depth > 2 and randi_range(0, 9) == 0) \ or dims.x * dims.y <= min_dim * min_dim \ or dims[new_axis] / 2 < min_dim: return BSPNode.new(new_axis, min_space, max_space, null, null) # Calculate min and max ranges so that a split # doesn't create a room that violates min dimensions var min_value = min_space[axis] + min_dim var max_value = max_space[axis] - min_dim var split = randi_range(min_value, max_value) #print("Spliting axis ", axis, " at ", split) var left_min_space = min_space var left_max_space = max_space left_max_space[axis] = split var left = generate_level(new_axis, left_min_space, left_max_space, depth + 1) var right_min_space = min_space right_min_space[axis] = split var right_max_space = max_space var right = generate_level(new_axis, right_min_space, right_max_space, depth + 1) return BSPNode.new(axis, min_space, max_space, left, right) func is_leaf(node: BSPNode) -> bool: return node.left == null and node.right == null func generate_grid(map: BSPNode, grid: Array[Tile]): if is_leaf(map): for y in range(map.min_dims.y + 1, map.max_dims.y - 1): for x in range(map.min_dims.x + 1, map.max_dims.x - 1): grid[y * width + x] = Tile.FLOOR else: generate_grid(map.left, grid) generate_grid(map.right, grid) # Look for space on dividing wall to place door var other_axis = (map.axis + 1) % 2 var split_axis = map.left.max_dims[map.axis] var have_door = false var tries = 0 while not have_door: var test_door = randi_range(map.min_dims[other_axis], map.max_dims[other_axis] - 1) var door_pos = Vector2i.ZERO door_pos[map.axis] = split_axis door_pos[other_axis] = test_door var room_left = door_pos room_left[map.axis] -= 2 var room_right = door_pos room_right[map.axis] += 1 # Check if there are two spaces to connect # And ensure no door already exists in the space if grid[room_left.y * width + room_left.x] == Tile.FLOOR \ and grid[room_right.y * width + room_right.x] == Tile.FLOOR \ and grid[door_pos.y * width + door_pos.x] == Tile.WALL: have_door = true grid[door_pos.y * width + door_pos.x] = Tile.FLOOR door_pos[map.axis] -= 1 grid[door_pos.y * width + door_pos.x] = Tile.FLOOR tries += 1 if tries > 1000: print("Took too many attempts to generate a door") get_tree().quit() func generate_geo(grid: Array[Tile]): var csg_root = CSGCombiner3D.new() for y in range(height): for x in range(width): var tile = grid[y * width + x] if tile == Tile.FLOOR: var box = CSGBox3D.new() box.size = Vector3(1.5, 0.1, 1.5) box.position = 1.5*Vector3(x, 0, y) + 0.5 * box.size csg_root.add_child(box) elif tile == Tile.WALL: var box = CSGBox3D.new() box.size = Vector3(1.5, 2, 1.5) box.position = 1.5*Vector3(x, 0, y) + 0.5 * box.size csg_root.add_child(box) csg_root.use_collision = true add_child(csg_root) func _ready() -> void: var starting_axis = randi_range(0, 1) var min_space = Vector2i(0, 0) var max_space = Vector2i(width, height) var map = generate_level(starting_axis, min_space, max_space) var grid: Array[Tile] = [] grid.resize(width * height) grid.fill(Tile.WALL) generate_grid(map, grid) generate_geo(grid)