biome generator
This commit is contained in:
5
core/biome_generator/biome.gd
Normal file
5
core/biome_generator/biome.gd
Normal file
@@ -0,0 +1,5 @@
|
||||
extends Resource
|
||||
class_name Biome
|
||||
|
||||
@export var name: String = "New Biome"
|
||||
@export var available_chunks: Array[PackedScene]
|
||||
1
core/biome_generator/biome.gd.uid
Normal file
1
core/biome_generator/biome.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://wv6kcqkibium
|
||||
302
core/biome_generator/biome_generator.gd
Normal file
302
core/biome_generator/biome_generator.gd
Normal 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)
|
||||
1
core/biome_generator/biome_generator.gd.uid
Normal file
1
core/biome_generator/biome_generator.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ct2k25kslaxe5
|
||||
6
core/biome_generator/biome_generator.tscn
Normal file
6
core/biome_generator/biome_generator.tscn
Normal 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")
|
||||
46
core/biome_generator/chunk_info.gd
Normal file
46
core/biome_generator/chunk_info.gd
Normal 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]
|
||||
}
|
||||
}
|
||||
1
core/biome_generator/chunk_info.gd.uid
Normal file
1
core/biome_generator/chunk_info.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dg2h4kbqe8j3m
|
||||
Reference in New Issue
Block a user