1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
|
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 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_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 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]))
array[Mesh.ARRAY_NORMAL].append(normal)
array[Mesh.ARRAY_NORMAL].append(normal)
array[Mesh.ARRAY_NORMAL].append(normal)
array[Mesh.ARRAY_NORMAL].append(normal)
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)
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)
|