diff --git a/project.godot b/project.godot index afb0f98..802725c 100644 --- a/project.godot +++ b/project.godot @@ -22,7 +22,6 @@ driver/enable_input=true [autoload] GameManager="*res://scripts/game_manager.gd" -MapBuilder="*res://scripts/map_builder.gd" AssetLoader="*res://scripts/asset_loader.gd" [filesystem] diff --git a/scripts/classes/item_def.gd b/scripts/classes/item_def.gd index 1ccc64c..f4e7fa7 100644 --- a/scripts/classes/item_def.gd +++ b/scripts/classes/item_def.gd @@ -7,3 +7,9 @@ var render_distance: float var flags: int var childs: Array[TDFX] var colfile: ColFile + +# LoD system additions +var lod_distances: Array[float] = [] +var num_lods: int = 0 +var is_big_building: bool = false +var related_model: ItemDef = null diff --git a/scripts/classes/streamed_mesh.gd b/scripts/classes/streamed_mesh.gd index e5ad5b2..37034ed 100644 --- a/scripts/classes/streamed_mesh.gd +++ b/scripts/classes/streamed_mesh.gd @@ -1,40 +1,140 @@ class_name StreamedMesh extends MeshInstance3D +# LoD System Constants +const DRAW_DISTANCE_FACTOR = 1.5 +const MAGIC_LOD_DISTANCE = 330.0 +const VEHICLE_LOD_DISTANCE = 70.0 +const VEHICLE_DRAW_DISTANCE = 280.0 + var _idef: ItemDef var _thread := Thread.new() -var _mesh_buf: Mesh +var _mesh_buf: Array[Mesh] = [] # Array to store different LoD meshes +var _current_lod_level: int = -1 # Current LoD level being displayed +var _lod_nodes: Array[MeshInstance3D] = [] # Child nodes for LoD models func _init(idef: ItemDef): _idef = idef + + # Create child nodes for each LoD level + if _idef.num_lods > 0: + for i in range(_idef.num_lods): + var lod_node := MeshInstance3D.new() + lod_node.name = "LOD_" + str(i) + lod_node.visible = false + add_child(lod_node) + _lod_nodes.append(lod_node) + _mesh_buf.append(null) + else: + # Default mesh buffer for base model + _mesh_buf.append(null) func _exit_tree(): if _thread.is_alive(): _thread.wait_to_finish() func _process(delta: float) -> void: - if _thread.is_started() == false: - if get_viewport().get_camera_3d() != null: - var dist := get_viewport().get_camera_3d().global_position.distance_to(global_position) - if dist < visibility_range_end and mesh == null: - _thread.start(_load_mesh) - while _thread.is_alive(): - await get_tree().process_frame - _thread.wait_to_finish() - mesh = _mesh_buf - elif dist > visibility_range_end and mesh != null: - mesh = null - -func _load_mesh() -> void: + if get_viewport().get_camera_3d() == null: + return + + # Calculate distance to camera + var camera_pos = get_viewport().get_camera_3d().global_position + var raw_distance = camera_pos.distance_to(global_position) + var distance = raw_distance / DRAW_DISTANCE_FACTOR + + # Select appropriate LoD level based on distance + var selected_lod = _select_lod_level(distance) + + # If LoD level changed or mesh not loaded, load the appropriate mesh + if selected_lod != _current_lod_level or (selected_lod >= 0 and _get_active_mesh() == null): + _current_lod_level = selected_lod + + # Hide all LoD nodes first + for node in _lod_nodes: + node.visible = false + mesh = null + + # If object is too far, don't show anything + if selected_lod < 0: + return + + # If we need to load a mesh but haven't yet + if _thread.is_started() == false and _mesh_buf[selected_lod] == null: + _thread.start(Callable(_load_mesh).bind(selected_lod)) + while _thread.is_alive(): + await get_tree().process_frame + _thread.wait_to_finish() + + # Show the appropriate mesh + if selected_lod == 0: + # Base model goes on the main instance + mesh = _mesh_buf[0] + elif selected_lod < _lod_nodes.size() + 1: + # LoD models go on child nodes + _lod_nodes[selected_lod - 1].mesh = _mesh_buf[selected_lod] + _lod_nodes[selected_lod - 1].visible = true + +func _select_lod_level(distance: float) -> int: + # Special handling for big buildings + if _idef.is_big_building: + if distance < MAGIC_LOD_DISTANCE and _idef.related_model != null: + return 0 # Show detailed model + return -1 # Too far, don't show + + # Normal LoD selection + if _idef.lod_distances.size() > 0: + # Check against each LoD distance threshold + for i in range(_idef.lod_distances.size()): + if distance < _idef.lod_distances[i]: + return i # Return the appropriate LoD level + + # Object is too far away, don't render + return -1 + else: + # No LoD information, use simple visibility range + return 0 if distance < _idef.render_distance else -1 + +func _get_active_mesh() -> Mesh: + if _current_lod_level == 0: + return mesh + elif _current_lod_level > 0 and _current_lod_level <= _lod_nodes.size(): + return _lod_nodes[_current_lod_level - 1].mesh + return null + +func _get_lod_model_name(lod_level: int) -> String: + var base_name = _idef.model_name + if lod_level == 0: + return base_name + else: + return base_name + "_l" + str(lod_level) + +func _load_mesh(lod_level: int) -> void: AssetLoader.mutex.lock() if _idef.flags & 0x40: + AssetLoader.mutex.unlock() return - var access := AssetLoader.open_asset(_idef.model_name + ".dff") + + # Get model name with appropriate LoD suffix + var model_name = _get_lod_model_name(lod_level) + + # Try to open the asset file + var access = AssetLoader.open_asset(model_name + ".dff") + if access == null: + # If the specific LoD model doesn't exist, fall back to the base model + if lod_level > 0: + access = AssetLoader.open_asset(_idef.model_name + ".dff") + + # If still no model, exit + if access == null: + AssetLoader.mutex.unlock() + return + + # Load the mesh geometry var glist := RWClump.new(access).geometry_list for geometry in glist.geometries: - _mesh_buf = geometry.mesh - for surf_id in _mesh_buf.get_surface_count(): - var material := _mesh_buf.surface_get_material(surf_id) as StandardMaterial3D + _mesh_buf[lod_level] = geometry.mesh + for surf_id in _mesh_buf[lod_level].get_surface_count(): + var material := _mesh_buf[lod_level].surface_get_material(surf_id) as StandardMaterial3D material.cull_mode = BaseMaterial3D.CULL_DISABLED if _idef.flags & 0x08: material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD @@ -50,5 +150,5 @@ func _load_mesh() -> void: BaseMaterial3D.TRANSPARENCY_ALPHA_HASH if _idef.flags & 0x04 and not _idef.flags & 0x08 else BaseMaterial3D.TRANSPARENCY_ALPHA ) break - _mesh_buf.surface_set_material(surf_id, material) + _mesh_buf[lod_level].surface_set_material(surf_id, material) AssetLoader.mutex.unlock() diff --git a/scripts/map_builder.gd b/scripts/map_builder.gd deleted file mode 100644 index 89af055..0000000 --- a/scripts/map_builder.gd +++ /dev/null @@ -1,185 +0,0 @@ -extends Node - -var items: Dictionary[int, ItemDef] -var itemchilds: Array[TDFX] -var placements: Array[ItemPlacement] -var collisions: Array[ColFile] -var map: Node3D -var _loaded := false - -func _ready() -> void: - var file := FileAccess.open(GameManager.gta_path + "data/gta3.dat", FileAccess.READ) - assert(file != null, "%d" % FileAccess.get_open_error()) - while not file.eof_reached(): - var line := file.get_line() - if not line.begins_with("#"): - var tokens := line.split(" ", false) - if tokens.size() > 0: - match tokens[0]: - "IDE": - _read_map_data(tokens[1], _read_ide_line) - "COLFILE": - var colfile := AssetLoader.open(GameManager.gta_path + tokens[2]) - - while colfile.get_position() < colfile.get_length(): - collisions.append(ColFile.new(colfile)) - "IPL": - _read_map_data(tokens[1], _read_ipl_line) - "CDIMAGE": - AssetLoader.load_cd_image(tokens[1]) - _: - push_warning("implement %s" % tokens[0]) - for child in itemchilds: - items[child.parent].childs.append(child) - for colfile in collisions: - if colfile.model_id in items: - items[colfile.model_id].colfile = colfile - else: - for k in items: - var item := items[k] as ItemDef - if item.model_name.matchn(colfile.model_name): - items[k].colfile = colfile - -func _read_ide_line(section: String, tokens: Array[String]): - var item := ItemDef.new() - var id := tokens[0].to_int() - match section: - "objs": - item.model_name = tokens[1] - item.txd_name = tokens[2] - item.render_distance = tokens[4].to_float() - item.flags = tokens[tokens.size() - 1].to_int() - items[id] = item - "tobj": - # TODO: Timed objects - item.model_name = tokens[1] - item.txd_name = tokens[2] - items[id] = item - "2dfx": - var parent := tokens[0].to_int() - # Convert GTA to Godot coordinate system - var position := Vector3( - tokens[1].to_float(), - tokens[3].to_float(), - -tokens[2].to_float() ) - var color := Color( - tokens[4].to_float() / 255, - tokens[5].to_float() / 255, - tokens[6].to_float() / 255 ) - match tokens[8].to_int(): - 0: - var lightdef := TDFXLight.new() - lightdef.parent = parent - lightdef.position = position - lightdef.color = color - lightdef.render_distance = tokens[11].to_float() - lightdef.range = tokens[12].to_float() - lightdef.shadow_intensity = tokens[15].to_int() - itemchilds.append(lightdef) - var type: - push_warning("implement 2DFX type %d" % type) - -func _read_ipl_line(section: String, tokens: Array[String]): - match section: - "inst": - var placement := ItemPlacement.new() - placement.id = tokens[0].to_int() - placement.model_name = tokens[1].to_lower() - # Convert GTA to Godot coordinate system - placement.position = Vector3( - tokens[2].to_float(), - tokens[4].to_float(), - -tokens[3].to_float(), ) - # Scale conversion follows the same pattern - placement.scale = Vector3( - tokens[5].to_float(), - tokens[7].to_float(), - tokens[6].to_float(), ) - # Quaternion conversion requires negating components - placement.rotation = Quaternion( - -tokens[8].to_float(), - -tokens[10].to_float(), - -tokens[9].to_float(), - tokens[11].to_float(), ) - placements.append(placement) - -func _read_map_data(path: String, line_handler: Callable) -> void: - var file := AssetLoader.open(path) - assert(file != null, "%d" % FileAccess.get_open_error() ) - var section: String - while not file.eof_reached(): - var line := file.get_line() - if line.length() == 0 or line.begins_with("#"): - continue - var tokens := line.replace(" ", "").split(",", false) - if tokens.size() == 1: - section = tokens[0] - else: - line_handler.call(section, tokens) - -func clear_map() -> void: - map = Node3D.new() - -func spawn_placement(ipl: ItemPlacement) -> Node3D: - return spawn(ipl.id, ipl.model_name, ipl.position, ipl.scale, ipl.rotation) - -func spawn(id: int, model_name: String, position: Vector3, scale: Vector3, rotation: Quaternion) -> Node3D: - var item := items[id] as ItemDef - if item.flags & 0x40: - return Node3D.new() - var instance := StreamedMesh.new(item) - instance.position = position - instance.scale = scale - instance.quaternion = rotation - instance.visibility_range_end = item.render_distance - for child in item.childs: - if child is TDFXLight: - var light := OmniLight3D.new() - light.position = child.position - light.light_color = child.color - light.distance_fade_enabled = true - # TODO: Remove half distance when https://github.com/godotengine/godot/issues/56657 is solved - light.distance_fade_begin = child.render_distance / 2.0 - light.omni_range = child.range - light.light_energy = float(child.shadow_intensity) / 20.0 -# light.shadow_enabled = true - instance.add_child(light) - var sb := StaticBody3D.new() - if item.colfile != null: - for collision in item.colfile.collisions: - var colshape := CollisionShape3D.new() - if collision is ColFile.TBox: - var aabb := AABB() - # Get min and max positions from collision box - var min_pos := collision.min as Vector3 - var max_pos := collision.max as Vector3 - - # Ensure AABB has positive size by sorting min/max for each axis - aabb.position = Vector3( - min(min_pos.x, max_pos.x), - min(min_pos.y, max_pos.y), - min(min_pos.z, max_pos.z) - ) - aabb.end = Vector3( - max(min_pos.x, max_pos.x), - max(min_pos.y, max_pos.y), - max(min_pos.z, max_pos.z) - ) - - # Only create the shape if size is valid - if aabb.size.x > 0 and aabb.size.y > 0 and aabb.size.z > 0: - var shape := BoxShape3D.new() - shape.size = aabb.size - colshape.shape = shape - colshape.position = aabb.get_center() - sb.add_child(colshape) - else: - sb.add_child(colshape) - if item.colfile.vertices.size() > 0: - var colshape := CollisionShape3D.new() - var shape := ConcavePolygonShape3D.new() - shape.set_faces(item.colfile.vertices) - colshape.shape = shape - sb.add_child(colshape) - instance.add_child(sb) - return instance diff --git a/scripts/map_loader.gd b/scripts/map_loader.gd new file mode 100644 index 0000000..99b78ef --- /dev/null +++ b/scripts/map_loader.gd @@ -0,0 +1,313 @@ +class_name MapLoader +extends Node + +signal loading_progress(progress_percent: float) + +var _items: Dictionary[int, ItemDef] +var _itemchilds: Array[TDFX] +var _placements: Array[ItemPlacement] +var _collisions: Array[ColFile] +var _parsed := false + +func _ready() -> void: + pass + +func _read_ide_line(section: String, tokens: Array[String]): + var item := ItemDef.new() + var id := tokens[0].to_int() + match section: + "objs": + item.model_name = tokens[1] + item.txd_name = tokens[2] + + # Parse LoD information + var num_lods = tokens[3].to_int() + item.num_lods = num_lods + + # Set render distance for the base model + item.render_distance = tokens[4].to_float() + + # Parse LoD distances if available + for i in range(num_lods): + if 4 + i < tokens.size() - 1: # Avoid reading flags as LoD distance + var lod_distance = tokens[4 + i].to_float() + item.lod_distances.append(lod_distance) + + # Check if this is a big building (based on research notes) + if item.lod_distances.size() > 0 and item.lod_distances[0] > 300.0 and num_lods < 3: + item.is_big_building = true + # Note: related model association will be done after all models are loaded + + item.flags = tokens[tokens.size() - 1].to_int() + _items[id] = item + "tobj": + # TODO: Timed objects + item.model_name = tokens[1] + item.txd_name = tokens[2] + + # Parse LoD information for timed objects too + if tokens.size() > 4: + var num_lods = tokens[3].to_int() + item.num_lods = num_lods + + # Set render distance for the base model + item.render_distance = tokens[4].to_float() + + # Parse LoD distances if available + for i in range(num_lods): + if 4 + i < tokens.size() - 1: # Avoid reading flags as LoD distance + var lod_distance = tokens[4 + i].to_float() + item.lod_distances.append(lod_distance) + + _items[id] = item + "2dfx": + var parent := tokens[0].to_int() + # Convert GTA to Godot coordinate system + var position := Vector3( + tokens[1].to_float(), + tokens[3].to_float(), + -tokens[2].to_float() ) + var color := Color( + tokens[4].to_float() / 255, + tokens[5].to_float() / 255, + tokens[6].to_float() / 255 ) + match tokens[8].to_int(): + 0: + var lightdef := TDFXLight.new() + lightdef.parent = parent + lightdef.position = position + lightdef.color = color + lightdef.render_distance = tokens[11].to_float() + lightdef.range = tokens[12].to_float() + lightdef.shadow_intensity = tokens[15].to_int() + _itemchilds.append(lightdef) + var type: + push_warning("implement 2DFX type %d" % type) + +func _read_ipl_line(section: String, tokens: Array[String]): + match section: + "inst": + var placement := ItemPlacement.new() + placement.id = tokens[0].to_int() + placement.model_name = tokens[1].to_lower() + # Convert GTA to Godot coordinate system + placement.position = Vector3( + tokens[2].to_float(), + tokens[4].to_float(), + -tokens[3].to_float(), ) + # Scale conversion follows the same pattern + placement.scale = Vector3( + tokens[5].to_float(), + tokens[7].to_float(), + tokens[6].to_float(), ) + # Quaternion conversion requires negating components + placement.rotation = Quaternion( + -tokens[8].to_float(), + -tokens[10].to_float(), + -tokens[9].to_float(), + tokens[11].to_float(), ) + _placements.append(placement) + +func _read_map_data(path: String, line_handler: Callable) -> void: + var file := AssetLoader.open(path) + assert(file != null, "%d" % FileAccess.get_open_error() ) + var section: String + while not file.eof_reached(): + var line := file.get_line() + if line.length() == 0 or line.begins_with("#"): + continue + var tokens := line.replace(" ", "").split(",", false) + if tokens.size() == 1: + section = tokens[0] + else: + line_handler.call(section, tokens) + +func _find_related_models() -> void: + # Associate big buildings with their related low-detail models + # Based on the research notes, big buildings follow specific naming patterns + # For example, "LODxxx" is matched with "HDRxxx" + + var lod_models := {} + var hd_models := {} + + # First, collect all potential LOD and HD models by naming convention + for id in _items: + var item := _items[id] as ItemDef + var model_name := item.model_name.to_lower() + + if model_name.begins_with("lod"): + lod_models[model_name.substr(3)] = id + elif model_name.begins_with("hdr"): + hd_models[model_name.substr(3)] = id + + # Now associate the related models + for suffix in lod_models: + if suffix in hd_models: + var lod_id = lod_models[suffix] + var hd_id = hd_models[suffix] + + # Associate the HD model with its LOD model + if _items[hd_id].is_big_building: + _items[hd_id].related_model = _items[lod_id] + + # Also check for other naming patterns if needed + # (Add more patterns based on GTA3 specific conventions) + +func parse_map_data() -> void: + if _parsed: + return + + var file := FileAccess.open(GameManager.gta_path + "data/gta3.dat", FileAccess.READ) + assert(file != null, "%d" % FileAccess.get_open_error()) + + print("Loading map data...") + while not file.eof_reached(): + var line := file.get_line() + if not line.begins_with("#"): + var tokens := line.split(" ", false) + if tokens.size() > 0: + match tokens[0]: + "IDE": + _read_map_data(tokens[1], _read_ide_line) + "COLFILE": + var colfile := AssetLoader.open(GameManager.gta_path + tokens[2]) + + while colfile.get_position() < colfile.get_length(): + _collisions.append(ColFile.new(colfile)) + "IPL": + _read_map_data(tokens[1], _read_ipl_line) + "CDIMAGE": + AssetLoader.load_cd_image(tokens[1]) + _: + push_warning("implement %s" % tokens[0]) + for child in _itemchilds: + _items[child.parent].childs.append(child) + for colfile in _collisions: + if colfile.model_id in _items: + _items[colfile.model_id].colfile = colfile + else: + for k in _items: + var item := _items[k] as ItemDef + if item.model_name.matchn(colfile.model_name): + _items[k].colfile = colfile + + # Find and associate related models for big buildings + _find_related_models() + + _parsed = true + +func load_map() -> Node3D: + # Make sure map data is parsed + parse_map_data() + + var map := Node3D.new() + + var start_t := Time.get_ticks_msec() + var target = _placements.size() + var count := 0 + + print("Loading map...") + for ipl in _placements: + map.add_child(spawn_placement(ipl)) + count += 1 + + # No await statement - calculate and emit progress, but don't yield + var progress = float(count) / float(target) + call_deferred("emit_signal", "loading_progress", progress) + + print("Map load completed in %f seconds" % ((Time.get_ticks_msec() - start_t) / 1000.0)) + return map + +func spawn_placement(ipl: ItemPlacement) -> Node3D: + return spawn(ipl.id, ipl.model_name, ipl.position, ipl.scale, ipl.rotation) + +func spawn(id: int, model_name: String, position: Vector3, scale: Vector3, rotation: Quaternion) -> Node3D: + var item := _items[id] as ItemDef + if item.flags & 0x40: + return Node3D.new() + + # Create a Node3D container for big buildings with related models + var container: Node3D + + if item.is_big_building and item.related_model != null: + # For big buildings, create a container node + container = Node3D.new() + container.name = "BigBuilding_" + str(id) + container.position = position + container.scale = scale + container.quaternion = rotation + + # Create the high-detail model + var high_detail := StreamedMesh.new(item) + high_detail.name = "HighDetail" + container.add_child(high_detail) + + # Create the low-detail model using the related model definition + if item.related_model != null: + var low_detail := StreamedMesh.new(item.related_model) + low_detail.name = "LowDetail" + container.add_child(low_detail) + + # Add a script to handle switching between high and low detail + # (The StreamedMesh already handles this via _select_lod_level) + else: + # For regular models + container = StreamedMesh.new(item) + container.position = position + container.scale = scale + container.quaternion = rotation + + # Add effects and child objects + for child in item.childs: + if child is TDFXLight: + var light := OmniLight3D.new() + light.position = child.position + light.light_color = child.color + light.distance_fade_enabled = true + light.distance_fade_begin = child.render_distance + light.omni_range = child.range + light.light_energy = float(child.shadow_intensity) / 20.0 + light.shadow_enabled = true + container.add_child(light) + + # Add collision + var sb := StaticBody3D.new() + if item.colfile != null: + for collision in item.colfile.collisions: + var colshape := CollisionShape3D.new() + if collision is ColFile.TBox: + var aabb := AABB() + # Get min and max positions from collision box + var min_pos := collision.min as Vector3 + var max_pos := collision.max as Vector3 + + # Ensure AABB has positive size by sorting min/max for each axis + aabb.position = Vector3( + min(min_pos.x, max_pos.x), + min(min_pos.y, max_pos.y), + min(min_pos.z, max_pos.z) + ) + aabb.end = Vector3( + max(min_pos.x, max_pos.x), + max(min_pos.y, max_pos.y), + max(min_pos.z, max_pos.z) + ) + + # Only create the shape if size is valid + if aabb.size.x > 0 and aabb.size.y > 0 and aabb.size.z > 0: + var shape := BoxShape3D.new() + shape.size = aabb.size + colshape.shape = shape + colshape.position = aabb.get_center() + sb.add_child(colshape) + else: + sb.add_child(colshape) + if item.colfile.vertices.size() > 0: + var colshape := CollisionShape3D.new() + var shape := ConcavePolygonShape3D.new() + shape.set_faces(item.colfile.vertices) + colshape.shape = shape + sb.add_child(colshape) + container.add_child(sb) + + return container diff --git a/scripts/map_builder.gd.uid b/scripts/map_loader.gd.uid similarity index 100% rename from scripts/map_builder.gd.uid rename to scripts/map_loader.gd.uid diff --git a/scripts/map_test.gd b/scripts/map_test.gd index 9f17294..db258cd 100644 --- a/scripts/map_test.gd +++ b/scripts/map_test.gd @@ -1,23 +1,22 @@ extends Node -@onready var world := Node3D.new() var suzanne := preload("res://prefabs/suzanne.tscn") +var map_loader: MapLoader func _ready() -> void: - var start := Time.get_ticks_msec() - var target = MapBuilder.placements.size() - var count := 0 - var start_t := Time.get_ticks_msec() -# add_child(MapBuilder.map) - for ipl in MapBuilder.placements: - world.add_child(MapBuilder.spawn_placement(ipl)) - count += 1 - if Time.get_ticks_msec() - start > (1.0 / 30.0) * 1000: - start = Time.get_ticks_msec() - print("%f" % (float(count) / float(target))) - await get_tree().physics_frame - print("Map load completed in %f seconds" % ((Time.get_ticks_msec() - start_t) / 1000)) - add_child(world) + map_loader = MapLoader.new() + add_child(map_loader) + + # Connect signals for progress updates + map_loader.loading_progress.connect(_on_loading_progress) + + # TODO: Implement a proper loading screen before enabling threaded loading + # For now, load the map directly + var map := map_loader.load_map() + add_child(map) + +func _on_loading_progress(progress: float) -> void: + print("Loading: %d%%" % int(progress * 100)) func _unhandled_input(event: InputEvent) -> void: if event is InputEventKey: diff --git a/scripts/world.gd b/scripts/world.gd index eea0795..589d0bd 100644 --- a/scripts/world.gd +++ b/scripts/world.gd @@ -1,29 +1,30 @@ extends Node -@onready var world := Node3D.new() @onready var sun = $sun @onready var moon = $moon @onready var sky = $WorldEnvironment var car := preload("res://scenes/car.tscn") +var map_loader: MapLoader func _ready() -> void: - var start := Time.get_ticks_msec() - var target = MapBuilder.placements.size() - var count := 0 - var start_t := Time.get_ticks_msec() -# add_child(MapBuilder.map) - for ipl in MapBuilder.placements: - world.add_child(MapBuilder.spawn_placement(ipl)) - count += 1 - if Time.get_ticks_msec() - start > (1.0 / 30.0) * 1000: - start = Time.get_ticks_msec() - print("%f" % (float(count) / float(target))) - await get_tree().physics_frame - print("Map load completed in %f seconds" % ((Time.get_ticks_msec() - start_t) / 1000)) - add_child(world) + map_loader = MapLoader.new() + add_child(map_loader) + + # Connect signals for progress updates + map_loader.loading_progress.connect(_on_loading_progress) + + # TODO: Implement a proper loading screen before enabling threaded loading + # For now, load the map directly + var world_node := map_loader.load_map() + + # Add world to scene and setup environment + add_child(world_node) sky.environment = load("res://scenes/world/day.tres") moon.visible = not moon.visible +func _on_loading_progress(progress: float) -> void: + print("Loading: %d%%" % int(progress * 100)) + func _unhandled_input(event: InputEvent) -> void: if Input.is_action_pressed("spawn"): var car_node := car.instantiate()