diff options
Diffstat (limited to 'scripts/bsp_level_generator.gd')
-rw-r--r-- | scripts/bsp_level_generator.gd | 256 |
1 files changed, 230 insertions, 26 deletions
diff --git a/scripts/bsp_level_generator.gd b/scripts/bsp_level_generator.gd index af34e0b..543eb1d 100644 --- a/scripts/bsp_level_generator.gd +++ b/scripts/bsp_level_generator.gd @@ -1,20 +1,38 @@ -extends Node2D +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 wall_inst = preload("res://models/wall.blend") +var locker_inst = preload("res://prefabs/locker.tscn") +var level_geo: Node3D + +enum Direction {LEFT, RIGHT} + +signal grid_generated(grid: Array[Tile], p_grid_width: int, p_grid_height: int) + var min_room_size: int = min_dim * min_dim -var rects: Array[Vector4i] = [] +var grid_width = width + 1 +var grid_height = height + 1 + +var rooms: Array[Rect2i] = [] class BSPNode: + var axis: int var min_dims: Vector2i var max_dims: Vector2i var left: BSPNode var right: BSPNode - func _init(p_min_dims: Vector2i, p_max_dims: Vector2i, p_left: BSPNode, p_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 @@ -22,24 +40,19 @@ class BSPNode: func generate_level(axis: int, min_space: Vector2i, max_space: Vector2i, depth: int = 0) -> BSPNode: var dims = max_space - min_space - if dims[axis] / 2 < min_dim: - #rects.append(Vector4i(min_space.x, min_space.y, max_space.x, max_space.y)) - return null - 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: - rects.append(Vector4i(min_space.x, min_space.y, max_space.x, max_space.y)) - return BSPNode.new(min_space, max_space, null, null) + 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) + #print("Spliting axis ", axis, " at ", split) var left_min_space = min_space var left_max_space = max_space @@ -51,28 +64,219 @@ func generate_level(axis: int, min_space: Vector2i, max_space: Vector2i, depth: var right_max_space = max_space var right = generate_level(new_axis, right_min_space, right_max_space, depth + 1) - assert((left == null and right == null) or (left != null and right != null)) + 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 check_door(grid: Array[Tile.Tile], axis: int, door_pos: Vector2i) -> bool: + var room_left = door_pos + room_left[axis] -= 1 + + var room_right = door_pos + room_right[axis] += 1 + + if door_pos.x >= grid_width or door_pos.y >= grid_height: + return false + if room_left.x >= grid_width or room_left.y >= grid_height: + return false + if room_right.x >= grid_width or room_right.y >= grid_height: + return false + + # Check if there are two spaces to connect + # And ensure no door already exists in the space + return grid[room_left.y * grid_width + room_left.x] == Tile.Tile.FLOOR \ + and grid[room_right.y * grid_width + room_right.x] == Tile.Tile.FLOOR \ + and grid[door_pos.y * grid_width + door_pos.x] == Tile.Tile.WALL + +func generate_grid(map: BSPNode, grid: Array[Tile.Tile]) -> void: + if is_leaf(map): + for y in range(map.min_dims.y, map.max_dims.y - 1): + for x in range(map.min_dims.x, map.max_dims.x - 1): + grid[(y+1) * grid_width + (x+1)] = Tile.Tile.FLOOR + + # TODO double check room dimensions are correct here + var room = Rect2i(map.min_dims + Vector2i.ONE, map.max_dims - map.min_dims) + rooms.append(room) + 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 + 1 + var door_pos2 = door_pos + door_pos2[other_axis] += 1 + + if check_door(grid, map.axis, door_pos) and check_door(grid, map.axis, door_pos2): + have_door = true + + # Place grid for mesh + grid[door_pos.y * grid_width + door_pos.x] = Tile.Tile.DOOR + grid[door_pos2.y * grid_width + door_pos2.x] = Tile.Tile.DOOR + #door_pos[map.axis] -= 1 + #grid[door_pos.y * grid_width + door_pos.x] = Tile.Tile.FLOOR + + tries += 1 + if tries > 1000: + print("Took too many attempts to generate a door") + get_tree().quit() + return + +func get_tile(grid: Array[Tile.Tile], pos: Vector2i) -> Tile.Tile: + return grid[pos.y * grid_width + pos.x] + +func place_lockers(grid: Array[Tile.Tile]) -> void: + for room in rooms: + var num_lockers = randi_range(0, 2) + print("Generating ", num_lockers, "Lockers") + for i in range(num_lockers): + var found = false + while not found: + var pos = Vector2i(randi_range(room.position.x, room.end.x - 1), randi_range(room.position.y, room.end.y - 1)) + if grid[pos.y * grid_width + pos.x] == Tile.Tile.FLOOR \ + and (get_tile(grid, pos + Vector2i(1, 0)) == Tile.Tile.WALL \ + or get_tile(grid, pos + Vector2i(-1, 0)) == Tile.Tile.WALL \ + or get_tile(grid, pos + Vector2i(0, 1)) == Tile.Tile.WALL \ + or get_tile(grid, pos + Vector2i(0, -1)) == Tile.Tile.WALL) \ + and get_tile(grid, pos + Vector2i(1, 0)) != Tile.Tile.DOOR \ + and get_tile(grid, pos + Vector2i(-1, 0)) != Tile.Tile.DOOR \ + and get_tile(grid, pos + Vector2i(0, 1)) != Tile.Tile.DOOR \ + and get_tile(grid, pos + Vector2i(0, -1)) != Tile.Tile.DOOR: + grid[pos.y * grid_width + pos.x] = Tile.Tile.LOCKER + found = true + +func place_player(grid: Array[Tile.Tile]) -> Rect2i: + var player_room: Rect2i = rooms.pick_random() + var player_pos = Vector2i(player_room.position.x + player_room.size.x / 2, player_room.position.y + player_room.size.y / 2) + + grid[player_pos.y * grid_width + player_pos.x] = Tile.Tile.PLAYER + return player_room + +func place_enemies(grid: Array[Tile.Tile], player_room: Rect2i) -> void: + var num_enemies = randi_range(4, 10) + + for i in range(num_enemies): + var found_pos = false + while not found_pos: + var room: Rect2i = rooms.pick_random() + if room == player_room: + continue + + var pos = Vector2i(randi_range(room.position.x, room.end.x - 1), randi_range(room.position.y, room.end.y - 1)) + if grid[pos.y * grid_width + pos.x] == Tile.Tile.FLOOR: + grid[pos.y * grid_width + pos.x] = Tile.Tile.ENEMY + found_pos = true + +func place_exit(grid: Array[Tile.Tile], player_room: Rect2i) -> void: + var found_pos = false + + while not found_pos: + var room: Rect2i = rooms.pick_random() + if room == player_room: + continue + + var pos = Vector2i(randi_range(room.position.x, room.end.x - 1), randi_range(room.position.y, room.end.y - 1)) + var left = pos + Vector2i.LEFT + var right = pos + Vector2i.RIGHT + var up = pos + Vector2i.UP + var down = pos + Vector2i.DOWN + if grid[pos.y * grid_width + pos.x] == Tile.Tile.FLOOR \ + and grid[left.y * grid_width + left.x] == Tile.Tile.FLOOR \ + and grid[right.y * grid_width + right.x] == Tile.Tile.FLOOR \ + and grid[up.y * grid_width + up.x] == Tile.Tile.FLOOR \ + and grid[down.y * grid_width + down.x] == Tile.Tile.FLOOR: + grid[pos.y * grid_width + pos.x] = Tile.Tile.PORTAL + found_pos = true + +func populate_grid(grid: Array[Tile.Tile]) -> void: + place_lockers(grid) + var player_room = place_player(grid) + place_exit(grid, player_room) + place_enemies(grid, player_room) + +func generate_plane(array: Array, pos: Vector3, dim: Array[Vector3], normal: Vector3) -> void: + var index = len(array[Mesh.ARRAY_VERTEX]) + array[Mesh.ARRAY_VERTEX].append(wall_thickness*pos) + array[Mesh.ARRAY_VERTEX].append(wall_thickness*(pos+dim[0])) + array[Mesh.ARRAY_VERTEX].append(wall_thickness*(pos+dim[0]+dim[1])) + array[Mesh.ARRAY_VERTEX].append(wall_thickness*(pos+dim[1])) - if left == null and right == null: - rects.append(Vector4i(min_space.x, min_space.y, max_space.x, max_space.y)) + array[Mesh.ARRAY_NORMAL].append(normal) + array[Mesh.ARRAY_NORMAL].append(normal) + array[Mesh.ARRAY_NORMAL].append(normal) + array[Mesh.ARRAY_NORMAL].append(normal) - return BSPNode.new(min_space, max_space, left, right) + array[Mesh.ARRAY_INDEX].append(index + 0) + array[Mesh.ARRAY_INDEX].append(index + 1) + array[Mesh.ARRAY_INDEX].append(index + 2) + array[Mesh.ARRAY_INDEX].append(index + 0) + array[Mesh.ARRAY_INDEX].append(index + 2) + array[Mesh.ARRAY_INDEX].append(index + 3) + +func generate_geo(grid: Array[Tile.Tile], array: Array) -> void: + for y in range(grid_height): + for x in range(grid_width): + var tile = grid[y * grid_width + x] + + if tile == Tile.Tile.WALL: + generate_plane(array, Vector3(x, 2, y), [Vector3(0, 0, 1), Vector3(0, -2, 0)], Vector3.LEFT) + generate_plane(array, Vector3(x, 2, y+1), [Vector3(1, 0, 0), Vector3(0, -2, 0)], Vector3.BACK) + generate_plane(array, Vector3(x+1, 2, y+1), [Vector3(0, 0, -1), Vector3(0, -2, 0)], Vector3.RIGHT) + generate_plane(array, Vector3(x+1, 2, y), [Vector3(-1, 0, 0), Vector3(0, -2, 0)], Vector3.FORWARD) + var wall: Node3D = level_geo.get_node("./Wall").duplicate() + wall.position = Vector3(x, 0, y) + add_child(wall) + else: + generate_plane(array, Vector3(x, 0, y), [Vector3(1, 0, 0), Vector3(0, 0, 1)], Vector3.UP) + var floor_tile: Node3D = level_geo.get_node("./Floor").duplicate() + floor_tile.position = Vector3(x, 0, y) + add_child(floor_tile) + + var ciel_tile: Node3D = level_geo.get_node("./Floor").duplicate() + ciel_tile.rotate_z(deg_to_rad(180)) + ciel_tile.position = Vector3(x+1, 2, y) + add_child(ciel_tile) + + if tile == Tile.Tile.LOCKER: + var locker: Node3D = locker_inst.instantiate() + locker.position = Vector3(x, 0, y) + add_child(locker) 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.Tile] = [] + grid.resize(grid_width * grid_height) + grid.fill(Tile.Tile.WALL) + + var surface_array = [] + surface_array.resize(Mesh.ARRAY_MAX) + + surface_array[Mesh.ARRAY_VERTEX] = PackedVector3Array() + surface_array[Mesh.ARRAY_INDEX] = PackedInt32Array() + surface_array[Mesh.ARRAY_NORMAL] = PackedVector3Array() + + generate_grid(map, grid) + populate_grid(grid) + grid_generated.emit(grid, grid_width, grid_height) + level_geo = wall_inst.instantiate() + generate_geo(grid, surface_array) -func _draw(): - var mult = 1 - for rect in rects: - var pos1 = 5*Vector2i(rect.x, rect.y) - var pos2 = 5*Vector2i(rect.z, rect.w) - var dims = pos2 - pos1 - draw_rect(Rect2(pos1, dims), mult*Color(0.01, 0.01, 0.01)) - draw_line(pos1, Vector2i(pos1.x, pos2.y), Color(0, 1, 0)) - draw_line(Vector2i(pos1.x, pos2.y), pos2, Color(0, 1, 0)) - draw_line(pos2, Vector2i(pos2.x, pos1.y), Color(0, 1, 0)) - draw_line(Vector2i(pos2.x, pos1.y), pos1, Color(0, 1, 0)) - mult += 2 + var mesh = ArrayMesh.new() + mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, surface_array) + var tri_mesh = mesh.create_trimesh_shape() + $NavigationRegion3D/StaticBody3D/CollisionShape3D.shape = tri_mesh + #$NavigationRegion3D.navigation_mesh = NavigationMesh.new() + #$NavigationRegion3D.navigation_mesh.radius + $NavigationRegion3D.bake_navigation_mesh(false) |