1. ver1
照搬 Unity 的做法 https://blog.csdn.net/PriceCheap/article/details/125962352
版本:Godot_v4.0-alpha13_win64
该脚本附加在 GridMap 节点上,节点的 Meshibrary 需要至少一个元素
MapGenerator.gd
class_name MapGenerator
extends GridMap
enum TileType {Empty, Wall}
@export var width : int = 64
@export var height : int = 36
@export var mapSeed : String
@export var useRandomSeed : bool = true
@export_range(0.0,1.0) var threshold : float = 0.5
@export_range(0,20) var smoothLevel : int = 4
@export var wallThresholdSize : int = 50
@export var roomThresholdSize : int = 50
@export var passageWidth : int = 4
@export var borderSize : int = 1
var map : Array
var upDownLeftRight : Array = [Vector2i(0,1), Vector2i(0,-1), Vector2i(-1,0), Vector2i(1,0)]
var survivingRooms : Array
const Room = preload("Room.gd")
func _ready():
GenerateMap()
SetGridMap()
func _input(event):
if event is InputEventMouseButton:
match event.button_index:
MOUSE_BUTTON_LEFT:
survivingRooms.clear()
GenerateMap()
clear()
SetGridMap()
func GenerateMap():
for x in range(width):
map.append([])
for y in range(height):
map[x].append(TileType.Empty)
RandomFillMap()
for i in range(smoothLevel):
SmoothMap()
ProcessMap()
ConnectAllRoomsToMainRoom(survivingRooms)
func SetGridMap():
for x in range(width):
for y in range(height):
if map[x][y] == TileType.Empty:
set_cell_item(Vector3i(x,0,y),0,0)
func RandomFillMap():
if useRandomSeed:
mapSeed = Time.get_datetime_string_from_system()
seed(mapSeed.hash())
for x in range(width):
for y in range(height):
if x == 0 || x == width - 1 || y == 0 || y == height - 1:
map[x][y] = TileType.Wall
else:
map[x][y] = TileType.Wall if randf_range(0,1) < threshold else TileType.Empty
func SmoothMap():
for x in range(width):
for y in range(height):
var neighbourWallTiles = GetSurroundingWallCount(x, y)
if neighbourWallTiles > 4:
map[x][y] = TileType.Wall
elif neighbourWallTiles < 4:
map[x][y] = TileType.Empty
func GetSurroundingWallCount(x, y):
var wallCount = 0
for nx in [x-1, x+1]:
for ny in [y-1, y+1]:
if nx >= 0 && nx < width && ny >= 0 && ny < height:
wallCount += 1 if map[x][y] == TileType.Wall else 0
else:
wallCount += 1
return wallCount
func ProcessMap():
var currentIndex : int = 0
var maxIndex : int = 0
var maxSize : int = 0
var wallRegions = GetRegions(TileType.Wall)
for wallRegion in wallRegions:
if wallRegion.size() < wallThresholdSize:
for tile in wallRegion:
map[tile.x][tile.y] = TileType.Empty
var roomRegions = GetRegions(TileType.Empty)
for roomRegion in roomRegions:
if roomRegion.size() < roomThresholdSize:
for tile in roomRegion:
map[tile.x][tile.y] = TileType.Wall
else:
var sRoom = Room.new(roomRegion, map)
survivingRooms.append(sRoom)
if maxSize < roomRegion.size():
maxSize = roomRegion.size()
maxIndex = currentIndex
currentIndex += 1
if survivingRooms.size() == 0:
print_debug("No Survived Rooms Here!!")
else:
survivingRooms[maxIndex].isMainRoom = true
survivingRooms[maxIndex].isAccessibleFromMainRoom = true
func GetRegions(tileType):
var regions : Array
var mapFlags = BitMap.new()
mapFlags.create(Vector2(width, height))
for x in range(width):
for y in range(height):
if mapFlags.get_bit(Vector2(x, y)) == false && map[x][y] == tileType:
regions.append(GetRegionTiles(x, y, tileType, mapFlags))
return regions
func GetRegionTiles(startX, startY, tileType, mapFlags):
var tiles : Array
var quene1 : Array
var quene2 : Array
quene1.append(Vector2i(startX, startY))
mapFlags.set_bit(Vector2(startX, startY), true)
while quene1.size() > 0:
var tile = quene1.pop_back()
tiles.append(tile)
for i in range(4):
var x = tile.x + upDownLeftRight[i].x;
var y = tile.y + upDownLeftRight[i].y;
if IsInMapRange(x, y) && mapFlags.get_bit(Vector2(x, y)) == false && map[x][y] == tileType:
mapFlags.set_bit(Vector2(x, y), true)
quene2.append(Vector2i(x, y))
if quene1.size() == 0:
quene1 = quene2
quene2 = Array()
return tiles
func ConnectAllRoomsToMainRoom(allRooms):
for room in allRooms:
ConnectToClosestRoom(room, allRooms)
var count = 0
for room in allRooms:
if room.isAccessibleFromMainRoom:
count += 1
if count != allRooms.size():
ConnectAllRoomsToMainRoom(allRooms)
func ConnectToClosestRoom(roomA, roomListB):
var bestDistance : int = 9223372036854775807
var bestTileA : Vector2i
var bestTileB : Vector2i
var bestRoomB
var hasChecked = false
for roomB in roomListB:
if roomA == roomB || roomA.IsConnected(roomB):
continue
for tileA in roomA.edgeTiles:
for tileB in roomB.edgeTiles:
var distanceBetweenRooms = (tileA - tileB).length_squared()
if distanceBetweenRooms < bestDistance:
bestDistance = distanceBetweenRooms
bestTileA = tileA
bestTileB = tileB
bestRoomB = roomB
if bestRoomB != null:
CreatePassage(roomA, bestRoomB, bestTileA, bestTileB)
func CreatePassage(roomA, roomB, tileA, tileB):
roomA.ConnectRooms(roomB)
var line = GetLine(tileA, tileB)
for coord in line:
DrawCircle(coord, passageWidth)
func GetLine(from, to):
var line : Array
var x = from.x
var y = from.y
var dx = to.x - from.x
var dy = to.y - from.y
var inverted = false
var step = sign(dx)
var gradientStep = sign(dy)
var longest = abs(dx)
var shortest = abs(dy)
if longest < shortest:
inverted = true
longest = abs(dy)
shortest = abs(dx)
step = sign(dy)
gradientStep = sign(dx)
var gradientAccumulation = longest / 2
for i in range(longest):
line.append(Vector2i(x, y))
if inverted:
y += step
else:
x += step
gradientAccumulation += shortest
if gradientAccumulation >= longest:
if inverted:
x += gradientStep
else:
y += gradientStep
gradientAccumulation -= longest
return line
func DrawCircle(c, r):
for x in range(-r, r):
for y in range(-r, r):
if x * x + y * y <= r * r:
var drawX = c.x + x
var drawY = c.y + y
if IsInMapRange(drawX, drawY):
map[drawX][drawY] = TileType.Empty
func IsInMapRange(x, y):
return x >= 0 && x < width && y >= 0 && y < height
该脚本不用附加到节点上
Room.gd
class_name Room
extends Object
enum TileType {Empty, Wall}
var tiles : Array = []
var edgeTiles : Array = []
var connectedRooms : Array = []
var isAccessibleFromMainRoom : bool = false
var isMainRoom : bool = false
var upDownLeftRight : Array = [Vector2i(0,1), Vector2i(0,-1), Vector2i(-1,0), Vector2i(1,0)]
func _init(roomTiles, map):
tiles = roomTiles
UpdateEdgeTiles(map)
func UpdateEdgeTiles(map):
edgeTiles.clear()
for tile in tiles:
for i in range(4):
var x = tile.x + upDownLeftRight[i].x
var y = tile.y + upDownLeftRight[i].y
if map[x][y] == TileType.Wall && !edgeTiles.has(tile):
edgeTiles.append(tile)
func MarkAccessibleFromMainRoom():
if !isAccessibleFromMainRoom:
isAccessibleFromMainRoom = true
for connectedRoom in connectedRooms:
connectedRoom.MarkAccessibleFromMainRoom()
func ConnectRooms(roomB):
if isAccessibleFromMainRoom:
roomB.MarkAccessibleFromMainRoom()
elif roomB.isAccessibleFromMainRoom:
MarkAccessibleFromMainRoom()
connectedRooms.append(roomB)
roomB.connectedRooms.append(self)
func IsConnected(otherRoom):
if connectedRooms.find(otherRoom) == -1:
return false
else:
return true
本来 TileType 和 upDownLeftRight 应该做成全局变量的 但是 4.0-alpha 的 autoload 设置有点问题,我就没有用
2. ver2
具体来说,问题在于,perload 一个引用了 autoload 中的脚本会报错 https://github.com/godotengine/godot/issues/58551 因此我的地图生成器在 preload 房间脚本的时候,房间脚本里面如果用到了自动加载的 GEnum,就不行 这是一个已知的 bug
如果没有这个 bug 的话,可以写得更好看一点
genum.gd
extends Object
enum TileType {Empty, Wall}
var Vector2_Dir : Array = [Vector2i(0,1), Vector2i(0,-1), Vector2i(-1,0), Vector2i(1,0)]
room_1.gd
extends Object
var tiles : Array = []
var edgeTiles : Array = []
var connectedRooms : Array = []
var isAccessibleFromMainRoom : bool = false
var isMainRoom : bool = false
func _init(roomTiles, map):
tiles = roomTiles
UpdateEdgeTiles(map)
func UpdateEdgeTiles(map):
edgeTiles.clear()
for tile in tiles:
for i in range(4):
var x = tile.x + GEnum.Vector2_Dir[i].x
var y = tile.y + GEnum.Vector2_Dir[i].y
if map[x][y] == GEnum.TileType.Wall && !edgeTiles.has(tile):
edgeTiles.append(tile)
func MarkAccessibleFromMainRoom():
if !isAccessibleFromMainRoom:
isAccessibleFromMainRoom = true
for connectedRoom in connectedRooms:
connectedRoom.MarkAccessibleFromMainRoom()
func ConnectRooms(roomB):
if isAccessibleFromMainRoom:
roomB.MarkAccessibleFromMainRoom()
elif roomB.isAccessibleFromMainRoom:
MarkAccessibleFromMainRoom()
connectedRooms.append(roomB)
roomB.connectedRooms.append(self)
func IsConnected(otherRoom):
if connectedRooms.find(otherRoom) == -1:
return false
else:
return true
map_generator_base
class_name MapGeneratorBase
extends GridMap
@export var width : int = 64
@export var height : int = 64
@export var mapSeed : String
@export var useRandomSeed : bool = true
var roomList : Array
func get_line(from, to):
var line : Array
var x = from.x
var y = from.y
var dx = to.x - from.x
var dy = to.y - from.y
var inverted = false
var step = sign(dx)
var gradientStep = sign(dy)
var longest = abs(dx)
var shortest = abs(dy)
if longest < shortest:
inverted = true
longest = abs(dy)
shortest = abs(dx)
step = sign(dy)
gradientStep = sign(dx)
var acc = 0
for i in range(longest):
line.append(Vector2i(x, y))
if inverted:
y += step
else:
x += step
acc += 2*shortest
if acc >= longest:
if inverted:
x += gradientStep
else:
y += gradientStep
acc -= 2*longest
return line
func is_in_map_range(x, y):
return x >= 0 && x < width && y >= 0 && y < height
map_generator_1.gd
extends MapGeneratorBase
const Room = preload("room_1.gd")
@export_range(0.0,1.0) var fillThreshold : float = 0.55
@export_range(0,20) var smoothLevel : int = 4
@export var wallThresholdSize : int = 50
@export var roomThresholdSize : int = 50
@export var passageWidth : int = 4
@export var borderSize : int = 1
var map : Array
func _ready():
generate_map()
set_gridmap_by_empty()
func _input(event):
if event is InputEventMouseButton:
match event.button_index:
MOUSE_BUTTON_LEFT:
roomList.clear()
generate_map()
clear()
set_gridmap_by_empty()
func generate_map():
for x in range(width):
map.append([])
for y in range(height):
map[x].append(GEnum.TileType.Empty)
random_fill_map()
for i in range(smoothLevel):
smooth_map()
eliminate_small_hole_and_wall()
connect_all_rooms_to_mainroom(roomList)
func set_gridmap_by_empty():
for x in range(width):
for y in range(height):
if map[x][y] == GEnum.TileType.Empty:
set_cell_item(Vector3i(x,0,y),0,0)
func random_fill_map():
if useRandomSeed:
mapSeed = Time.get_datetime_string_from_system()
seed(mapSeed.hash())
for x in range(width):
for y in range(height):
if x == 0 || x == width - 1 || y == 0 || y == height - 1:
map[x][y] = GEnum.TileType.Wall
else:
map[x][y] = GEnum.TileType.Wall if randf_range(0,1) < fillThreshold else GEnum.TileType.Empty
func smooth_map():
for x in range(width):
for y in range(height):
var neighbourWallTiles = get_surround_wall_count(x, y)
if neighbourWallTiles > 4:
map[x][y] = GEnum.TileType.Wall
elif neighbourWallTiles < 4:
map[x][y] = GEnum.TileType.Empty
func get_surround_wall_count(x, y):
var wallCount = 0
for nx in [x-1, x+1]:
for ny in [y-1, y+1]:
if nx >= 0 && nx < width && ny >= 0 && ny < height:
wallCount += 1 if map[x][y] == GEnum.TileType.Wall else 0
else:
wallCount += 1
return wallCount
func eliminate_small_hole_and_wall():
var currentIndex : int = 0
var maxIndex : int = 0
var maxSize : int = 0
var wallRegions = get_regions(GEnum.TileType.Wall)
for wallRegion in wallRegions:
if wallRegion.size() < wallThresholdSize:
for tile in wallRegion:
map[tile.x][tile.y] = GEnum.TileType.Empty
var roomRegions = get_regions(GEnum.TileType.Empty)
for roomRegion in roomRegions:
if roomRegion.size() < roomThresholdSize:
for tile in roomRegion:
map[tile.x][tile.y] = GEnum.TileType.Wall
else:
var sRoom = Room.new(roomRegion, map)
roomList.append(sRoom)
if maxSize < roomRegion.size():
maxSize = roomRegion.size()
maxIndex = currentIndex
currentIndex += 1
if roomList.size() == 0:
print_debug("No Survived Rooms Here!!")
else:
roomList[maxIndex].isMainRoom = true
roomList[maxIndex].isAccessibleFromMainRoom = true
func get_regions(tileType):
var regions : Array
var mapFlags = BitMap.new()
mapFlags.create(Vector2(width, height))
for x in range(width):
for y in range(height):
if mapFlags.get_bit(Vector2(x, y)) == false && map[x][y] == tileType:
regions.append(get_region_tiles(x, y, tileType, mapFlags))
return regions
func get_region_tiles(startX, startY, tileType, mapFlags):
var tiles : Array
var quene1 : Array
var quene2 : Array
quene1.append(Vector2i(startX, startY))
mapFlags.set_bit(Vector2(startX, startY), true)
while quene1.size() > 0:
var tile = quene1.pop_back()
tiles.append(tile)
for i in range(4):
var x = tile.x + GEnum.Vector2_Dir[i].x;
var y = tile.y + GEnum.Vector2_Dir[i].y;
if is_in_map_range(x, y) && mapFlags.get_bit(Vector2(x, y)) == false && map[x][y] == tileType:
mapFlags.set_bit(Vector2(x, y), true)
quene2.append(Vector2i(x, y))
if quene1.size() == 0:
quene1 = quene2
quene2 = Array()
return tiles
func connect_all_rooms_to_mainroom(allRooms):
for room in allRooms:
connect_to_closest_room(room, allRooms)
var count = 0
for room in allRooms:
if room.isAccessibleFromMainRoom:
count += 1
if count != allRooms.size():
connect_all_rooms_to_mainroom(allRooms)
func connect_to_closest_room(roomA, roomListB):
var bestDistance : int = 9223372036854775807
var bestTileA : Vector2i
var bestTileB : Vector2i
var bestRoomB
var hasChecked = false
for roomB in roomListB:
if roomA == roomB || roomA.IsConnected(roomB):
continue
for tileA in roomA.edgeTiles:
for tileB in roomB.edgeTiles:
var distanceBetweenRooms = (tileA - tileB).length_squared()
if distanceBetweenRooms < bestDistance:
bestDistance = distanceBetweenRooms
bestTileA = tileA
bestTileB = tileB
bestRoomB = roomB
if bestRoomB != null:
create_passage(roomA, bestRoomB, bestTileA, bestTileB)
func create_passage(roomA, roomB, tileA, tileB):
roomA.ConnectRooms(roomB)
var line = get_line(tileA, tileB)
for coord in line:
draw_circle(coord, passageWidth)
func draw_circle(c, r):
for x in range(-r, r):
for y in range(-r, r):
if x * x + y * y <= r * r:
var drawX = c.x + x
var drawY = c.y + y
if is_in_map_range(drawX, drawY):
map[drawX][drawY] = GEnum.TileType.Empty
|