模板脚本文件

This commit is contained in:
2026-09-12 22:18:23 +08:00
parent 2816092fb4
commit d3f1058d79
32 changed files with 3200 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
class_name Body
extends Node3D
const LERP_VELOCITY: float = 0.15
@export_category("Objects")
@export var _character: CharacterBody3D = null
@export var animation_player: AnimationPlayer = null
var _current_state: StringName = &""
func apply_rotation(_velocity: Vector3) -> void:
var new_rotation_y = lerp_angle(rotation.y, atan2(-_velocity.x, -_velocity.z), LERP_VELOCITY)
rotation.y = new_rotation_y
func get_movement_animation(_velocity: Vector3) -> StringName:
if not _character.is_on_floor():
if _velocity.y < 0:
return &"Fall"
if _current_state == &"Jump2":
return &"Jump2"
return &"Jump"
if _velocity:
if _character._is_running() and _character.is_on_floor():
return &"Sprint"
return &"Run"
return &"Idle"
func play_animation_state(state: StringName, restart: bool = false) -> void:
if not animation_player:
return
if not animation_player.has_animation(state):
return
if not restart and _current_state == state and animation_player.is_playing():
return
_current_state = state
animation_player.play(state)
+1
View File
@@ -0,0 +1 @@
uid://v8j54rbc0ik
+872
View File
@@ -0,0 +1,872 @@
class_name Character
extends CharacterBody3D
enum SkinColor { BLUE, YELLOW, GREEN, RED }
const NORMAL_SPEED = 6.0
const SPRINT_SPEED = 10.0
const JUMP_VELOCITY = 7.5
const FALL_GRAVITY_MULTIPLIER = 1.6
const BASE_NICKNAME_HEIGHT := 2.0
const SERVER_ANIMATION_REQUESTS_PER_SECOND := 10.0
const SERVER_ANIMATION_REQUEST_BURST := 6.0
const PICKUP_ANIMATION_DELAY_MSEC := 1000
const PICKUP_ANIMATION_WINDOW_MSEC := 2500
const PICKUP_REQUEST_COOLDOWN_MSEC := 1000
const ALLOWED_ANIMATION_STATES := {
&"Idle": true,
&"Run": true,
&"Sprint": true,
&"Jump": true,
&"Jump2": true,
&"Fall": true,
&"Attack1": true,
&"Emote2": true
}
const HAT_NODES_BY_ITEM := {
"fedora": "Fedora",
"headphones": "Headphones",
"pirate_hat": "PirateHat",
"sheriff_hat": "SheriffHat",
"sombrero": "Sombrero",
"wizard_hat": "WizardHat"
}
const WEAPON_NODES_BY_ITEM := {"sword": "Sword", "sword_big": "SwordBig", "axe": "Axe"}
const BACKPACK_NODES_BY_ITEM := {"backpack": "Backpack"}
const HEAD_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach/"
const HAND_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/LeftHandAttach/"
const BACK_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/BackAttach/"
const FIRST_PERSON_HIDDEN_BONES: Array[StringName] = [&"Head", &"HeadTop"]
@export var skin_color: SkinColor = SkinColor.BLUE
@export_category("Nickname")
@export_range(0.0, 1.0, 0.01) var nickname_clearance: float = 0.2
@export_category("Objects")
@export var _body: Node3D = null
@export var _spring_arm_offset: SpringArmCharacter = null
@export_category("Skin Colors")
@export var blue_texture: CompressedTexture2D
@export var yellow_texture: CompressedTexture2D
@export var green_texture: CompressedTexture2D
@export var red_texture: CompressedTexture2D
var player_inventory: PlayerInventory
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
var can_double_jump = true
var has_double_jumped = false
var is_attacking := false
var is_collecting := false
var _current_speed: float
var _spawn_point = Vector3(0, 5, 0)
var _animation_sequence := 0
var _last_applied_animation_sequence := 0
var _last_requested_animation: StringName = &""
var _appearance_sync_requesters: Dictionary = {}
var _server_animation_request_tokens := SERVER_ANIMATION_REQUEST_BURST
var _last_server_animation_token_update_msec := 0
var _server_pickup_animation_started_msec := -1
var _last_server_pickup_request_msec := -PICKUP_REQUEST_COOLDOWN_MSEC
var _pickup_area_camera_yaw_offset := 0.0
var _equipped_hat_visual_id := ""
@onready var nickname: Label3D = $PlayerNick/Nickname
@onready var _bottom_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Bottom")
@onready var _chest_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Chest")
@onready var _face_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Face")
@onready var _limbs_head_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/LimbsAndHead")
@onready var _skeleton: Skeleton3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D")
@onready var _pickup_area: Area3D = $GodotRobot3D/InfrontArea3D
@onready var _first_person_hud: CanvasLayer = $FirstPersonHUD
func _enter_tree():
set_multiplayer_authority(str(name).to_int())
$SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority()
func _ready():
if multiplayer.is_server():
player_inventory = PlayerInventory.new()
_add_starting_items()
call_deferred("_sync_equipment_appearance")
if not is_multiplayer_authority():
call_deferred("_sync_inventory_to_owner")
set_player_skin(skin_color)
var animation_player := get_node_or_null("GodotRobot3D/AnimationPlayer") as AnimationPlayer
if animation_player:
animation_player.animation_finished.connect(_on_animation_finished)
_body.play_animation_state(&"Idle")
nickname.visible = true
_set_nickname_height(BASE_NICKNAME_HEIGHT)
call_deferred("_update_nickname_height")
if not multiplayer.is_server():
call_deferred("_request_equipment_appearance")
if is_multiplayer_authority() and _spring_arm_offset:
_pickup_area_camera_yaw_offset = wrapf(
_pickup_area.global_rotation.y - _spring_arm_offset.global_rotation.y, -PI, PI
)
_spring_arm_offset.perspective_changed.connect(_on_camera_perspective_changed)
_on_camera_perspective_changed(_spring_arm_offset.is_first_person)
func _on_camera_perspective_changed(first_person: bool) -> void:
if not is_multiplayer_authority():
return
_body.visible = true
_bottom_mesh.visible = not first_person
_chest_mesh.visible = not first_person
_face_mesh.visible = not first_person
_limbs_head_mesh.visible = true
nickname.visible = not first_person
_first_person_hud.visible = first_person
for bone_name in FIRST_PERSON_HIDDEN_BONES:
var bone_index := _skeleton.find_bone(bone_name)
if bone_index >= 0:
_skeleton.set_bone_pose_scale(bone_index, Vector3.ZERO if first_person else Vector3.ONE)
if first_person:
_align_pickup_area_with_camera()
else:
_pickup_area.rotation = Vector3.ZERO
call_deferred("_refresh_nickname_height_after_perspective_change")
func _refresh_nickname_height_after_perspective_change() -> void:
if not is_multiplayer_authority() or _is_local_first_person():
return
var height := _calculate_nickname_height(_equipped_hat_visual_id)
_set_nickname_height(height)
nickname.visible = true
if multiplayer.is_server():
_broadcast_nickname_height(height)
func _align_pickup_area_with_camera() -> void:
var pickup_rotation := _pickup_area.global_rotation
pickup_rotation.y = wrapf(_spring_arm_offset.global_rotation.y + _pickup_area_camera_yaw_offset, -PI, PI)
_pickup_area.global_rotation = pickup_rotation
func _physics_process(delta):
if not multiplayer.has_multiplayer_peer() or not is_multiplayer_authority():
return
var current_scene = get_tree().get_current_scene()
var should_freeze = false
if current_scene:
if current_scene.has_method("is_gameplay_input_blocked") and current_scene.is_gameplay_input_blocked():
should_freeze = true
elif current_scene.has_method("is_chat_visible") and current_scene.is_chat_visible():
should_freeze = true
elif current_scene.has_method("is_inventory_visible") and current_scene.is_inventory_visible():
should_freeze = true
if is_attacking or is_collecting:
velocity.x = 0
velocity.z = 0
_apply_gravity(delta)
move_and_slide()
return
if should_freeze:
_freeze()
_apply_gravity(delta)
move_and_slide()
_request_animation(_body.get_movement_animation(velocity))
return
if Input.is_action_just_pressed("pickup") and is_on_floor() and _has_collectible_item_in_front():
is_collecting = true
_request_animation(&"Emote2", true)
return
if Input.is_action_just_pressed("attack") and is_on_floor():
_start_attack()
return
if is_on_floor():
can_double_jump = true
has_double_jumped = false
if Input.is_action_just_pressed("jump"):
velocity.y = JUMP_VELOCITY
can_double_jump = true
_request_animation(&"Jump", true)
else:
_apply_gravity(delta)
if can_double_jump and not has_double_jumped and Input.is_action_just_pressed("jump"):
velocity.y = JUMP_VELOCITY
has_double_jumped = true
can_double_jump = false
_request_animation(&"Jump2", true)
_move()
var collided = move_and_slide()
if collided:
_push_collided_items()
_request_animation(_body.get_movement_animation(velocity))
func _apply_gravity(delta: float) -> void:
if is_on_floor():
return
var gravity_multiplier = FALL_GRAVITY_MULTIPLIER if velocity.y < 0 else 1.0
velocity.y -= gravity * gravity_multiplier * delta
func _start_attack() -> void:
if is_attacking or is_collecting or not is_on_floor():
return
is_attacking = true
velocity.x = 0
velocity.z = 0
_request_animation(&"Attack1", true)
func _on_animation_finished(animation_name: StringName) -> void:
match animation_name:
&"Attack1":
is_attacking = false
&"Emote2":
is_collecting = false
func _request_animation(state: StringName, restart: bool = false) -> void:
if not ALLOWED_ANIMATION_STATES.has(state):
return
if not restart and _last_requested_animation == state:
return
_last_requested_animation = state
_body.play_animation_state(state, restart)
if multiplayer.is_server():
request_animation_state(state)
else:
request_animation_state.rpc_id(1, state)
@rpc("any_peer", "call_local", "reliable")
func request_animation_state(state: StringName) -> void:
if not multiplayer.is_server() or not _is_owner_request():
return
if not ALLOWED_ANIMATION_STATES.has(state):
return
if not _server_consume_animation_request_token():
return
if state == &"Emote2":
if not _is_grounded_on_server() or not _has_collectible_item_in_front():
return
_server_pickup_animation_started_msec = Time.get_ticks_msec()
else:
_server_pickup_animation_started_msec = -1
_server_publish_animation(state)
func _server_consume_animation_request_token() -> bool:
var now := Time.get_ticks_msec()
if _last_server_animation_token_update_msec == 0:
_last_server_animation_token_update_msec = now
else:
var elapsed_seconds := (now - _last_server_animation_token_update_msec) / 1000.0
_server_animation_request_tokens = minf(
SERVER_ANIMATION_REQUEST_BURST,
_server_animation_request_tokens + elapsed_seconds * SERVER_ANIMATION_REQUESTS_PER_SECOND
)
_last_server_animation_token_update_msec = now
if _server_animation_request_tokens < 1.0:
return false
_server_animation_request_tokens -= 1.0
return true
func _server_publish_animation(state: StringName) -> void:
if not multiplayer.is_server():
return
_animation_sequence += 1
sync_animation_state.rpc(state, _animation_sequence)
sync_animation_state(state, _animation_sequence)
@rpc("any_peer", "call_local", "reliable")
func sync_animation_state(state: StringName, sequence: int) -> void:
var sender_id := multiplayer.get_remote_sender_id()
if sender_id != 1 and not (sender_id == 0 and multiplayer.is_server()):
return
if sequence <= _last_applied_animation_sequence:
return
_last_applied_animation_sequence = sequence
if not ALLOWED_ANIMATION_STATES.has(state):
return
# The owner already played the input-driven state immediately.
if is_multiplayer_authority():
return
_body.play_animation_state(state, true)
func _push_collided_items() -> void:
for i in get_slide_collision_count():
var c = get_slide_collision(i)
if c.get_collider() is RigidBody3D:
apply_force_to_server_object.rpc_id(1, c.get_collider().name, -c.get_normal())
func _process(_delta):
if not multiplayer.has_multiplayer_peer():
return
if not is_multiplayer_authority():
return
var first_person := _spring_arm_offset != null and _spring_arm_offset.is_first_person
if first_person:
_align_pickup_area_with_camera()
var camera_input_blocked := false
var current_scene := get_tree().get_current_scene()
if current_scene and current_scene.has_method("is_camera_input_blocked"):
camera_input_blocked = current_scene.is_camera_input_blocked()
_first_person_hud.visible = (
first_person and not camera_input_blocked and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
)
_check_out_of_bounds()
func _freeze():
velocity.x = 0
velocity.z = 0
_current_speed = 0
_request_animation(&"Idle")
func _move() -> void:
var input_direction: Vector2 = Vector2.ZERO
if is_multiplayer_authority():
input_direction = Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
var direction: Vector3 = transform.basis * Vector3(input_direction.x, 0, input_direction.y).normalized()
_is_running()
direction = direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y)
if direction:
velocity.x = direction.x * _current_speed
velocity.z = direction.z * _current_speed
_body.apply_rotation(velocity)
return
velocity.x = move_toward(velocity.x, 0, _current_speed)
velocity.z = move_toward(velocity.z, 0, _current_speed)
func _is_running() -> bool:
if Input.is_action_pressed("shift"):
_current_speed = SPRINT_SPEED
return true
_current_speed = NORMAL_SPEED
return false
func _check_out_of_bounds():
if global_transform.origin.y < -15.0:
_reset_position_after_fall()
func _reset_position_after_fall():
global_transform.origin = _spawn_point
velocity = Vector3.ZERO
func _get_texture_from_name(color: SkinColor) -> CompressedTexture2D:
match color:
SkinColor.BLUE:
return blue_texture
SkinColor.GREEN:
return green_texture
SkinColor.RED:
return red_texture
SkinColor.YELLOW:
return yellow_texture
_:
return blue_texture
func set_player_skin(skin_name: SkinColor) -> void:
var texture = _get_texture_from_name(skin_name)
_set_mesh_texture(_bottom_mesh, texture)
_set_mesh_texture(_chest_mesh, texture)
_set_mesh_texture(_face_mesh, texture)
_set_mesh_texture(_limbs_head_mesh, texture)
func _set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2D) -> void:
if mesh_instance:
var new_material := StandardMaterial3D.new()
new_material.albedo_texture = texture
mesh_instance.set_surface_override_material(0, new_material)
@rpc("any_peer", "call_local", "reliable")
func sync_inventory_to_owner(inventory_data: Dictionary):
var sender_id = multiplayer.get_remote_sender_id()
if sender_id != 1 and not (sender_id == 0 and multiplayer.is_server()):
return
if not is_multiplayer_authority():
return
if not player_inventory:
player_inventory = PlayerInventory.new()
player_inventory.from_dict(inventory_data)
var level_scene = get_tree().get_current_scene()
if (
level_scene
and get_multiplayer_authority() == multiplayer.get_unique_id()
and level_scene.has_method("update_local_inventory_display")
):
level_scene.update_local_inventory_display()
@rpc("any_peer", "call_local", "reliable")
func request_move_item(from_slot: int, to_slot: int, quantity: int = -1):
if not multiplayer.is_server():
return
var requesting_client = multiplayer.get_remote_sender_id()
if not _is_owner_request():
push_warning(
(
"Client "
+ str(requesting_client)
+ " tried to modify inventory for player "
+ str(get_multiplayer_authority())
)
)
return
if not player_inventory:
return
if not player_inventory.is_slot_active(from_slot) or not player_inventory.is_slot_active(to_slot):
push_warning("Invalid slot indices: from=" + str(from_slot) + " to=" + str(to_slot))
return
if quantity != -1 and quantity <= 0:
push_warning("Invalid move quantity: " + str(quantity))
return
var success = false
if quantity == -1:
success = player_inventory.move_item(from_slot, to_slot)
if not success:
success = player_inventory.swap_items(from_slot, to_slot)
else:
success = player_inventory.move_item(from_slot, to_slot, quantity)
if success:
_sync_inventory_to_owner()
@rpc("any_peer", "call_local", "reliable")
func request_add_item(item_id: String, quantity: int = 1):
if not multiplayer.is_server():
return
var requesting_client = multiplayer.get_remote_sender_id()
var is_local_server_call = requesting_client == 0 and multiplayer.get_unique_id() == 1
if requesting_client != 1 and not is_local_server_call:
push_warning(
"Client " + str(requesting_client) + " tried to add items to player " + str(get_multiplayer_authority())
)
return
if not player_inventory:
return
if quantity <= 0:
push_warning("Invalid quantity: " + str(quantity))
return
var item = ItemDatabase.get_item(item_id)
if not item:
push_warning("Item not found: " + item_id)
return
var remaining = player_inventory.add_item(item, quantity)
var added = quantity - remaining
if added > 0:
_sync_inventory_to_owner()
func request_add_single_item(item_id: String) -> bool:
if not multiplayer.is_server():
return false
if player_inventory == null:
return false
var item = ItemDatabase.get_item(item_id)
if not item:
push_warning("Item not found: " + item_id)
return false
var remaining = player_inventory.add_item(item, 1)
if remaining == 0:
_sync_inventory_to_owner()
return true
return false
@rpc("any_peer", "call_local", "reliable")
func request_remove_item(item_id: String, quantity: int = 1):
if not multiplayer.is_server():
return
var requesting_client = multiplayer.get_remote_sender_id()
if not _is_owner_request():
push_warning(
(
"Client "
+ str(requesting_client)
+ " tried to remove items from player "
+ str(get_multiplayer_authority())
)
)
return
if not player_inventory:
return
if quantity <= 0:
push_warning("Invalid quantity: " + str(quantity))
return
var removed = player_inventory.remove_item(item_id, quantity)
if removed > 0:
_sync_inventory_to_owner()
@rpc("authority", "call_local", "reliable")
func add_world_item(scene_path: String, player_position: Vector3) -> void:
var item_container = get_node_or_null("/root/Level/Environment/ItemContainer")
if not item_container:
push_warning("ItemContainer not found at /root/Level/Environment/ItemContainer")
return
if scene_path.is_empty() or not ResourceLoader.exists(scene_path):
push_warning("Cannot add world item: invalid scene path '" + scene_path + "'")
return
var packed_scene = load(scene_path) as PackedScene
if not packed_scene:
push_warning("Cannot add world item: scene path is not a PackedScene '" + scene_path + "'")
return
var instance_item := packed_scene.instantiate() as Node3D
if not instance_item:
push_warning("Cannot add world item: scene root is not a Node3D '" + scene_path + "'")
return
item_container.add_child(instance_item, true)
instance_item.global_position = player_position
func get_inventory() -> PlayerInventory:
return player_inventory
func _sync_inventory_to_owner() -> void:
if not multiplayer.is_server() or not player_inventory:
return
var owner_id = get_multiplayer_authority()
if owner_id == 1:
sync_inventory_to_owner(player_inventory.to_dict())
else:
sync_inventory_to_owner.rpc_id(owner_id, player_inventory.to_dict())
@rpc("any_peer", "call_local", "reliable")
func request_equip_item(from_slot: int, item_type: Item.ItemType) -> void:
if not multiplayer.is_server() or not _is_owner_request():
return
if not player_inventory or not player_inventory.is_slot_active(from_slot):
return
if item_type != Item.ItemType.WEAPON and item_type != Item.ItemType.HAT and item_type != Item.ItemType.BACKPACK:
return
if player_inventory.equip_from_slot(from_slot, item_type):
_sync_inventory_to_owner()
_sync_equipment_appearance()
@rpc("any_peer", "call_local", "reliable")
func request_unequip_item(item_type: Item.ItemType, destination_slot: int = -1) -> void:
if not multiplayer.is_server() or not _is_owner_request():
return
if not player_inventory:
return
if item_type != Item.ItemType.WEAPON and item_type != Item.ItemType.HAT and item_type != Item.ItemType.BACKPACK:
return
if destination_slot < -1 or destination_slot >= PlayerInventory.MAX_INVENTORY_SIZE:
return
if destination_slot >= 0 and not player_inventory.is_slot_active(destination_slot):
return
if player_inventory.unequip_to_slot(item_type, destination_slot):
_sync_inventory_to_owner()
_sync_equipment_appearance()
func _is_owner_request() -> bool:
var sender := multiplayer.get_remote_sender_id()
return sender == get_multiplayer_authority() or (sender == 0 and multiplayer.is_server())
func _sync_equipment_appearance() -> void:
if not multiplayer.is_server() or not player_inventory:
return
var weapon_id := player_inventory.equipped_weapon.item_id
var hat_id := player_inventory.equipped_hat.item_id
var backpack_id := player_inventory.equipped_backpack.item_id
var nickname_height := _calculate_nickname_height(hat_id)
_broadcast_nickname_height(nickname_height)
sync_equipment_appearance.rpc(weapon_id, hat_id, backpack_id)
sync_equipment_appearance(weapon_id, hat_id, backpack_id)
func _request_equipment_appearance() -> void:
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
return
request_equipment_appearance.rpc_id(1)
@rpc("any_peer", "reliable")
func request_equipment_appearance() -> void:
if not multiplayer.is_server() or not player_inventory:
return
var requester_id := multiplayer.get_remote_sender_id()
if requester_id <= 0:
return
if _appearance_sync_requesters.has(requester_id):
return
_appearance_sync_requesters[requester_id] = true
_sync_equipment_appearance_to_peer(requester_id)
func _sync_equipment_appearance_to_peer(peer_id: int) -> void:
if not multiplayer.is_server() or not player_inventory or peer_id <= 0:
return
var weapon_id := player_inventory.equipped_weapon.item_id
var hat_id := player_inventory.equipped_hat.item_id
var backpack_id := player_inventory.equipped_backpack.item_id
sync_equipment_appearance.rpc_id(peer_id, weapon_id, hat_id, backpack_id)
@rpc("any_peer", "reliable")
func sync_equipment_appearance(weapon_id: String, hat_id: String, backpack_id: String) -> void:
var sender := multiplayer.get_remote_sender_id()
if sender != 1 and not (sender == 0 and multiplayer.is_server()):
return
_set_equipment_visibility(weapon_id, hat_id, backpack_id)
func _set_equipment_visibility(weapon_id: String, hat_id: String, backpack_id: String) -> void:
_equipped_hat_visual_id = hat_id
_set_equipment_nodes_visibility(HEAD_EQUIPMENT_PATH, HAT_NODES_BY_ITEM, hat_id)
_set_equipment_nodes_visibility(HAND_EQUIPMENT_PATH, WEAPON_NODES_BY_ITEM, weapon_id)
_set_equipment_nodes_visibility(BACK_EQUIPMENT_PATH, BACKPACK_NODES_BY_ITEM, backpack_id)
func _broadcast_nickname_height(height: float) -> void:
if not multiplayer.is_server():
return
var level_scene := get_tree().get_current_scene()
if level_scene and level_scene.has_method("register_player_nickname_height"):
level_scene.register_player_nickname_height(get_multiplayer_authority(), height)
func _set_equipment_nodes_visibility(parent_path: String, nodes_by_item: Dictionary, equipped_item_id: String) -> void:
for item_id in nodes_by_item:
var equipment := get_node_or_null(parent_path + str(nodes_by_item[item_id])) as Node3D
if equipment:
equipment.visible = item_id == equipped_item_id
func _update_nickname_height(hat_id: String = "") -> void:
if not nickname:
return
nickname.visible = not _is_local_first_person()
_set_nickname_height(_calculate_nickname_height(hat_id))
func _calculate_nickname_height(hat_id: String) -> float:
var target_height := BASE_NICKNAME_HEIGHT
var equipped_hat := _get_hat_node(hat_id)
if equipped_hat:
var hat_top := _get_visual_top(equipped_hat)
if hat_top > -INF and hat_top < INF:
target_height = max(BASE_NICKNAME_HEIGHT, hat_top + nickname_clearance)
return target_height
func _set_nickname_height(height: float) -> void:
var nickname_position := nickname.position
nickname_position.y = height
nickname.position = nickname_position
func apply_synced_nickname_height(height: float) -> void:
if not nickname:
return
nickname.visible = not _is_local_first_person()
if height > -INF and height < INF:
_set_nickname_height(maxf(BASE_NICKNAME_HEIGHT, height))
else:
_set_nickname_height(BASE_NICKNAME_HEIGHT)
func _is_local_first_person() -> bool:
return is_multiplayer_authority() and _spring_arm_offset != null and _spring_arm_offset.is_first_person
func get_current_nickname_height() -> float:
return nickname.position.y if nickname else BASE_NICKNAME_HEIGHT
func _get_hat_node(hat_id: String) -> Node3D:
if HAT_NODES_BY_ITEM.has(hat_id):
var requested_hat_path := HEAD_EQUIPMENT_PATH + str(HAT_NODES_BY_ITEM[hat_id])
var requested_hat := get_node_or_null(requested_hat_path) as Node3D
if requested_hat:
return requested_hat
return null
func _get_visual_top(root: Node3D) -> float:
var visual_top := -INF
if root is MeshInstance3D:
visual_top = _get_mesh_top(root as MeshInstance3D)
for child in root.find_children("*", "MeshInstance3D", true, false):
var mesh_instance := child as MeshInstance3D
if mesh_instance:
visual_top = max(visual_top, _get_mesh_top(mesh_instance))
return visual_top
func _get_mesh_top(mesh_instance: MeshInstance3D) -> float:
var visual_top := -INF
var mesh_bounds := mesh_instance.get_aabb()
for endpoint_index in range(8):
var endpoint_global := mesh_instance.to_global(mesh_bounds.get_endpoint(endpoint_index))
visual_top = max(visual_top, to_local(endpoint_global).y)
return visual_top
func _add_starting_items():
if not player_inventory:
return
var backpack := ItemDatabase.get_item("backpack")
if backpack:
player_inventory.add_item(backpack, 1)
var starting_item_ids: Array[String] = [
"fedora",
"headphones",
"pirate_hat",
"sheriff_hat",
"sombrero",
"wizard_hat",
"sword",
"sword_big",
"axe",
"chicken_leg",
"bone",
"chalice"
]
for item_id in starting_item_ids:
var item = ItemDatabase.get_item(item_id)
if item:
player_inventory.add_item(item, 1)
func pickup() -> void:
if multiplayer.is_server():
request_pickup()
else:
request_pickup.rpc_id(1)
@rpc("any_peer", "call_local", "reliable")
func request_pickup() -> void:
if not multiplayer.is_server() or not _is_owner_request():
return
var now := Time.get_ticks_msec()
if now - _last_server_pickup_request_msec < PICKUP_REQUEST_COOLDOWN_MSEC:
return
_last_server_pickup_request_msec = now
var animation_elapsed := now - _server_pickup_animation_started_msec
if (
_server_pickup_animation_started_msec < 0
or animation_elapsed < PICKUP_ANIMATION_DELAY_MSEC
or animation_elapsed > PICKUP_ANIMATION_WINDOW_MSEC
):
return
if not _is_grounded_on_server():
return
if not _has_collectible_item_in_front():
return
_server_pickup_animation_started_msec = -1
_server_pickup()
func _server_pickup() -> void:
for item in _get_collectible_items_in_front():
var result := request_add_single_item(item.item_id)
if result and item.is_inside_tree():
item.queue_free()
func _has_collectible_item_in_front() -> bool:
return not _get_collectible_items_in_front().is_empty()
func _get_collectible_items_in_front() -> Array[ItemRigidBody3D]:
var collectible_items: Array[ItemRigidBody3D] = []
var pickup_area := get_node_or_null("GodotRobot3D/InfrontArea3D") as Area3D
if not pickup_area:
return collectible_items
for body in pickup_area.get_overlapping_bodies():
var item := body as ItemRigidBody3D
if item and not item.item_id.is_empty() and ItemDatabase.get_item(item.item_id):
collectible_items.append(item)
return collectible_items
@rpc("any_peer", "call_local", "reliable")
func apply_force_to_server_object(object_name: String, normal: Vector3) -> void:
var object_node = get_node_or_null("/root/Level/Environment/ItemContainer")
if object_node:
for n in object_node.get_children():
if n.name == object_name and n is RigidBody3D:
n.apply_force(normal * 100)
func _is_grounded_on_server() -> bool:
if not multiplayer.is_server() or not is_inside_tree():
return false
var query := PhysicsRayQueryParameters3D.new()
query.from = global_position + Vector3.UP * 0.15
query.to = global_position + Vector3.DOWN * 0.3
query.collision_mask = 2
query.exclude = [get_rid()]
query.collide_with_areas = false
return not get_world_3d().direct_space_state.intersect_ray(query).is_empty()
+1
View File
@@ -0,0 +1 @@
uid://c2si8gkbnde0c
+86
View File
@@ -0,0 +1,86 @@
class_name SpringArmCharacter
extends Node3D
signal perspective_changed(first_person: bool)
const MOUSE_SENSIBILITY: float = 0.005
@export_category("Objects")
@export var _spring_arm: SpringArm3D = null
@export var _first_person_anchor: Node3D = null
@export_category("Camera Perspective")
@export_range(0.0, 10.0, 0.05) var third_person_distance := 5.0
@export_range(0.0, 1.0, 0.05) var first_person_distance := 0.0
@export_range(0.0, 3.0, 0.05) var third_person_height := 2.0
@export_range(1.0, 179.0, 1.0) var third_person_fov := 75.0
@export_range(1.0, 179.0, 1.0) var first_person_fov := 90.0
var is_first_person := false
var _yaw := 0.0
var _pitch := 0.0
@onready var _camera: Camera3D = _spring_arm.get_node_or_null("Camera3D") as Camera3D
func _ready() -> void:
_yaw = rotation.y
_pitch = _spring_arm.rotation.x if _spring_arm else 0.0
_apply_axis_lock()
_apply_perspective()
func _process(_delta: float) -> void:
_apply_axis_lock()
if is_first_person and _spring_arm and _first_person_anchor:
_spring_arm.global_position = _first_person_anchor.global_position
func _unhandled_input(event: InputEvent) -> void:
if not is_multiplayer_authority():
return
var current_scene := get_tree().get_current_scene()
if current_scene and current_scene.has_method("is_camera_input_blocked"):
if current_scene.is_camera_input_blocked():
return
if event.is_action_pressed("toggle_camera"):
is_first_person = not is_first_person
_apply_perspective()
perspective_changed.emit(is_first_person)
get_viewport().set_input_as_handled()
return
if not (event is InputEventMouseMotion):
return
if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
return
_yaw = wrapf(_yaw - event.relative.x * MOUSE_SENSIBILITY, -PI, PI)
_pitch = clampf(_pitch - event.relative.y * MOUSE_SENSIBILITY, -PI / 4.0, PI / 24.0)
_apply_axis_lock()
func _apply_axis_lock() -> void:
rotation = Vector3(0.0, _yaw, 0.0)
if _spring_arm:
_spring_arm.rotation = Vector3(_pitch, 0.0, 0.0)
if _camera:
_camera.rotation = Vector3.ZERO
func _apply_perspective() -> void:
if not _spring_arm:
return
if is_first_person:
_spring_arm.spring_length = first_person_distance
if _first_person_anchor:
_spring_arm.global_position = _first_person_anchor.global_position
if _camera:
_camera.fov = first_person_fov
else:
_spring_arm.spring_length = third_person_distance
_spring_arm.position = Vector3(0.0, third_person_height, 0.0)
if _camera:
_camera.fov = third_person_fov
+1
View File
@@ -0,0 +1 @@
uid://bj7yrijm7bppq