In this lesson we’ll build on top of the previous part to expand the dungeon generator with mixed rectangular and organic rooms.
To render out the organic room data we’ll use the Geometry.is_point_in_polygon()
built-in function, which as the name implies, checks if a point falls within the given polygon.
Add the following at the end of your BasicDungeon.gd
script:
func _lessv_x(v1: Vector2, v2: Vector2) -> bool: return v1.x < v2.x func _lessv_y(v1: Vector2, v2: Vector2) -> bool: return v1.y < v2.y
These are helper functions which we’ll use in _add_room()
once we update it to also create polygonal rooms.
Before moving on, also make sure to declare:
const FACTOR := 1.0 / 8.0
at the top of your script as well. It’s a constant we’ll be using in _add_room()
:
func _add_room(rng: RandomNumberGenerator, data: Dictionary, rooms: Array, room: Rect2) -> void: rooms.push_back(room) if rng.randi_range(0, 1) == 0: for x in range(room.position.x, room.end.x): for y in range(room.position.y, room.end.y): data[Vector2(x, y)] = null else: var unit := FACTOR * room.size var order := [ room.grow_individual(-unit.x, 0, -unit.x, unit.y - room.size.y), room.grow_individual(unit.x - room.size.x, -unit.y, 0, -unit.y), room.grow_individual(-unit.x, unit.y - room.size.y, -unit.x, 0), room.grow_individual(0, -unit.y, unit.x - room.size.x, -unit.y) ] var poly := [] for index in range(order.size()): var rect: Rect2 = order[index] var is_even := index % 2 == 0 var poly_partial := [] for r in range(rng.randi_range(1, 2)): poly_partial.push_back(Vector2( rng.randf_range(rect.position.x, rect.end.x), rng.randf_range(rect.position.y, rect.end.y) )) poly_partial.sort_custom(self, "_lessv_x" if is_even else "_lessv_y") if index > 1: poly_partial.invert() poly += poly_partial for x in range(room.position.x, room.end.x): for y in range(room.position.y, room.end.y): var point := Vector2(x, y) if Geometry.is_point_in_polygon(point, poly): data[point] = null
That’s all there is to it. Now we have a working basic dungeon generator that picks either rectangular or organic (polygonal) rooms to place on each iteration.
Let’s go over the modifications.
Since we’ll have to pick which type of room to place: rectangular or organic, we need to pass in the RandomNumberGenerator
so we have to modify the function signature appropriately:
func _add_room(rng: RandomNumberGenerator, data: Dictionary, rooms: Array, room: Rect2) -> void:
We then pick either the rectangular room or the organic room with a 50% chance based on rng.randi_range(0, 1)
. The code for adding the rectangular room hasn’t changed so we’ll focus on the else branch:
unit
size based on FACTOR
and the given room size.unit
, we create the four rectangles in an ordered array with the help of Rect2.grow_individual()
.poly_partial
._lessv_x()
or _lessv_y()
depending on index
to sort this array so that we have the points in clock-wise order. Each iteration we append poly_partial
to poly
. Now we’re ready to generate the actual room data based on this polygon.room.position
and room.end
, just like in the rectangular rooms case, but this time we check if the given point
is inside the constructed polygon with the help of Geometry.is_point_in_polygon()
. We save the information only if the test succeeds.And that’s all there is to it. Now we have extended our algorithm to generate both rectangular and organic rooms randomly.
Here’s the entire code up to this point:
extends Node2D const FACTOR := 1.0 / 8.0 export var level_size := Vector2(100, 80) export var rooms_size := Vector2(10, 14) export var rooms_max := 15 onready var level: TileMap = $Level onready var camera: Camera2D = $Camera2D func _ready() -> void: _setup_camera() _generate() func _setup_camera() -> void: camera.position = level.map_to_world(level_size / 2) var z := max(level_size.x, level_size.y) / 8 camera.zoom = Vector2(z, z) func _generate() -> void: level.clear() for vector in _generate_data(): level.set_cellv(vector, 0) func _generate_data() -> Array: var rng := RandomNumberGenerator.new() rng.randomize() var data := {} var rooms := [] for r in range(rooms_max): var room := _get_random_room(rng) if _intersects(rooms, room): continue _add_room(rng, data, rooms, room) if rooms.size() > 1: var room_previous: Rect2 = rooms[-2] _add_connection(rng, data, room_previous, room) return data.keys() func _get_random_room(rng: RandomNumberGenerator) -> Rect2: var width := rng.randi_range(rooms_size.x, rooms_size.y) var height := rng.randi_range(rooms_size.x, rooms_size.y) var x := rng.randi_range(0, level_size.x - width - 1) var y := rng.randi_range(0, level_size.y - height - 1) return Rect2(x, y, width, height) func _add_room(rng: RandomNumberGenerator, data: Dictionary, rooms: Array, room: Rect2) -> void: rooms.push_back(room) if rng.randi_range(0, 1) == 0: for x in range(room.position.x, room.end.x): for y in range(room.position.y, room.end.y): data[Vector2(x, y)] = null else: var unit := FACTOR * room.size var order := [ room.grow_individual(-unit.x, 0, -unit.x, unit.y - room.size.y), room.grow_individual(unit.x - room.size.x, -unit.y, 0, -unit.y), room.grow_individual(-unit.x, unit.y - room.size.y, -unit.x, 0), room.grow_individual(0, -unit.y, unit.x - room.size.x, -unit.y) ] var poly := [] for index in range(order.size()): var rect: Rect2 = order[index] var is_even := index % 2 == 0 var poly_partial := [] for r in range(rng.randi_range(1, 2)): poly_partial.push_back(Vector2( rng.randf_range(rect.position.x, rect.end.x), rng.randf_range(rect.position.y, rect.end.y) )) poly_partial.sort_custom(self, "_lessv_x" if is_even else "_lessv_y") if index > 1: poly_partial.invert() poly += poly_partial for x in range(room.position.x, room.end.x): for y in range(room.position.y, room.end.y): var point := Vector2(x, y) if Geometry.is_point_in_polygon(point, poly): data[point] = null func _add_connection(rng: RandomNumberGenerator, data: Dictionary, room1: Rect2, room2: Rect2) -> void: var room_center1 := (room1.position + room1.end) / 2 var room_center2 := (room2.position + room2.end) / 2 if rng.randi_range(0, 1) == 0: _add_corridor(data, room_center1.x, room_center2.x, room_center1.y, Vector2.AXIS_X) _add_corridor(data, room_center1.y, room_center2.y, room_center2.x, Vector2.AXIS_Y) else: _add_corridor(data, room_center1.y, room_center2.y, room_center1.x, Vector2.AXIS_Y) _add_corridor(data, room_center1.x, room_center2.x, room_center2.y, Vector2.AXIS_X) func _add_corridor(data: Dictionary, start: int, end: int, constant: int, axis: int) -> void: for t in range(min(start, end), max(start, end) + 1): var point := Vector2.ZERO match axis: Vector2.AXIS_X: point = Vector2(t, constant) Vector2.AXIS_Y: point = Vector2(constant, t) data[point] = null func _intersects(rooms: Array, room: Rect2) -> bool: var out := false for room_other in rooms: if room.intersects(room_other): out = true break return out func _lessv_x(v1: Vector2, v2: Vector2) -> bool: return v1.x < v2.x func _lessv_y(v1: Vector2, v2: Vector2) -> bool: return v1.y < v2.y