程序洞穴--二进制空间划分#

procedural_caves_bsp.py#
1"""
2This example procedurally develops a random cave based on
3Binary Space Partitioning (BSP)
4
5For more information, see:
6https://roguebasin.roguelikedevelopment.org/index.php?title=Basic_BSP_Dungeon_generation
7https://github.com/DanaL/RLDungeonGenerator
8
9If Python and Arcade are installed, this example can be run from the command line with:
10python -m arcade.examples.procedural_caves_bsp
11"""
12
13import random
14import arcade
15import timeit
16import math
17
18# Sprite scaling. Make this larger, like 0.5 to zoom in and add
19# 'mystery' to what you can see. Make it smaller, like 0.1 to see
20# more of the map.
21WALL_SPRITE_SCALING = 0.5
22PLAYER_SPRITE_SCALING = 0.25
23
24WALL_SPRITE_SIZE = int(128 * WALL_SPRITE_SCALING)
25
26# How big the grid is
27GRID_WIDTH = 100
28GRID_HEIGHT = 100
29
30AREA_WIDTH = GRID_WIDTH * WALL_SPRITE_SIZE
31AREA_HEIGHT = GRID_HEIGHT * WALL_SPRITE_SIZE
32
33# How fast the player moves
34MOVEMENT_SPEED = 5
35
36# How close the player can get to the edge before we scroll.
37VIEWPORT_MARGIN = 300
38
39# How big the window is
40WINDOW_WIDTH = 800
41WINDOW_HEIGHT = 600
42WINDOW_TITLE = "Procedural Caves BSP Example"
43
44MERGE_SPRITES = False
45
46
47class Room:
48 """ A room """
49 def __init__(self, r, c, h, w):
50 self.row = r
51 self.col = c
52 self.height = h
53 self.width = w
54
55
56class RLDungeonGenerator:
57 """ Generate the dungeon """
58 def __init__(self, w, h):
59 """ Create the board """
60 self.MAX = 15 # Cutoff for when we want to stop dividing sections
61 self.width = w
62 self.height = h
63 self.leaves = []
64 self.dungeon = []
65 self.rooms = []
66
67 for h in range(self.height):
68 row = []
69 for w in range(self.width):
70 row.append('#')
71
72 self.dungeon.append(row)
73
74 def random_split(self, min_row, min_col, max_row, max_col):
75 # We want to keep splitting until the sections get down to the threshold
76 seg_height = max_row - min_row
77 seg_width = max_col - min_col
78
79 if seg_height < self.MAX and seg_width < self.MAX:
80 self.leaves.append((min_row, min_col, max_row, max_col))
81 elif seg_height < self.MAX <= seg_width:
82 self.split_on_vertical(min_row, min_col, max_row, max_col)
83 elif seg_height >= self.MAX > seg_width:
84 self.split_on_horizontal(min_row, min_col, max_row, max_col)
85 else:
86 if random.random() < 0.5:
87 self.split_on_horizontal(min_row, min_col, max_row, max_col)
88 else:
89 self.split_on_vertical(min_row, min_col, max_row, max_col)
90
91 def split_on_horizontal(self, min_row, min_col, max_row, max_col):
92 split = (min_row + max_row) // 2 + random.choice((-2, -1, 0, 1, 2))
93 self.random_split(min_row, min_col, split, max_col)
94 self.random_split(split + 1, min_col, max_row, max_col)
95
96 def split_on_vertical(self, min_row, min_col, max_row, max_col):
97 split = (min_col + max_col) // 2 + random.choice((-2, -1, 0, 1, 2))
98 self.random_split(min_row, min_col, max_row, split)
99 self.random_split(min_row, split + 1, max_row, max_col)
100
101 def carve_rooms(self):
102 for leaf in self.leaves:
103 # We don't want to fill in every possible room or the
104 # dungeon looks too uniform
105 if random.random() > 0.80:
106 continue
107 section_width = leaf[3] - leaf[1]
108 section_height = leaf[2] - leaf[0]
109
110 # The actual room's height and width will be 60-100% of the
111 # available section.
112 room_width = round(random.randrange(60, 100) / 100 * section_width)
113 room_height = round(random.randrange(60, 100) / 100 * section_height)
114
115 # If the room doesn't occupy the entire section we are carving it from,
116 # 'jiggle' it a bit in the square
117 if section_height > room_height:
118 room_start_row = leaf[0] + random.randrange(section_height - room_height)
119 else:
120 room_start_row = leaf[0]
121
122 if section_width > room_width:
123 room_start_col = leaf[1] + random.randrange(section_width - room_width)
124 else:
125 room_start_col = leaf[1]
126
127 self.rooms.append(Room(room_start_row, room_start_col, room_height, room_width))
128 for r in range(room_start_row, room_start_row + room_height):
129 for c in range(room_start_col, room_start_col + room_width):
130 self.dungeon[r][c] = '.'
131
132 @staticmethod
133 def are_rooms_adjacent(room1, room2):
134 """ See if two rooms are next to each other. """
135 adj_rows = []
136 adj_cols = []
137 for r in range(room1.row, room1.row + room1.height):
138 if room2.row <= r < room2.row + room2.height:
139 adj_rows.append(r)
140
141 for c in range(room1.col, room1.col + room1.width):
142 if room2.col <= c < room2.col + room2.width:
143 adj_cols.append(c)
144
145 return adj_rows, adj_cols
146
147 @staticmethod
148 def distance_between_rooms(room1, room2):
149 """ Get the distance between two rooms """
150 centre1 = (room1.row + room1.height // 2, room1.col + room1.width // 2)
151 centre2 = (room2.row + room2.height // 2, room2.col + room2.width // 2)
152
153 return math.sqrt((centre1[0] - centre2[0]) ** 2 + (centre1[1] - centre2[1]) ** 2)
154
155 def carve_corridor_between_rooms(self, room1, room2):
156 """ Make a corridor between rooms """
157 if room2[2] == 'rows':
158 row = random.choice(room2[1])
159 # Figure out which room is to the left of the other
160 if room1.col + room1.width < room2[0].col:
161 start_col = room1.col + room1.width
162 end_col = room2[0].col
163 else:
164 start_col = room2[0].col + room2[0].width
165 end_col = room1.col
166 for c in range(start_col, end_col):
167 self.dungeon[row][c] = '.'
168
169 if end_col - start_col >= 4:
170 self.dungeon[row][start_col] = '+'
171 self.dungeon[row][end_col - 1] = '+'
172 elif start_col == end_col - 1:
173 self.dungeon[row][start_col] = '+'
174 else:
175 col = random.choice(room2[1])
176 # Figure out which room is above the other
177 if room1.row + room1.height < room2[0].row:
178 start_row = room1.row + room1.height
179 end_row = room2[0].row
180 else:
181 start_row = room2[0].row + room2[0].height
182 end_row = room1.row
183
184 for r in range(start_row, end_row):
185 self.dungeon[r][col] = '.'
186
187 if end_row - start_row >= 4:
188 self.dungeon[start_row][col] = '+'
189 self.dungeon[end_row - 1][col] = '+'
190 elif start_row == end_row - 1:
191 self.dungeon[start_row][col] = '+'
192
193 def find_closest_unconnect_groups(self, groups, room_dict):
194 """
195 Find two nearby rooms that are in difference groups, draw
196 a corridor between them and merge the groups
197 """
198
199 shortest_distance = 99999
200 start = None
201 start_group = None
202 nearest = None
203
204 for group in groups:
205 for room in group:
206 key = (room.row, room.col)
207 for other in room_dict[key]:
208 if other[0] not in group and other[3] < shortest_distance:
209 shortest_distance = other[3]
210 start = room
211 nearest = other
212 start_group = group
213
214 self.carve_corridor_between_rooms(start, nearest)
215
216 # Merge the groups
217 other_group = None
218 for group in groups:
219 if nearest[0] in group:
220 other_group = group
221 break
222
223 start_group += other_group
224 groups.remove(other_group)
225
226 def connect_rooms(self):
227 """
228 Build a dictionary containing an entry for each room. Each bucket will
229 hold a list of the adjacent rooms, weather they are adjacent along rows or
230 columns and the distance between them.
231
232 Also build the initial groups (which start of as a list of individual rooms)
233 """
234 groups = []
235 room_dict = {}
236 for room in self.rooms:
237 key = (room.row, room.col)
238 room_dict[key] = []
239 for other in self.rooms:
240 other_key = (other.row, other.col)
241 if key == other_key:
242 continue
243 adj = self.are_rooms_adjacent(room, other)
244 if len(adj[0]) > 0:
245 room_dict[key].append((other, adj[0], 'rows', self.distance_between_rooms(room, other)))
246 elif len(adj[1]) > 0:
247 room_dict[key].append((other, adj[1], 'cols', self.distance_between_rooms(room, other)))
248
249 groups.append([room])
250
251 while len(groups) > 1:
252 self.find_closest_unconnect_groups(groups, room_dict)
253
254 def generate_map(self):
255 """ Make the map """
256 self.random_split(1, 1, self.height - 1, self.width - 1)
257 self.carve_rooms()
258 self.connect_rooms()
259
260
261class MyGame(arcade.Window):
262 """
263 Main application class.
264 """
265
266 def __init__(self, width, height, title):
267 super().__init__(width, height, title)
268
269 self.grid = None
270 self.wall_list = None
271 self.player_list = None
272 self.player_sprite = None
273 self.cam = None
274 self.physics_engine = None
275
276 self.processing_time = 0
277 self.draw_time = 0
278
279 self.background_color = arcade.color.BLACK
280
281 def setup(self):
282 """ Set up the game """
283 self.wall_list = arcade.SpriteList(use_spatial_hash=True)
284 self.player_list = arcade.SpriteList()
285
286 # Create cave system using a 2D grid
287 dg = RLDungeonGenerator(GRID_WIDTH, GRID_HEIGHT)
288 dg.generate_map()
289
290 # Create sprites based on 2D grid
291 texture = arcade.load_texture(":resources:images/tiles/grassCenter.png")
292 if not MERGE_SPRITES:
293 # This is the simple-to-understand method. Each grid location
294 # is a sprite.
295 for row in range(dg.height):
296 for column in range(dg.width):
297 value = dg.dungeon[row][column]
298 if value == '#':
299 wall = arcade.BasicSprite(texture, scale=WALL_SPRITE_SCALING)
300 wall.center_x = column * WALL_SPRITE_SIZE + WALL_SPRITE_SIZE / 2
301 wall.center_y = row * WALL_SPRITE_SIZE + WALL_SPRITE_SIZE / 2
302 self.wall_list.append(wall)
303 else:
304 # This uses new Arcade 1.3.1 features, that allow me to create a
305 # larger sprite with a repeating texture. So if there are multiple
306 # cells in a row with a wall, we merge them into one sprite, with a
307 # repeating texture for each cell. This reduces our sprite count.
308 for row in range(dg.height):
309 column = 0
310 while column < dg.width:
311 while column < dg.width and dg.dungeon[row][column] != '#':
312 column += 1
313 start_column = column
314 while column < dg.width and dg.dungeon[row][column] == '#':
315 column += 1
316 end_column = column - 1
317
318 column_count = end_column - start_column + 1
319 column_mid = (start_column + end_column) / 2
320
321 wall = arcade.BasicSprite(texture, scale=WALL_SPRITE_SCALING)
322 wall.center_x = column_mid * WALL_SPRITE_SIZE + WALL_SPRITE_SIZE / 2
323 wall.center_y = row * WALL_SPRITE_SIZE + WALL_SPRITE_SIZE / 2
324 wall.width = WALL_SPRITE_SIZE * column_count
325 self.wall_list.append(wall)
326
327 # Set up the player
328 self.player_sprite = arcade.Sprite(
329 ":resources:images/animated_characters/female_person/femalePerson_idle.png",
330 scale=PLAYER_SPRITE_SCALING)
331 self.player_list.append(self.player_sprite)
332
333 # Randomly place the player. If we are in a wall, repeat until we aren't.
334 placed = False
335 while not placed:
336
337 # Randomly position
338 self.player_sprite.center_x = random.randrange(AREA_WIDTH)
339 self.player_sprite.center_y = random.randrange(AREA_HEIGHT)
340
341 # Are we in a wall?
342 walls_hit = arcade.check_for_collision_with_list(self.player_sprite, self.wall_list)
343 if len(walls_hit) == 0:
344 # Not in a wall! Success!
345 placed = True
346
347 self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite,
348 self.wall_list)
349
350 self.cam = arcade.camera.Camera2D()
351
352 def on_draw(self):
353 """ Render the screen. """
354
355 # Start timing how long this takes
356 draw_start_time = timeit.default_timer()
357
358 # This command should happen before we start drawing. It will clear
359 # the screen to the background color, and erase what we drew last frame.
360 self.clear()
361
362 # Draw the sprites
363 self.wall_list.draw()
364 self.player_list.draw()
365
366 # Draw info on the screen
367 sprite_count = len(self.wall_list)
368
369 output = f"Sprite Count: {sprite_count}"
370 arcade.draw_text(output,
371 self.cam.left + 20,
372 WINDOW_HEIGHT - 20 + self.cam.bottom,
373 arcade.color.WHITE, 16)
374
375 output = f"Drawing time: {self.draw_time:.3f}"
376 arcade.draw_text(output,
377 self.cam.left + 20,
378 WINDOW_HEIGHT - 40 + self.cam.bottom,
379 arcade.color.WHITE, 16)
380
381 output = f"Processing time: {self.processing_time:.3f}"
382 arcade.draw_text(output,
383 self.cam.left + 20,
384 WINDOW_HEIGHT - 60 + self.cam.bottom,
385 arcade.color.WHITE, 16)
386
387 self.draw_time = timeit.default_timer() - draw_start_time
388
389 def on_key_press(self, key, modifiers):
390 """Called whenever a key is pressed. """
391
392 if key == arcade.key.UP:
393 self.player_sprite.change_y = MOVEMENT_SPEED
394 elif key == arcade.key.DOWN:
395 self.player_sprite.change_y = -MOVEMENT_SPEED
396 elif key == arcade.key.LEFT:
397 self.player_sprite.change_x = -MOVEMENT_SPEED
398 elif key == arcade.key.RIGHT:
399 self.player_sprite.change_x = MOVEMENT_SPEED
400
401 def on_key_release(self, key, modifiers):
402 """Called when the user releases a key. """
403
404 if key == arcade.key.UP or key == arcade.key.DOWN:
405 self.player_sprite.change_y = 0
406 elif key == arcade.key.LEFT or key == arcade.key.RIGHT:
407 self.player_sprite.change_x = 0
408
409 def on_update(self, delta_time):
410 """ Movement and game logic """
411
412 start_time = timeit.default_timer()
413
414 # Move the player
415 self.physics_engine.update()
416
417 # --- Manage Scrolling ---
418
419 # Track if we need to change the viewport
420
421 changed = False
422
423 # Scroll left
424 left_bndry = self.cam.left + VIEWPORT_MARGIN
425 if self.player_sprite.left < left_bndry:
426 self.cam.left -= left_bndry - self.player_sprite.left
427 changed = True
428
429 # Scroll right
430 right_bndry = self.cam.right - VIEWPORT_MARGIN
431 if self.player_sprite.right > right_bndry:
432 self.cam.left += self.player_sprite.right - right_bndry
433 changed = True
434
435 # Scroll up
436 top_bndry = self.cam.top - VIEWPORT_MARGIN
437 if self.player_sprite.top > top_bndry:
438 self.cam.bottom += self.player_sprite.top - top_bndry
439 changed = True
440
441 # Scroll down
442 bottom_bndry = self.cam.bottom + VIEWPORT_MARGIN
443 if self.player_sprite.bottom < bottom_bndry:
444 self.cam.bottom -= bottom_bndry - self.player_sprite.bottom
445 changed = True
446
447 if changed:
448 self.cam.use()
449
450 # Save the time it took to do this.
451 self.processing_time = timeit.default_timer() - start_time
452
453
454def main():
455 """ Main function, start up window and run """
456 game = MyGame(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
457 game.setup()
458 arcade.run()
459
460
461if __name__ == "__main__":
462 main()