biome generator

This commit is contained in:
2026-04-17 15:31:28 +02:00
parent 060bccd96a
commit b77a6263aa
91 changed files with 4111 additions and 78 deletions

View File

@@ -0,0 +1,5 @@
extends Resource
class_name Biome
@export var name: String = "New Biome"
@export var available_chunks: Array[PackedScene]

View File

@@ -0,0 +1 @@
uid://wv6kcqkibium

View File

@@ -0,0 +1,302 @@
extends Node3D
@export_group("Rails")
@export var rail_path: Path3D
@export_group("Biomes")
@export var biome_list: Array[Biome]
@export_group("Grid and Area")
@export var chunk_size: float = 20.0
@export var eye_line: int = 3
@export var district_scale: float = 0.05
var board: Dictionary = {}
var last_pos_train: Vector2i = Vector2i(999999, 999999)
var noise_generator: FastNoiseLite
var altitude_generator: FastNoiseLite
var manual_biome: Biome = null
func _ready() -> void:
if biome_list.is_empty() or rail_path == null: return
noise_generator = FastNoiseLite.new()
noise_generator.noise_type = FastNoiseLite.TYPE_PERLIN
noise_generator.seed = randi()
noise_generator.frequency = district_scale
altitude_generator = FastNoiseLite.new()
altitude_generator.noise_type = FastNoiseLite.TYPE_PERLIN
altitude_generator.seed = randi()
altitude_generator.frequency = district_scale * 0.5
_update_set_pieces()
func _update_set_pieces() -> void:
var set_pieces = get_tree().get_nodes_in_group("set_pieces")
print("🔍 Found ", set_pieces.size(), " in 'set_pieces' group.")
for sp in set_pieces:
if not "exclusive_biome" in sp or sp.exclusive_biome == "":
print("⚠️ Set Piece '", sp.name, "' not have exclusive biome -> always visible.")
continue
var local_biome = ""
if manual_biome != null:
local_biome = manual_biome.nome
else:
var grid_x = roundi(sp.global_position.x / chunk_size)
var grid_z = roundi(sp.global_position.z / chunk_size)
local_biome = _get_procedural_biome_name(noise_generator.get_noise_2d(grid_x, grid_z))
print("👉 Analyze: '", sp.name, "' | Need: [", sp.exclusive_biome, "] | Current biome is: [", local_biome, "]")
if local_biome == sp.exclusive_biome:
sp.show()
sp.process_mode = Node.PROCESS_MODE_INHERIT
print(" ✅ Temple is visible.")
else:
sp.hide()
sp.process_mode = Node.PROCESS_MODE_DISABLED
print(" ❌ Temple is a invisible ghost.")
func _destroy_and_regenrate_world() -> void:
#1. Turn off old temples
_update_set_pieces()
#2. Destroy all models
for pos in board.keys():
var cella = board[pos]
if cella["tipo"] == "bioma":
if cella.has("nodo") and is_instance_valid(cella["nodo"]):
cella["nodo"].queue_free()
#3. Clean board
board.clear()
#4. Create train
last_pos_train = Vector2i(999999, 999999)
_train_radar()
last_pos_train = Vector2i(999999, 999999)
_train_radar()
func _physics_process(_delta: float) -> void:
if rail_path == null or rail_path.train_instance == null: return
var train_pos = rail_path.train_instance.global_position
var grid_x = roundi(train_pos.x / chunk_size)
var grid_z = roundi(train_pos.z / chunk_size)
var current_pos = Vector2i(grid_x, grid_z)
if current_pos != last_pos_train:
last_pos_train = current_pos
_generate_pieces_around_train(current_pos)
_clean_far_chunks(current_pos)
_train_radar()
func _generate_pieces_around_train(centro: Vector2i) -> void:
for x in range(-eye_line, eye_line + 1):
for z in range(-eye_line, eye_line + 1):
var grid_pos = centro + Vector2i(x, z)
if not board.has(grid_pos):
var ce_obstacle = _register_cell_with_laser(grid_pos)
if not ce_obstacle:
_add_compatible_biome(grid_pos)
func _choose_catalogue_by_cell(grid_pos: Vector2i) -> Array[PackedScene]:
if manual_biome != null:
return manual_biome.available_chunks
var value = noise_generator.get_noise_2d(grid_pos.x, grid_pos.y)
var normalized_value = (value + 1.0) / 2.0
var indice = clamp(int(normalized_value * biome_list.size()), 0, biome_list.size() - 1)
return biome_list[indice].available_chunks
func _get_procedural_biome_name(value: float) -> String:
if manual_biome != null:
return manual_biome.name
var normalized_value = (value + 1.0) / 2.0
var index = clamp(int(normalized_value * biome_list.size()), 0, biome_list.size() - 1)
return biome_list[index].name
func _register_cell_with_laser(grid_pos: Vector2i) -> bool:
if board.has(grid_pos): return true
var world_pos = Vector3(grid_pos.x * chunk_size, 0, grid_pos.y * chunk_size)
var space_state = get_world_3d().direct_space_state
var ray_origin = Vector3(world_pos.x, 50.0, world_pos.z)
var ray_end = Vector3(world_pos.x, -10.0, world_pos.z)
var query = PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
var result = space_state.intersect_ray(query)
if result.size() > 0:
var collider = result["collider"]
var current_node = collider
var exit_found = {"north": false, "est": false, "south": false, "west": false}
var height_found = {"north": 0, "est": 0, "south": 0, "west": 0}
while current_node != null and current_node != get_tree().root:
if current_node.has_method("get_rotated_data"):
var rotation_steps = roundi(rad_to_deg(current_node.global_rotation.y) / -90.0)
var data = current_node.get_rotated_data(rotation_steps)
exit_found = data["connections"]
height_found = data["heights"]
break
current_node = current_node.get_parent()
board[grid_pos] = {"type": "obstacle", "exit": exit_found, "heights": height_found}
return true
return false
func _add_compatible_biome(grid_pos: Vector2i) -> void:
var req_conn_north = _needed_connections(grid_pos + Vector2i(0, -1), "south")
var req_conn_est = _needed_connections(grid_pos + Vector2i(1, 0), "west")
var req_conn_south = _needed_connections(grid_pos + Vector2i(0, 1), "north")
var req_conn_west = _needed_connections(grid_pos + Vector2i(-1, 0), "est")
var req_height_north = _needed_heights(grid_pos + Vector2i(0, -1), "south")
var req_height_est = _needed_heights(grid_pos + Vector2i(1, 0), "west")
var req_height_south = _needed_heights(grid_pos + Vector2i(0, 1), "north")
var req_height_west = _needed_heights(grid_pos + Vector2i(-1, 0), "est")
var height_noise = altitude_generator.get_noise_2d(grid_pos.x, grid_pos.y)
var height_target = clamp(roundi((height_noise + 1.0) / 2.0), 0, 1)
var zone_catalogue = _choose_catalogue_by_cell(grid_pos)
var valid_candidates = []
for scene in zone_catalogue:
var test_chunk = scene.instantiate()
if test_chunk.has_method("get_rotated_data"):
for rot in range(4):
var data = test_chunk.get_rotated_data(rot)
var u_conn = data["connections"]
var u_height = data["heights"]
var match_conn_n = (req_conn_north == -1) or ((req_conn_north == 1) == u_conn["north"])
var match_conn_e = (req_conn_est == -1) or ((req_conn_est == 1) == u_conn["est"])
var match_conn_s = (req_conn_south == -1) or ((req_conn_south == 1) == u_conn["south"])
var match_conn_o = (req_conn_west == -1) or ((req_conn_west == 1) == u_conn["west"])
var match_alt_n = (req_height_north == -1) or (req_height_north == u_height["north"])
var match_alt_e = (req_height_est == -1) or (req_height_est == u_height["est"])
var match_alt_s = (req_height_south == -1) or (req_height_south == u_height["south"])
var match_alt_o = (req_height_west == -1) or (req_height_west == u_height["west"])
var diff_n = abs(u_height["north"] - height_target) if req_height_north == -1 else 0
var diff_e = abs(u_height["est"] - height_target) if req_height_est == -1 else 0
var diff_s = abs(u_height["south"] - height_target) if req_height_south == -1 else 0
var diff_o = abs(u_height["west"] - height_target) if req_height_west == -1 else 0
var excessive_jumps = diff_n > 1 or diff_e > 1 or diff_s > 1 or diff_o > 1
if match_conn_n and match_conn_e and match_conn_s and match_conn_o and match_alt_n and match_alt_e and match_alt_s and match_alt_o and not excessive_jumps:
var score = 0
if req_height_north == -1 and u_height["north"] == height_target: score += 1
if req_height_est == -1 and u_height["est"] == height_target: score += 1
if req_height_south == -1 and u_height["south"] == height_target: score += 1
if req_height_west == -1 and u_height["west"] == height_target: score += 1
valid_candidates.append({"scene": scene, "rotation": rot, "data": data, "score": score})
test_chunk.queue_free()
if valid_candidates.size() > 0:
var max_score = -1
for c in valid_candidates:
if c.score > max_score: max_score = c.score
var best_candidate = []
for c in valid_candidates:
if c.score == max_score: best_candidate.append(c)
var choise = best_candidate.pick_random()
var new_chunk = choise.scene.instantiate()
add_child(new_chunk)
new_chunk.position = Vector3(grid_pos.x * chunk_size, 0, grid_pos.y * chunk_size)
new_chunk.rotation.y = choise.rotation * (-PI / 2.0)
board[grid_pos] = {"type": "biome", "exit": choise.data["connections"], "heights": choise.data["heights"], "nodo": new_chunk}
else:
push_warning("⚠️ One piece miss for position " + str(grid_pos) + "!")
var fmt_alt = func(req): return str(req) if req != -1 else "Free"
#var fmt_conn = func(req): return "Yes" if req == 1 else ("No" if req == 0 else "Free")
print("We need missed piece -> North: ", fmt_alt.call(req_height_north), " | Est: ", fmt_alt.call(req_height_est), " | South: ", fmt_alt.call(req_height_south), " | West: ", fmt_alt.call(req_height_west))
var backup = zone_catalogue[0].instantiate()
add_child(backup)
backup.position = Vector3(grid_pos.x * chunk_size, 0, grid_pos.y * chunk_size)
var safe_heights = {
"north": req_height_north if req_height_north != -1 else height_target,
"est": req_height_est if req_height_est != -1 else height_target,
"south": req_height_south if req_height_south != -1 else height_target,
"west": req_height_west if req_height_west != -1 else height_target
}
board[grid_pos] = {"type": "biome", "exit": {"north":false, "est":false, "south":false, "west":false}, "heights": safe_heights, "node": backup}
func _needed_connections(near_pos: Vector2i, side_needed: String) -> int:
if not board.has(near_pos):
_register_cell_with_laser(near_pos)
if board.has(near_pos) and board[near_pos].has("exit"):
return 1 if board[near_pos]["exit"][side_needed] else 0
return -1
func _needed_heights(near_pos: Vector2i, side_needed: String) -> int:
if board.has(near_pos) and board[near_pos].has("heights"):
return board[near_pos]["heights"][side_needed]
return -1
func _train_radar() -> void:
if rail_path == null or rail_path.curve == null or rail_path.train_instance == null: return
if manual_biome != null:
return
var train_pos = rail_path.train_instance.global_position
var grid_x = roundi(train_pos.x / chunk_size)
var grid_z = roundi(train_pos.z / chunk_size)
var current_biome = _get_procedural_biome_name(noise_generator.get_noise_2d(grid_x, grid_z))
var current_progress = rail_path.train_progress
var total_length = rail_path.curve.get_baked_length()
for step in range(1, 31):
var next_progress = wrapf(current_progress + (step * chunk_size), 0.0, total_length)
var next_local_pos = rail_path.curve.sample_baked(next_progress, true)
var next_global_pos = rail_path.to_global(next_local_pos)
var f_x = roundi(next_global_pos.x / chunk_size)
var f_z = roundi(next_global_pos.z / chunk_size)
var next_biome = _get_procedural_biome_name(noise_generator.get_noise_2d(f_x, f_z))
if next_biome != current_biome:
#distanza_cambio = step * chunk_size
#prossimo_bioma = next_biome
break
func _clean_far_chunks(centro_attuale: Vector2i) -> void:
var cells_to_remove = []
var safe_margin = 2
for grid_pos in board.keys():
var cell = board[grid_pos]
if cell["type"] == "obstacle": continue
var dist_x = abs(grid_pos.x - centro_attuale.x)
var dist_z = abs(grid_pos.y - centro_attuale.y)
if dist_x > eye_line + safe_margin or dist_z > eye_line + safe_margin:
if cell.has("node") and is_instance_valid(cell["node"]):
cell["node"].queue_free()
cells_to_remove.append(grid_pos)
for pos in cells_to_remove:
board.erase(pos)

View File

@@ -0,0 +1 @@
uid://ct2k25kslaxe5

View File

@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://ujv2f1l4d2ps"]
[ext_resource type="Script" uid="uid://ct2k25kslaxe5" path="res://core/biome_generator/biome_generator.gd" id="1_c7ilo"]
[node name="biome_generator" type="Node3D" unique_id=1861369341]
script = ExtResource("1_c7ilo")

View File

@@ -0,0 +1,46 @@
extends Node3D
class_name ChunkInfo
@export_enum("Biome", "straight_track", "curved_track") var chunk_type: int = 0
@export_group("Set Piece Rules (Unique pieces)")
@export var exclusive_biome: String = "" # Es: "Forest"
@export_group("Base exit (no roation)")
@export var north: bool = false
@export var est: bool = false
@export var south: bool = false
@export var west: bool = false
@export_group("Maring heights (level 0, 1, 2)")
@export var height_north: int = 0
@export var height_est: int = 0
@export var height_south: int = 0
@export var height_west: int = 0
func _ready() -> void:
if exclusive_biome != "":
add_to_group("set_pieces")
func get_rotated_data(steps_90: int) -> Dictionary:
var original_exit = [north, est, south, west]
var original_height = [height_north, height_est, height_south, height_west]
var calculated_exit = []
var calculated_height = []
for i in range(4):
var index = (i - steps_90 + 4) % 4
calculated_exit.append(original_exit[index])
calculated_height.append(original_height[index])
return {
"connections": {
"north": calculated_exit[0], "est": calculated_exit[1],
"south": calculated_exit[2], "west": calculated_exit[3]
},
"heights": {
"north": calculated_height[0], "est": calculated_height[1],
"south": calculated_height[2], "west": calculated_height[3]
}
}

View File

@@ -0,0 +1 @@
uid://dg2h4kbqe8j3m