fix stutter

This commit is contained in:
2026-04-30 16:21:08 +02:00
parent b7308c5d66
commit 8c7f1c3c82
2 changed files with 284 additions and 90 deletions

View File

@@ -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:
@@ -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,8 +97,19 @@ func _destroy_and_regenrate_world() -> void:
#Create train
last_pos_train = Vector2i(999999, 999999)
_train_radar()
last_pos_train = Vector2i(999999, 999999)
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:
@@ -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,6 +134,168 @@ 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):
@@ -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,14 +415,13 @@ 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)
for candidate in metadata["rotations"]:
var rot = candidate["rotation"]
var data = candidate["data"]
var u_conn = data["connections"]
var u_height = data["heights"]
@@ -265,8 +450,6 @@ func _add_compatible_biome(grid_pos: Vector2i) -> void:
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:
@@ -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:

View File

@@ -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):