From 8c7f1c3c821d9b1cf0315aeb3f48d8f3858806a9 Mon Sep 17 00:00:00 2001 From: Overside srl Date: Thu, 30 Apr 2026 16:21:08 +0200 Subject: [PATCH] fix stutter --- core/biome_generator/biome_generator.gd | 329 +++++++++++++++++------- core/daynight/environment_manager.gd | 45 +++- 2 files changed, 284 insertions(+), 90 deletions(-) diff --git a/core/biome_generator/biome_generator.gd b/core/biome_generator/biome_generator.gd index ca24a7b..7846d4a 100644 --- a/core/biome_generator/biome_generator.gd +++ b/core/biome_generator/biome_generator.gd @@ -1,5 +1,9 @@ extends Node3D +const CHUNK_GENERATION_STEPS_PER_FRAME: int = 2 +const CHUNK_CLEANUP_STEPS_PER_FRAME: int = 4 +const LAMPPOST_WIRE_STEPS_PER_FRAME: int = 1 + @export_group("Rails") @export var rail_path: Path3D @@ -20,6 +24,12 @@ var last_pos_train: Vector2i = Vector2i(999999, 999999) var noise_generator: FastNoiseLite var altitude_generator: FastNoiseLite var wire_connections: Dictionary = {} +var chunk_candidate_cache: Dictionary = {} +var pending_generation_cells: Array[Vector2i] = [] +var pending_cleanup_cells: Array[Vector2i] = [] +var pending_wire_cells: Array[Vector2i] = [] +var pending_wire_lookup: Dictionary = {} +var pending_radar_update: bool = false var manual_biome: Biome = null @@ -36,6 +46,7 @@ func _ready() -> void: altitude_generator.seed = randi() altitude_generator.frequency = district_scale * 0.5 + _warm_chunk_candidate_cache() _update_set_pieces() func _update_set_pieces() -> void: @@ -47,7 +58,7 @@ func _update_set_pieces() -> void: 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 @@ -55,7 +66,7 @@ func _update_set_pieces() -> void: 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() @@ -67,15 +78,18 @@ func _update_set_pieces() -> void: print(" ❌ Temple is an invisible ghost.") func _destroy_and_regenrate_world() -> void: + _warm_chunk_candidate_cache() + _clear_pending_world_work() + #Turn off old temples _update_set_pieces() #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() + if cella["type"] != "obstacle": + if cella.has("node") and is_instance_valid(cella["node"]): + cella["node"].queue_free() #Clean board and wire connections board.clear() @@ -83,9 +97,20 @@ func _destroy_and_regenrate_world() -> void: #Create train last_pos_train = Vector2i(999999, 999999) - _train_radar() - last_pos_train = Vector2i(999999, 999999) - _train_radar() + if rail_path != null and rail_path.train_instance != null: + var train_pos = rail_path.train_instance.global_position + var current_pos = Vector2i(roundi(train_pos.x / chunk_size), roundi(train_pos.z / chunk_size)) + _refresh_world_work(current_pos) + pending_radar_update = true + +func _process(_delta: float) -> void: + _drain_cleanup_queue() + _drain_generation_queue() + _drain_wire_queue() + + if pending_radar_update: + pending_radar_update = false + _train_radar() func _physics_process(_delta: float) -> void: if rail_path == null or rail_path.train_instance == null: return @@ -97,9 +122,8 @@ func _physics_process(_delta: float) -> void: if current_pos != last_pos_train: last_pos_train = current_pos - _generate_pieces_around_train(current_pos) - _clean_far_chunks(current_pos) - _train_radar() + _refresh_world_work(current_pos) + pending_radar_update = true func collect_all_chunkinfo(root: Node, list: Array[Node]) -> void: if root == null: return @@ -110,11 +134,173 @@ func collect_all_chunkinfo(root: Node, list: Array[Node]) -> void: for child in root.get_children(): collect_all_chunkinfo(child, list) +func _warm_chunk_candidate_cache() -> void: + var unique_scenes: Dictionary = {} + + for biome in biome_list: + if biome == null: + continue + for scene in biome.available_chunks: + if scene == null: + continue + unique_scenes[_get_chunk_scene_cache_key(scene)] = scene + + if manual_biome != null: + for scene in manual_biome.available_chunks: + if scene == null: + continue + unique_scenes[_get_chunk_scene_cache_key(scene)] = scene + + for scene in unique_scenes.values(): + _get_chunk_scene_metadata(scene) + +func _get_chunk_scene_cache_key(scene: PackedScene) -> String: + if scene == null: + return "" + if scene.resource_path != "": + return scene.resource_path + return "scene_%s" % scene.get_instance_id() + +func _get_chunk_scene_metadata(scene: PackedScene) -> Dictionary: + if scene == null: + return {} + + var key = _get_chunk_scene_cache_key(scene) + if chunk_candidate_cache.has(key): + return chunk_candidate_cache[key] + + var preview_chunk = scene.instantiate() + var info_list: Array[Node] = [] + collect_all_chunkinfo(preview_chunk, info_list) + var info_node = info_list[0] if info_list.size() > 0 else null + + if info_node == null or not info_node.has_method("get_rotated_data"): + preview_chunk.queue_free() + chunk_candidate_cache[key] = {} + return {} + + var info_path: NodePath = NodePath(".") + if info_node != preview_chunk: + info_path = preview_chunk.get_path_to(info_node) + + var rotations = [] + for rot in range(4): + rotations.append({ + "rotation": rot, + "data": info_node.get_rotated_data(rot) + }) + + var metadata = { + "info_path": info_path, + "rotations": rotations, + "has_lamppost": "have_lamppost" in info_node and info_node.have_lamppost, + } + preview_chunk.queue_free() + chunk_candidate_cache[key] = metadata + return metadata + +func _get_cached_chunk_info(instance: Node, scene: PackedScene) -> Node: + var metadata = _get_chunk_scene_metadata(scene) + if metadata.is_empty(): + return null + + var info_path: NodePath = metadata["info_path"] + if String(info_path) == ".": + return instance + return instance.get_node_or_null(info_path) + +func _clear_pending_world_work() -> void: + pending_generation_cells.clear() + pending_cleanup_cells.clear() + pending_wire_cells.clear() + pending_wire_lookup.clear() + pending_radar_update = false + +func _refresh_world_work(center: Vector2i) -> void: + _rebuild_generation_queue(center) + _rebuild_cleanup_queue(center) + +func _rebuild_generation_queue(center: Vector2i) -> void: + pending_generation_cells.clear() + + for radius in range(eye_line + 1): + for x in range(-radius, radius + 1): + for z in range(-radius, radius + 1): + if maxi(abs(x), abs(z)) != radius: + continue + + var grid_pos = center + Vector2i(x, z) + if board.has(grid_pos): + continue + pending_generation_cells.append(grid_pos) + +func _rebuild_cleanup_queue(center: Vector2i) -> void: + pending_cleanup_cells.clear() + 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 - center.x) + var dist_z = abs(grid_pos.y - center.y) + if dist_x > eye_line + safe_margin or dist_z > eye_line + safe_margin: + pending_cleanup_cells.append(grid_pos) + +func _drain_generation_queue() -> void: + var processed = 0 + while processed < CHUNK_GENERATION_STEPS_PER_FRAME and not pending_generation_cells.is_empty(): + var grid_pos = pending_generation_cells.pop_front() + if board.has(grid_pos): + continue + + var is_obstacle = _register_cell_with_ray(grid_pos) + if not is_obstacle: + _add_compatible_biome(grid_pos) + processed += 1 + +func _drain_cleanup_queue() -> void: + var processed = 0 + while processed < CHUNK_CLEANUP_STEPS_PER_FRAME and not pending_cleanup_cells.is_empty(): + var grid_pos = pending_cleanup_cells.pop_front() + if not board.has(grid_pos): + continue + + var cell = board[grid_pos] + if cell["type"] == "obstacle": + continue + + if cell.has("node") and is_instance_valid(cell["node"]): + cell["node"].queue_free() + board.erase(grid_pos) + processed += 1 + +func _queue_lamppost_wire_connection(grid_pos: Vector2i) -> void: + if pending_wire_lookup.has(grid_pos): + return + pending_wire_lookup[grid_pos] = true + pending_wire_cells.append(grid_pos) + +func _drain_wire_queue() -> void: + var processed = 0 + while processed < LAMPPOST_WIRE_STEPS_PER_FRAME and not pending_wire_cells.is_empty(): + var grid_pos = pending_wire_cells.pop_front() + pending_wire_lookup.erase(grid_pos) + + if not board.has(grid_pos): + continue + if not board[grid_pos].get("have_lamppost", false): + continue + + _connect_lamppost_wires(grid_pos) + processed += 1 + func _generate_pieces_around_train(center: Vector2i) -> void: for x in range(-eye_line, eye_line + 1): for z in range(-eye_line, eye_line + 1): var grid_pos = center + Vector2i(x, z) - + if not board.has(grid_pos): var ce_obstacle = _register_cell_with_ray(grid_pos) if not ce_obstacle: @@ -166,7 +352,7 @@ func _register_cell_with_ray(grid_pos: Vector2i) -> bool: if info_list.size() > 0: var min_distance = 999999.0 var center_cell_pos = Vector3(world_pos.x, 0, world_pos.z) - + for info in info_list: if info is Node3D: var pos_info = Vector3(info.global_position.x, 0, info.global_position.z) @@ -191,7 +377,7 @@ func _register_cell_with_ray(grid_pos: Vector2i) -> bool: var data = right_info_node.get_rotated_data(rotation_steps) exit_found = data["connections"] height_found = data["heights"] - + if "have_lamppost" in right_info_node: have_lamppost = right_info_node.have_lamppost @@ -206,7 +392,7 @@ func _register_cell_with_ray(grid_pos: Vector2i) -> bool: } if have_lamppost: - _connect_lamppost_wires(grid_pos) + _queue_lamppost_wire_connection(grid_pos) return true return false @@ -229,43 +415,40 @@ func _add_compatible_biome(grid_pos: Vector2i) -> void: var valid_candidates = [] for scene in zone_catalogue: - var test_chunk = scene.instantiate() - var test_list: Array[Node] = [] - collect_all_chunkinfo(test_chunk, test_list) - var info_test = test_list[0] if test_list.size() > 0 else null + var metadata = _get_chunk_scene_metadata(scene) + if metadata.is_empty(): + continue - if info_test != null and info_test.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"] + for candidate in metadata["rotations"]: + var rot = candidate["rotation"] + var data = candidate["data"] + 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 - 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() + valid_candidates.append({"scene": scene, "rotation": rot, "data": data, "score": score}) if valid_candidates.size() > 0: var max_score = -1 @@ -278,16 +461,14 @@ func _add_compatible_biome(grid_pos: Vector2i) -> void: 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) + add_child(new_chunk) - var new_list: Array[Node] = [] - collect_all_chunkinfo(new_chunk, new_list) - var info_new = new_list[0] if new_list.size() > 0 else null + var info_new = _get_cached_chunk_info(new_chunk, choise.scene) var have_lamppost = false - if info_new != null and "have_lamppost" in new_chunk: + if info_new != null and "have_lamppost" in info_new: have_lamppost = info_new.have_lamppost board[grid_pos] = { @@ -300,16 +481,14 @@ func _add_compatible_biome(grid_pos: Vector2i) -> void: } if have_lamppost: - _connect_lamppost_wires(grid_pos) + _queue_lamppost_wire_connection(grid_pos) else: var backup = zone_catalogue[0].instantiate() - add_child(backup) backup.position = Vector3(grid_pos.x * chunk_size, 0, grid_pos.y * chunk_size) + add_child(backup) - var backup_list: Array[Node] = [] - collect_all_chunkinfo(backup, backup_list) - var info_backup = backup_list[0] if backup_list.size() > 0 else null + var info_backup = _get_cached_chunk_info(backup, zone_catalogue[0]) var safe_heights = { "north": req_height_north if req_height_north != -1 else height_target, @@ -351,8 +530,6 @@ func _train_radar() -> void: var current_progress = rail_path.train_progress var total_length = rail_path.curve.get_baked_length() - #var exchange_distance = 0.0 - #var next_biome = "" for step in range(1, 31): var next_progress = wrapf(current_progress + (step * chunk_size), 0.0, total_length) @@ -364,44 +541,18 @@ func _train_radar() -> void: var future_biome = _get_procedural_biome_name(noise_generator.get_noise_2d(f_x, f_z)) if future_biome != current_biome: - #exchange_distance = step * chunk_size - #next_biome = future_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) - - func _get_lampposts(info_node: Node, side: String) -> Array[Node3D]: var lampposts: Array[Node3D] = [] if side == "sx": if "connection_left" in info_node and info_node.connection_left != null: for p in info_node.connection_left: if is_instance_valid(p): lampposts.append(p) - #if lampposts.is_empty() and "nodo_attacco_sx" in nodo_info and is_instance_valid(nodo_info.get("nodo_attacco_sx")): - # lampposts.append(nodo_info.get("nodo_attacco_sx")) else: if "connection_right" in info_node and info_node.connection_right != null: for p in info_node.connection_right: if is_instance_valid(p): lampposts.append(p) - #if lampposts.is_empty() and "nodo_attacco_dx" in nodo_info and is_instance_valid(nodo_info.get("nodo_attacco_dx")): - # lampposts.append(nodo_info.get("nodo_attacco_dx")) return lampposts func _connect_lamppost_wires(new_board_pos: Vector2i) -> void: @@ -471,7 +622,7 @@ func _connect_lamppost_wires(new_board_pos: Vector2i) -> void: if board.has(closest_pos) and board[closest_pos].get("have_lamppost", false): var closest_root = board[closest_pos]["node"] var closest_info = board[closest_pos]["info"] - + if not is_instance_valid(closest_root) or closest_root == new_root or closest_info == null: continue if closest_info is Node3D: closest_info.force_update_transform() diff --git a/core/daynight/environment_manager.gd b/core/daynight/environment_manager.gd index 0c259ef..13ad596 100644 --- a/core/daynight/environment_manager.gd +++ b/core/daynight/environment_manager.gd @@ -4,6 +4,7 @@ extends Node3D const NOISE_TEXTURE: Texture2D = preload("res://core/daynight/noise.tres") const WEATHER_SHADER: Material = preload("res://core/daynight/weather_overlay.tres") const WEATHER_PLAIN_SHADER: Material = preload("res://core/daynight/weather_plain_shader.tres") +const DYNAMIC_ENVIRONMENT_UPDATES_PER_FRAME: int = 2 #how many update of the environment (apply materials) will be done per frame @export var environment_config: EnvironmentConfig @@ -35,6 +36,7 @@ var weather_controller: WeatherController var day_tween: Tween var day_time: float = 0.0 +var pending_environment_nodes: Dictionary = {} func _ready() -> void: @@ -76,9 +78,50 @@ func _ready() -> void: weather_controller.name = "WeatherController" add_child(weather_controller) +func _process(_delta: float) -> void: + _process_pending_environment_nodes() + func _on_tree_node_added(node: Node) -> void: if node.is_in_group("wind_node") or node.is_in_group("weather_node") or node.is_in_group("weather_vegetables_node"): - call_deferred("_apply_dynamic_environment_materials", node) + _queue_dynamic_environment_node(node) + +func _queue_dynamic_environment_node(node: Node) -> void: + if not is_instance_valid(node): + return + + var ancestor = node.get_parent() + while ancestor != null: + if pending_environment_nodes.has(ancestor.get_instance_id()): + return + ancestor = ancestor.get_parent() + + var stale_ids: Array[int] = [] + for node_id in pending_environment_nodes.keys(): + var pending_node = pending_environment_nodes[node_id] + if not is_instance_valid(pending_node): + stale_ids.append(node_id) + continue + if node.is_ancestor_of(pending_node): + stale_ids.append(node_id) + + for node_id in stale_ids: + pending_environment_nodes.erase(node_id) + + pending_environment_nodes[node.get_instance_id()] = node + +func _process_pending_environment_nodes() -> void: + var processed = 0 + for node_id in pending_environment_nodes.keys(): + if processed >= DYNAMIC_ENVIRONMENT_UPDATES_PER_FRAME: + break + + var node = pending_environment_nodes[node_id] + pending_environment_nodes.erase(node_id) + if not is_instance_valid(node): + continue + + _apply_dynamic_environment_materials(node) + processed += 1 func _apply_dynamic_environment_materials(node: Node) -> void: if not is_instance_valid(node):