mirror of
https://github.com/FOSS-Supremacy/OpenLiberty.git
synced 2025-04-28 20:07:57 +03:00
Huge cleanup and improvements
This commit is contained in:
parent
d895da2671
commit
fdeee17337
177 changed files with 5407 additions and 352 deletions
|
@ -1,18 +1,14 @@
|
|||
extends Node
|
||||
|
||||
|
||||
var assets: Dictionary
|
||||
var mutex := Mutex.new()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
load_cd_image("models/gta3.img")
|
||||
|
||||
|
||||
func load_cd_image(path: String) -> void:
|
||||
var file := open(path.to_lower().trim_suffix(".img") + ".dir")
|
||||
assert(file != null, "%d" % FileAccess.get_open_error())
|
||||
|
||||
while not file.eof_reached():
|
||||
var entry := DirEntry.new()
|
||||
entry.img = path
|
||||
|
@ -20,11 +16,9 @@ func load_cd_image(path: String) -> void:
|
|||
entry.size = int(file.get_32()) * 2048
|
||||
assets[file.get_buffer(24).get_string_from_ascii().to_lower()] = entry
|
||||
|
||||
|
||||
func open(path: String) -> FileAccess:
|
||||
var diraccess := DirAccess.open(GameManager.gta_path)
|
||||
var parts := path.replace("\\", "/").split("/")
|
||||
|
||||
for part in parts:
|
||||
if part == parts[parts.size() - 1]:
|
||||
for file in diraccess.get_files():
|
||||
|
@ -35,20 +29,16 @@ func open(path: String) -> FileAccess:
|
|||
if dir.matchn(part):
|
||||
diraccess.change_dir(dir)
|
||||
break
|
||||
|
||||
return null
|
||||
|
||||
|
||||
func open_asset(name: String) -> FileAccess:
|
||||
if name.to_lower() in assets:
|
||||
var asset = assets[name.to_lower()] as DirEntry
|
||||
var access := open(assets[name.to_lower()].img)
|
||||
access.seek(asset.offset)
|
||||
return access
|
||||
|
||||
return open("models/" + name)
|
||||
|
||||
|
||||
class DirEntry:
|
||||
var img: String
|
||||
var offset: int
|
||||
|
|
16
scripts/car/car.gd
Normal file
16
scripts/car/car.gd
Normal file
|
@ -0,0 +1,16 @@
|
|||
extends VehicleBody3D
|
||||
|
||||
const MAX_STEER = 0.8
|
||||
const ENGINE_POWER = 300
|
||||
|
||||
func _process(delta):
|
||||
steering = move_toward(steering, Input.get_axis("right", "left") * MAX_STEER, delta * 2.5)
|
||||
engine_force = Input.get_axis("break", "run") * ENGINE_POWER
|
||||
|
||||
func _enter_tree():
|
||||
set_multiplayer_authority(name.to_int())
|
||||
|
||||
func _physics_process(delta):
|
||||
if is_multiplayer_authority():
|
||||
steering = move_toward(steering, Input.get_axis("right", "left") * MAX_STEER, delta * 2.5)
|
||||
engine_force = Input.get_axis("break", "run") * ENGINE_POWER
|
12
scripts/car/lights.gd
Normal file
12
scripts/car/lights.gd
Normal file
|
@ -0,0 +1,12 @@
|
|||
extends Node3D
|
||||
|
||||
@onready var external_lights = [ $left_light, $right_light ]
|
||||
@onready var internal_light = $internal_light
|
||||
|
||||
func _physics_process(_delta):
|
||||
if Input.is_action_just_pressed("internal_light"):
|
||||
internal_light.visible = not internal_light.visible
|
||||
|
||||
if Input.is_action_just_pressed("external_lights"):
|
||||
for light in external_lights:
|
||||
light.visible = not light.visible
|
6
scripts/car/microphone.gd
Normal file
6
scripts/car/microphone.gd
Normal file
|
@ -0,0 +1,6 @@
|
|||
extends AudioStreamPlayer3D
|
||||
|
||||
func _physics_process(_delta):
|
||||
if Input.is_action_pressed("switch_microphone"):
|
||||
self.playing = not self.playing
|
||||
self.stream_paused = not self.stream_paused
|
26
scripts/car/music.gd
Normal file
26
scripts/car/music.gd
Normal file
|
@ -0,0 +1,26 @@
|
|||
extends Node3D
|
||||
|
||||
@onready var speakers = [ $back_left_speaker, $back_right_speaker, $front_left_speaker, $front_right_speaker ]
|
||||
|
||||
func _physics_process(_delta):
|
||||
if Input.is_action_just_pressed("play_pause_music"):
|
||||
for speaker in speakers:
|
||||
speaker.playing = not speaker.playing
|
||||
speaker.stream_paused = not speaker.stream_paused
|
||||
if Input.is_action_just_pressed("decrease_music_volume"):
|
||||
for speaker in speakers:
|
||||
speaker.unit_size = speaker.unit_size - 0.2
|
||||
if Input.is_action_just_pressed("increase_music_volume"):
|
||||
for speaker in speakers:
|
||||
speaker.unit_size = speaker.unit_size + 0.2
|
||||
|
||||
#func load_audio_files():
|
||||
#var dir = DirAccess.open("res://music")
|
||||
#if dir:
|
||||
#dir.list_dir_begin()
|
||||
#var file_name = dir.get_next()
|
||||
#while file_name != "":
|
||||
#if ".mp3" in file_name:
|
||||
#for speaker in speakers:
|
||||
#speaker.stream = load(file_name)
|
||||
#file_name = dir.get_next()
|
|
@ -1,45 +1,35 @@
|
|||
class_name ColFile
|
||||
extends RefCounted
|
||||
|
||||
|
||||
var fourcc: String
|
||||
var file_size: int
|
||||
var model_name: String
|
||||
var model_id: int
|
||||
var tbounds: TBounds
|
||||
|
||||
var collisions: Array[TBase]
|
||||
var vertices: PackedVector3Array
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
fourcc = file.get_buffer(4).get_string_from_ascii()
|
||||
assert(fourcc == "COLL")
|
||||
|
||||
file_size = file.get_32()
|
||||
model_name = file.get_buffer(22).get_string_from_ascii()
|
||||
model_id = file.get_16()
|
||||
tbounds = TBounds.new(file)
|
||||
|
||||
for i in file.get_32():
|
||||
collisions.append(TSphere.new(file))
|
||||
file.get_32()
|
||||
|
||||
for i in file.get_32():
|
||||
collisions.append(TBox.new(file))
|
||||
|
||||
var unsorted := PackedVector3Array()
|
||||
|
||||
for i in file.get_32():
|
||||
unsorted.append(TVertex.new(file).position)
|
||||
|
||||
for i in file.get_32():
|
||||
var face := TFace.new(file)
|
||||
vertices.append(unsorted[face.a])
|
||||
vertices.append(unsorted[face.b])
|
||||
vertices.append(unsorted[face.c])
|
||||
|
||||
|
||||
class TBase:
|
||||
func read_vector3(file: FileAccess) -> Vector3:
|
||||
var result := Vector3()
|
||||
|
@ -48,14 +38,12 @@ class TBase:
|
|||
result.z = file.get_float()
|
||||
return result
|
||||
|
||||
|
||||
class TBounds extends TBase:
|
||||
var radius: float
|
||||
var center: Vector3
|
||||
var min: Vector3
|
||||
var max: Vector3
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
radius = file.get_float()
|
||||
|
||||
|
@ -63,61 +51,50 @@ class TBounds extends TBase:
|
|||
min = read_vector3(file)
|
||||
max = read_vector3(file)
|
||||
|
||||
|
||||
class TSurface extends TBase:
|
||||
var material: int
|
||||
var flag: int
|
||||
var brightness: int
|
||||
var light: int
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
material = file.get_8()
|
||||
flag = file.get_8()
|
||||
brightness = file.get_8()
|
||||
light = file.get_8()
|
||||
|
||||
|
||||
class TSphere extends TBase:
|
||||
var radius: float
|
||||
var center: Vector3
|
||||
var surface: TSurface
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
radius = file.get_float()
|
||||
center = read_vector3(file)
|
||||
|
||||
surface = TSurface.new(file)
|
||||
|
||||
|
||||
class TBox extends TBase:
|
||||
var min: Vector3
|
||||
var max: Vector3
|
||||
var surface: TSurface
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
min = read_vector3(file)
|
||||
max = read_vector3(file)
|
||||
surface = TSurface.new(file)
|
||||
|
||||
|
||||
class TVertex extends TBase:
|
||||
var position: Vector3
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
position = read_vector3(file)
|
||||
|
||||
|
||||
class TFace extends TBase:
|
||||
var a: int
|
||||
var b: int
|
||||
var c: int
|
||||
var surface: TSurface
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
a = file.get_32()
|
||||
b = file.get_32()
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
class_name ItemDef
|
||||
extends RefCounted
|
||||
|
||||
|
||||
var model_name: String
|
||||
var txd_name: String
|
||||
var render_distance: float
|
||||
var flags: int
|
||||
|
||||
var childs: Array[TDFX]
|
||||
var colfile: ColFile
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
class_name ItemPlacement
|
||||
extends RefCounted
|
||||
|
||||
|
||||
var id: int
|
||||
var model_name: String
|
||||
var position: Vector3
|
||||
|
|
|
@ -1,22 +1,17 @@
|
|||
class_name StreamedMesh
|
||||
extends MeshInstance3D
|
||||
|
||||
|
||||
var _idef: ItemDef
|
||||
var _thread := Thread.new()
|
||||
|
||||
var _mesh_buf: Mesh
|
||||
|
||||
|
||||
func _init(idef: ItemDef):
|
||||
_idef = idef
|
||||
|
||||
|
||||
func _exit_tree():
|
||||
if _thread.is_alive():
|
||||
_thread.wait_to_finish()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _thread.is_started() == false:
|
||||
var dist := get_viewport().get_camera_3d().global_position.distance_to(global_position)
|
||||
|
@ -29,15 +24,12 @@ func _process(delta: float) -> void:
|
|||
elif dist > visibility_range_end and mesh != null:
|
||||
mesh = null
|
||||
|
||||
|
||||
func _load_mesh() -> void:
|
||||
AssetLoader.mutex.lock()
|
||||
if _idef.flags & 0x40:
|
||||
return
|
||||
|
||||
var access := AssetLoader.open_asset(_idef.model_name + ".dff")
|
||||
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():
|
||||
|
@ -46,22 +38,16 @@ func _load_mesh() -> void:
|
|||
if _idef.flags & 0x08:
|
||||
material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
|
||||
if material.has_meta("texture_name"):
|
||||
var txd := RWTextureDict.new(AssetLoader.open_asset(_idef.txd_name + ".txd"))
|
||||
var texture_name = material.get_meta("texture_name")
|
||||
|
||||
for raster in txd.textures:
|
||||
if texture_name.matchn(raster.name):
|
||||
material.albedo_texture = ImageTexture.create_from_image(raster.image)
|
||||
if raster.has_alpha:
|
||||
material.transparency = (
|
||||
BaseMaterial3D.TRANSPARENCY_ALPHA_HASH if _idef.flags & 0x04 and not _idef.flags & 0x08
|
||||
else BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
)
|
||||
|
||||
else BaseMaterial3D.TRANSPARENCY_ALPHA )
|
||||
break
|
||||
|
||||
_mesh_buf.surface_set_material(surf_id, material)
|
||||
|
||||
AssetLoader.mutex.unlock()
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
class_name TDFX
|
||||
extends RefCounted
|
||||
|
||||
|
||||
var parent: int
|
||||
var position: Vector3
|
||||
var color: Color
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
class_name TDFXLight
|
||||
extends TDFX
|
||||
|
||||
|
||||
var render_distance: float
|
||||
var range: float
|
||||
var shadow_intensity: int
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
extends Node
|
||||
|
||||
|
||||
var gta_path: String
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if OS.has_feature("editor"):
|
||||
gta_path = ProjectSettings.globalize_path("res://gta/")
|
||||
else:
|
||||
gta_path = OS.get_executable_path().get_base_dir() + "/"
|
||||
|
||||
print("GTA path: %s" % gta_path)
|
||||
|
||||
var err := get_tree().change_scene_to_file("res://scenes/main_menu/main_menu.tscn")
|
||||
assert(err == OK, "failed to load main menu")
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
extends Node
|
||||
|
||||
|
||||
var items: Dictionary
|
||||
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("#"):
|
||||
|
@ -34,10 +29,8 @@ func _ready() -> void:
|
|||
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
|
||||
|
@ -47,7 +40,6 @@ func _ready() -> void:
|
|||
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()
|
||||
|
@ -57,27 +49,22 @@ func _read_ide_line(section: String, tokens: Array[String]):
|
|||
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()
|
||||
var position := Vector3(
|
||||
tokens[1].to_float(),
|
||||
tokens[2].to_float(),
|
||||
tokens[3].to_float()
|
||||
)
|
||||
tokens[3].to_float() )
|
||||
var color := Color(
|
||||
tokens[4].to_float() / 255,
|
||||
tokens[5].to_float() / 255,
|
||||
tokens[6].to_float() / 255
|
||||
)
|
||||
|
||||
tokens[6].to_float() / 255 )
|
||||
match tokens[8].to_int():
|
||||
0:
|
||||
var lightdef := TDFXLight.new()
|
||||
|
@ -87,122 +74,91 @@ func _read_ide_line(section: String, tokens: Array[String]):
|
|||
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()
|
||||
|
||||
placement.position = Vector3(
|
||||
tokens[2].to_float(),
|
||||
tokens[3].to_float(),
|
||||
tokens[4].to_float(),
|
||||
)
|
||||
|
||||
tokens[4].to_float(), )
|
||||
placement.scale = Vector3(
|
||||
tokens[5].to_float(),
|
||||
tokens[6].to_float(),
|
||||
tokens[7].to_float(),
|
||||
)
|
||||
|
||||
tokens[7].to_float(), )
|
||||
placement.rotation = Quaternion(
|
||||
-tokens[8].to_float(),
|
||||
-tokens[9].to_float(),
|
||||
-tokens[10].to_float(),
|
||||
tokens[11].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())
|
||||
|
||||
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()
|
||||
map.rotation.x = deg_to_rad(-90.0)
|
||||
|
||||
|
||||
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()
|
||||
aabb.position = collision.min
|
||||
aabb.end = collision.max
|
||||
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = aabb.size
|
||||
|
||||
colshape.shape = shape
|
||||
colshape.position = aabb.get_center()
|
||||
|
||||
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
|
||||
|
|
28
scripts/map_test.gd
Normal file
28
scripts/map_test.gd
Normal file
|
@ -0,0 +1,28 @@
|
|||
extends Node
|
||||
|
||||
@onready var world := Node3D.new()
|
||||
var suzanne := preload("res://prefabs/suzanne.tscn")
|
||||
|
||||
func _ready() -> void:
|
||||
world.rotation.x = deg_to_rad(-90.0)
|
||||
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)
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey:
|
||||
if event.physical_keycode == KEY_SPACE and event.pressed:
|
||||
var node := suzanne.instantiate() as RigidBody3D
|
||||
add_child(node)
|
||||
node.global_position = get_viewport().get_camera_3d().global_position
|
19
scripts/menu.gd
Normal file
19
scripts/menu.gd
Normal file
|
@ -0,0 +1,19 @@
|
|||
extends Control
|
||||
|
||||
@onready var container := $VBoxContainer as VBoxContainer
|
||||
|
||||
const scenes := {
|
||||
"Texture viewer": "res://scenes/txd.tscn",
|
||||
"Flycam test": "res://scenes/flycam/flycam.tscn",
|
||||
"DFF Test": "res://scenes/model_test.tscn",
|
||||
"Map loader test": "res://scenes/map_test.tscn", }
|
||||
|
||||
func _ready() -> void:
|
||||
for k in scenes:
|
||||
var button := Button.new()
|
||||
button.text = k
|
||||
button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
button.pressed.connect(func _load_scene():
|
||||
get_tree().change_scene_to_file(scenes[k]) )
|
||||
container.add_child(button)
|
40
scripts/model_test.gd
Normal file
40
scripts/model_test.gd
Normal file
|
@ -0,0 +1,40 @@
|
|||
extends Node
|
||||
|
||||
@onready var spinbox: SpinBox = $GUI/VBoxContainer/HBoxContainer/SpinBox
|
||||
@onready var meshinstance: MeshInstance3D = $mesh
|
||||
var dff: RWClump
|
||||
var misc: RWTextureDict
|
||||
|
||||
func _ready() -> void:
|
||||
spinbox.rounded = true
|
||||
spinbox.max_value = 0
|
||||
misc = RWTextureDict.new(GameManager.open_file("models/misc.txd", FileAccess.READ))
|
||||
meshinstance.rotation.x = deg_to_rad(-90.0)
|
||||
|
||||
func _ld_dff() -> void:
|
||||
var dialog := FileDialog.new()
|
||||
dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
dialog.current_dir = GameManager.gta_path
|
||||
dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
dialog.add_filter("*.dff")
|
||||
add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(600, 400))
|
||||
var file_path := (await dialog.file_selected) as String
|
||||
remove_child(dialog)
|
||||
var file := FileAccess.open(file_path, FileAccess.READ)
|
||||
dff = RWClump.new(file)
|
||||
spinbox.value = 0
|
||||
spinbox.max_value = dff.geometry_list.geometry_count - 1
|
||||
_ld_model(0)
|
||||
|
||||
func _ld_model(value: float) -> void:
|
||||
var geometry := dff.geometry_list.geometries[int(value)]
|
||||
meshinstance.mesh = geometry.mesh
|
||||
var material := geometry.material_list.materials[0] as RWMaterial
|
||||
meshinstance.material_override = material.material
|
||||
if material.is_textured:
|
||||
var texname := material.texture.texture_name.string
|
||||
for raster in misc.textures:
|
||||
if texname.to_lower() == raster.name:
|
||||
meshinstance.material_override.albedo_texture = ImageTexture.create_from_image(raster.image)
|
||||
break
|
25
scripts/multiplayer.gd
Normal file
25
scripts/multiplayer.gd
Normal file
|
@ -0,0 +1,25 @@
|
|||
extends Node3D
|
||||
|
||||
@onready var host = $host
|
||||
@onready var join = $join
|
||||
var peer = ENetMultiplayerPeer.new()
|
||||
@export var player_scene: PackedScene
|
||||
|
||||
func _on_host_pressed():
|
||||
host.visible = false
|
||||
join.visible = false
|
||||
peer.create_server(6006)
|
||||
multiplayer.multiplayer_peer = peer
|
||||
multiplayer.peer_connected.connect(_add_player)
|
||||
_add_player()
|
||||
|
||||
func _add_player(id = 1):
|
||||
var player = player_scene.instantiate()
|
||||
player.name = str(id)
|
||||
call_deferred("add_child",player)
|
||||
|
||||
func _on_join_pressed():
|
||||
host.visible = false
|
||||
join.visible = false
|
||||
peer.create_client("localhost", 6006)
|
||||
multiplayer.multiplayer_peer = peer
|
14
scripts/player/player.gd
Normal file
14
scripts/player/player.gd
Normal file
|
@ -0,0 +1,14 @@
|
|||
extends CharacterBody3D
|
||||
|
||||
@onready var pivot = $cam_origin
|
||||
|
||||
var mouse_sens = 0.4
|
||||
|
||||
func _ready():
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||
|
||||
func _input(event):
|
||||
if event is InputEventMouseMotion:
|
||||
pivot.rotate_y(deg_to_rad(-event.relative.x * mouse_sens))
|
||||
pivot.rotate_x(deg_to_rad(-event.relative.y * mouse_sens))
|
||||
pivot.rotation.z = 0
|
|
@ -4,7 +4,6 @@ extends RefCounted
|
|||
##
|
||||
## [url]https://gtamods.com/wiki/RenderWare_binary_stream_file[/url]
|
||||
|
||||
|
||||
enum ChunkType {
|
||||
STRING = 0x2,
|
||||
TEXTURE = 0x6,
|
||||
|
@ -33,16 +32,13 @@ var build: int:
|
|||
if library_id & 0xffff0000:
|
||||
return library_id & 0xffff
|
||||
return 0
|
||||
|
||||
var _start: int
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
type = file.get_32()
|
||||
size = file.get_32()
|
||||
library_id = file.get_32()
|
||||
_start = file.get_position()
|
||||
|
||||
|
||||
func skip(file: FileAccess) -> void:
|
||||
file.seek(_start + size)
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
class_name RWClump
|
||||
extends RWChunk
|
||||
|
||||
|
||||
var atomic_count: int
|
||||
var light_count: int
|
||||
var camera_count: int
|
||||
|
||||
var frame_list: RWFrameList
|
||||
var geometry_list: RWGeometryList
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
super(file)
|
||||
assert(type == ChunkType.CLUMP)
|
||||
|
||||
RWChunk.new(file)
|
||||
var atomic_count = file.get_32()
|
||||
if version > 0x33000:
|
||||
light_count = file.get_32()
|
||||
camera_count = file.get_32()
|
||||
|
||||
frame_list = RWFrameList.new(file)
|
||||
geometry_list = RWGeometryList.new(file)
|
||||
|
|
|
@ -1,44 +1,33 @@
|
|||
class_name RWFrameList
|
||||
extends RWChunk
|
||||
|
||||
|
||||
var frame_count: int
|
||||
var frames: Array[Frame]
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
super(file)
|
||||
assert(type == ChunkType.FRAME_LIST)
|
||||
|
||||
frame_count = file.get_32()
|
||||
frames.resize(frame_count)
|
||||
|
||||
for frame_i in frame_count:
|
||||
var frame := Frame.new()
|
||||
frame.rotation_matrix.resize(3)
|
||||
|
||||
for vec_i in 3:
|
||||
var x := file.get_float()
|
||||
var y := file.get_float()
|
||||
var z := file.get_float()
|
||||
|
||||
frame.rotation_matrix[vec_i] = Vector3(x, y, z)
|
||||
|
||||
var x := file.get_float()
|
||||
var y := file.get_float()
|
||||
var z := file.get_float()
|
||||
|
||||
frame.position.x = x
|
||||
frame.position.y = y
|
||||
frame.position.z = z
|
||||
frame.index = file.get_32()
|
||||
frame.flags = file.get_32()
|
||||
|
||||
frames[frame.index] = frame
|
||||
|
||||
skip(file)
|
||||
|
||||
|
||||
class Frame:
|
||||
var rotation_matrix: Array[Vector3]
|
||||
var position: Vector3
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
class_name RWGeometry
|
||||
extends RWChunk
|
||||
|
||||
|
||||
enum {
|
||||
rpGEOMETRYTRISTRIP = 0x00000001,
|
||||
rpGEOMETRYPOSITIONS = 0x00000002,
|
||||
|
@ -23,7 +22,6 @@ var morph_target_count: int
|
|||
var ambient: float
|
||||
var specular: float
|
||||
var diffuse: float
|
||||
|
||||
var uv_count: int
|
||||
var uvs: Array[PackedVector2Array]
|
||||
var tris: Array[Triangle]
|
||||
|
@ -37,14 +35,12 @@ var mesh: ArrayMesh:
|
|||
var morph_t := morph_targets[0]
|
||||
var st := SurfaceTool.new()
|
||||
var surfaces: Dictionary
|
||||
|
||||
# Split tris by their material ID
|
||||
for tri in tris:
|
||||
var mat_id := tri.material_id
|
||||
if not mat_id in surfaces:
|
||||
surfaces[mat_id] = []
|
||||
surfaces[mat_id].append(tri)
|
||||
|
||||
for surf_id in surfaces:
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
var surface = surfaces[surf_id] as Array[Triangle]
|
||||
|
@ -54,31 +50,23 @@ var mesh: ArrayMesh:
|
|||
st.set_normal(morph_t.normals[tri["vertex_%d" % i]])
|
||||
if uvs.size() > 0:
|
||||
st.set_uv(uvs[0][tri["vertex_%d" % i]])
|
||||
|
||||
st.add_vertex(morph_t.vertices[tri["vertex_%d" % i]])
|
||||
|
||||
var rwmaterial := material_list.materials[tri.material_id]
|
||||
var material := rwmaterial.material
|
||||
|
||||
if rwmaterial.is_textured:
|
||||
material.set_meta("texture_name", rwmaterial.texture.texture_name)
|
||||
st.set_material(material)
|
||||
|
||||
if format & rpGEOMETRYTRISTRIP == 0 and morph_t.has_normals == false:
|
||||
st.generate_normals()
|
||||
|
||||
if mesh == null:
|
||||
mesh = st.commit()
|
||||
else:
|
||||
st.commit(mesh)
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
super(file)
|
||||
assert(type == ChunkType.GEOMETRY)
|
||||
|
||||
RWChunk.new(file)
|
||||
format = file.get_32()
|
||||
tri_count = file.get_32()
|
||||
|
@ -88,18 +76,15 @@ func _init(file: FileAccess):
|
|||
ambient = file.get_float()
|
||||
specular = file.get_float()
|
||||
diffuse = file.get_float()
|
||||
|
||||
if format & rpGEOMETRYNATIVE == 0:
|
||||
if format & rpGEOMETRYPRELIT:
|
||||
file.seek(file.get_position() + (vert_count * 4)) # Skip
|
||||
|
||||
uv_count = (format & 0x00ff0000) >> 16
|
||||
if uv_count == 0:
|
||||
if format & rpGEOMETRYTEXTURED2:
|
||||
uv_count = 2
|
||||
elif format & rpGEOMETRYTEXTURED:
|
||||
uv_count = 1
|
||||
|
||||
for i in uv_count:
|
||||
var coords := PackedVector2Array()
|
||||
for j in vert_count:
|
||||
|
@ -107,7 +92,6 @@ func _init(file: FileAccess):
|
|||
var v := file.get_float()
|
||||
coords.append(Vector2(u, v))
|
||||
uvs.append(coords)
|
||||
|
||||
for i in tri_count:
|
||||
var tri := Triangle.new()
|
||||
tri.vertex_2 = file.get_16()
|
||||
|
@ -115,7 +99,6 @@ func _init(file: FileAccess):
|
|||
tri.material_id = file.get_16()
|
||||
tri.vertex_3 = file.get_16()
|
||||
tris.append(tri)
|
||||
|
||||
for i in morph_target_count:
|
||||
var morph_t := MorphTarget.new()
|
||||
morph_t.bounding_sphere = Sphere.new()
|
||||
|
@ -125,7 +108,6 @@ func _init(file: FileAccess):
|
|||
morph_t.bounding_sphere.radius = file.get_float()
|
||||
morph_t.has_vertices = file.get_32() != 0
|
||||
morph_t.has_normals = file.get_32() != 0
|
||||
|
||||
if morph_t.has_vertices:
|
||||
for j in vert_count:
|
||||
var vert := Vector3()
|
||||
|
@ -133,7 +115,6 @@ func _init(file: FileAccess):
|
|||
vert.y = file.get_float()
|
||||
vert.z = file.get_float()
|
||||
morph_t.vertices.append(vert)
|
||||
|
||||
if morph_t.has_normals:
|
||||
for j in vert_count:
|
||||
var normal := Vector3()
|
||||
|
@ -141,21 +122,16 @@ func _init(file: FileAccess):
|
|||
normal.y = file.get_float()
|
||||
normal.z = file.get_float()
|
||||
morph_t.normals.append(normal)
|
||||
|
||||
morph_targets.append(morph_t)
|
||||
|
||||
material_list = RWMaterialList.new(file)
|
||||
|
||||
skip(file)
|
||||
|
||||
|
||||
class Triangle:
|
||||
var vertex_2: int
|
||||
var vertex_1: int
|
||||
var material_id: int
|
||||
var vertex_3: int
|
||||
|
||||
|
||||
class MorphTarget:
|
||||
var bounding_sphere: Sphere
|
||||
var has_vertices: bool
|
||||
|
@ -163,7 +139,6 @@ class MorphTarget:
|
|||
var vertices: Array[Vector3]
|
||||
var normals: Array[Vector3]
|
||||
|
||||
|
||||
class Sphere:
|
||||
var x: float
|
||||
var y: float
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
class_name RWGeometryList
|
||||
extends RWChunk
|
||||
|
||||
|
||||
var geometry_count: int
|
||||
var geometries: Array[RWGeometry]
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
super(file)
|
||||
assert(type == ChunkType.GEOMETRY_LIST)
|
||||
|
||||
RWChunk.new(file)
|
||||
geometry_count = file.get_32()
|
||||
|
||||
for i in geometry_count:
|
||||
geometries.append(RWGeometry.new(file))
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
class_name RWMaterial
|
||||
extends RWChunk
|
||||
|
||||
|
||||
var color: Color
|
||||
var is_textured: bool
|
||||
var texture: RWTexture
|
||||
|
@ -9,21 +8,17 @@ var material: StandardMaterial3D:
|
|||
get:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = color
|
||||
|
||||
if version > 0x30400:
|
||||
mat.roughness = 1.0 - specular
|
||||
|
||||
return mat
|
||||
|
||||
var ambient: float
|
||||
var specular: float
|
||||
var diffuse: float
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
super(file)
|
||||
assert(type == ChunkType.MATERIAL)
|
||||
|
||||
RWChunk.new(file)
|
||||
file.get_32()
|
||||
color.r8 = file.get_8()
|
||||
|
@ -31,14 +26,11 @@ func _init(file: FileAccess):
|
|||
color.b8 = file.get_8()
|
||||
color.a8 = file.get_8()
|
||||
file.get_32()
|
||||
|
||||
is_textured = file.get_32() > 0
|
||||
|
||||
if version > 0x30400:
|
||||
ambient = file.get_float()
|
||||
specular = file.get_float()
|
||||
diffuse = file.get_float()
|
||||
|
||||
if is_textured:
|
||||
texture = RWTexture.new(file)
|
||||
skip(file)
|
||||
|
|
|
@ -1,27 +1,21 @@
|
|||
class_name RWMaterialList
|
||||
extends RWChunk
|
||||
|
||||
|
||||
var material_count: int
|
||||
var indices: Array[int]
|
||||
var materials: Array[RWMaterial]
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
super(file)
|
||||
assert(type == ChunkType.MATERIAL_LIST)
|
||||
|
||||
RWChunk.new(file)
|
||||
material_count = file.get_32()
|
||||
|
||||
for i in material_count:
|
||||
# For fuck's sake, someone PR a code to do this into Godot.
|
||||
indices.append((file.get_32() + (1 << 31)) % (1 << 32) - (1 << 31))
|
||||
|
||||
indices.append((file.get_32() + (1 << 31)) % (1 << 32) - (1 << 31) )
|
||||
for i in indices:
|
||||
if i == -1:
|
||||
materials.append(RWMaterial.new(file))
|
||||
else:
|
||||
assert(false, "implement")
|
||||
|
||||
skip(file)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
class_name RWRaster
|
||||
extends RWChunk
|
||||
|
||||
|
||||
enum {
|
||||
FORMAT_DEFAULT = 0x0000,
|
||||
FORMAT_1555 = 0x0100,
|
||||
|
@ -11,7 +10,6 @@ enum {
|
|||
FORMAT_8888 = 0x0500,
|
||||
FORMAT_888 = 0x0600,
|
||||
FORMAT_555 = 0x0A00,
|
||||
|
||||
FORMAT_EXT_AUTO_MIPMAP = 0x1000,
|
||||
FORMAT_EXT_PAL8 = 0x2000,
|
||||
FORMAT_EXT_PAL4 = 0x4000,
|
||||
|
@ -24,7 +22,6 @@ var u_addressing: int
|
|||
var v_addressing: int
|
||||
var name: String
|
||||
var mask_name: String
|
||||
|
||||
var raster_format: int
|
||||
var has_alpha: bool
|
||||
var width: int
|
||||
|
@ -33,24 +30,19 @@ var depth: int
|
|||
var num_levels: int
|
||||
var raster_type: int
|
||||
var compression: int
|
||||
|
||||
var _file: FileAccess
|
||||
var _image_start: int
|
||||
var image: Image: get = _load_image
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
super(file)
|
||||
assert(type == ChunkType.RASTER)
|
||||
|
||||
RWChunk.new(file)
|
||||
platform_id = file.get_32()
|
||||
filter_mode = file.get_8()
|
||||
|
||||
var uv_addressing = file.get_8()
|
||||
u_addressing = uv_addressing >> 4
|
||||
v_addressing = uv_addressing & 0xf
|
||||
|
||||
file.get_16()
|
||||
name = file.get_buffer(32).get_string_from_ascii()
|
||||
mask_name = file.get_buffer(32).get_string_from_ascii()
|
||||
|
@ -62,7 +54,6 @@ func _init(file: FileAccess):
|
|||
num_levels = file.get_8()
|
||||
raster_type = file.get_8()
|
||||
compression = file.get_8()
|
||||
|
||||
_file = file
|
||||
_image_start = file.get_position()
|
||||
skip(file)
|
||||
|
@ -72,7 +63,6 @@ func _load_image():
|
|||
var result: Image
|
||||
var format: Image.Format
|
||||
var read: int
|
||||
|
||||
match raster_format & 0x0f00:
|
||||
# FORMAT_1555:
|
||||
# format = FORMAT_1555
|
||||
|
@ -91,11 +81,9 @@ func _load_image():
|
|||
read = 3
|
||||
_:
|
||||
assert(false)
|
||||
|
||||
if raster_format & (FORMAT_EXT_PAL8 | FORMAT_EXT_PAL4):
|
||||
var psize := (16 if raster_format & FORMAT_EXT_PAL4 else 256)
|
||||
var palette := Image.create_from_data(psize, 1, false, format, _unpad(psize, read))
|
||||
|
||||
result = Image.create(width, height, raster_format & 0x8000, format)
|
||||
_file.get_32()
|
||||
for i in width * height:
|
||||
|
@ -108,17 +96,14 @@ func _load_image():
|
|||
# _file.get_32()
|
||||
# var unpadded := _unpad(width * height, read)
|
||||
# var data := PackedInt32Array()
|
||||
#
|
||||
# for i in unpadded.size() / 2:
|
||||
# var x := int(i % width)
|
||||
# var y := int(i / width)
|
||||
#
|
||||
# var pixel := unpadded[i] | unpadded[i + 1] << 16
|
||||
# var a := (pixel & 0x8000) >> 15
|
||||
# var r := (pixel & 0x7c00) >> 10
|
||||
# var g := (pixel & 0x03e0) >> 5
|
||||
# var b := pixel & 0x001f
|
||||
#
|
||||
# result.set_pixel(
|
||||
# x, y, Color(
|
||||
# r / 0x1f,
|
||||
|
@ -129,7 +114,6 @@ func _load_image():
|
|||
# )
|
||||
else:
|
||||
var data := PackedByteArray()
|
||||
|
||||
var mip_width := width
|
||||
var mip_height := height
|
||||
for i in num_levels:
|
||||
|
@ -137,28 +121,22 @@ func _load_image():
|
|||
data.append_array(_unpad(mip_width * mip_height, read))
|
||||
mip_width /= 2
|
||||
mip_height /= 2
|
||||
|
||||
result = Image.create_from_data(width, height, raster_format & FORMAT_EXT_MIPMAP, format, data)
|
||||
if raster_format & FORMAT_EXT_AUTO_MIPMAP:
|
||||
image.generate_mipmaps()
|
||||
|
||||
# Perform color conversion
|
||||
for i in width * height:
|
||||
var x := int(i % width)
|
||||
var y := int(i / width)
|
||||
var old := result.get_pixel(x, y)
|
||||
result.set_pixel(x, y, Color(old.b, old.g, old.r, old.a))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
func _unpad(length: int, read: int) -> PackedByteArray:
|
||||
var result := PackedByteArray()
|
||||
|
||||
for i in length:
|
||||
for j in read:
|
||||
result.append(_file.get_8())
|
||||
for j in 4 - read:
|
||||
_file.get_8()
|
||||
|
||||
return result
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
class_name RWString
|
||||
extends RWChunk
|
||||
|
||||
|
||||
var string: String
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
super(file)
|
||||
assert(type == ChunkType.STRING)
|
||||
|
||||
var chars: PackedByteArray
|
||||
while true:
|
||||
var char := file.get_8()
|
||||
|
@ -16,5 +13,4 @@ func _init(file: FileAccess):
|
|||
break
|
||||
chars.append(char)
|
||||
string = chars.get_string_from_ascii()
|
||||
|
||||
skip(file)
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
class_name RWTexture
|
||||
extends RWChunk
|
||||
|
||||
|
||||
var texture_name: String
|
||||
var mask_name: String
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
super(file)
|
||||
assert(type == ChunkType.TEXTURE)
|
||||
|
||||
RWChunk.new(file).skip(file)
|
||||
texture_name = RWString.new(file).string
|
||||
mask_name = RWString.new(file).string
|
||||
|
|
|
@ -2,22 +2,17 @@ class_name RWTextureDict
|
|||
extends RWChunk
|
||||
## RenderWare texture dictionary
|
||||
|
||||
|
||||
var texture_count: int
|
||||
var device_id: int
|
||||
var textures: Array[RWRaster]
|
||||
|
||||
|
||||
func _init(file: FileAccess):
|
||||
super(file)
|
||||
assert(type == ChunkType.TEXTURE_DICT)
|
||||
|
||||
RWChunk.new(file)
|
||||
texture_count = file.get_16()
|
||||
device_id = file.get_16()
|
||||
|
||||
for i in texture_count:
|
||||
var raster := RWRaster.new(file)
|
||||
textures.append(raster)
|
||||
|
||||
file.seek(_start + size)
|
||||
|
|
25
scripts/txd.gd
Normal file
25
scripts/txd.gd
Normal file
|
@ -0,0 +1,25 @@
|
|||
extends Control
|
||||
|
||||
var txd: RWTextureDict
|
||||
|
||||
func _load_image(index: int):
|
||||
$VBoxContainer/TextureRect.texture = null
|
||||
$VBoxContainer/TextureRect.texture = ImageTexture.create_from_image(txd.textures[index].image)
|
||||
|
||||
func _select_file():
|
||||
var dialog := FileDialog.new()
|
||||
dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
dialog.current_dir = GameManager.gta_path
|
||||
dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
dialog.add_filter("*.txd", "Texture Dictionary")
|
||||
add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(600, 400))
|
||||
var file_path := (await dialog.file_selected) as String
|
||||
remove_child(dialog)
|
||||
var file := FileAccess.open(file_path, FileAccess.READ)
|
||||
assert(file_path != null)
|
||||
txd = RWTextureDict.new(file)
|
||||
$VBoxContainer/HBoxContainer/OptionButton.clear()
|
||||
for raster in txd.textures:
|
||||
$VBoxContainer/HBoxContainer/OptionButton.add_item(raster.name)
|
||||
_load_image(0)
|
27
scripts/world.gd
Normal file
27
scripts/world.gd
Normal file
|
@ -0,0 +1,27 @@
|
|||
extends Node
|
||||
|
||||
@onready var world := Node3D.new()
|
||||
var car := preload("res://scenes/car.tscn")
|
||||
|
||||
func _ready() -> void:
|
||||
world.rotation.x = deg_to_rad(-90.0)
|
||||
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)
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if Input.is_action_pressed("spawn"):
|
||||
var car_node := car.instantiate()
|
||||
add_child(car_node)
|
||||
car_node.global_position = get_viewport().get_camera_3d().global_position
|
Loading…
Add table
Add a link
Reference in a new issue