class_name Ghost extends Node3D # Чужой игрок: киберниндзя со скелетом. Позиция и поворот плавно догоняют последнее полученное состояние, # анимация выбирается по флагам и скорости, которые прислал игрок (скорость считаем сами по смещению). # Свечение модели красится в цвет игрока (от его id) — так игроков отличают друг от друга. # load, не preload: скрипт лежит и на выделенном сервере, а модели там нет (сервер Ghost не создаёт) const MODEL_PATH = "res://player/character/character.glb" const SMOOTHING = 14.0 const FEET_OFFSET = -1.16 # Ноги относительно центра капсулы игрока const BLEND = 0.15 # Сглаживание между анимациями, сек const GLOW_ENERGY = 1.0 const LOOPED = ["idle", "walk", "run", "crouch_idle", "crouch_walk", "slide", "jump", "fall", "dash", "wallrun_R", "wallrun_L"] # Скорость, на которой анимация шагает без скольжения ног; реальную скорость приводим к ней через speed_scale const REF_SPEED = {"walk": 2.0, "run": 6.1, "crouch_walk": 1.4, "wallrun_R": 5.9, "wallrun_L": 5.9} const STAND_SPEED = 0.8 # Медленнее — стоит const RUN_SPEED = 4.5 # Быстрее — бежит, медленнее — идёт const SLIDE_SPEED = 6.5 # В приседе быстрее — подкат const AIR_JUMP_KICK = 5.0 # Рывок вертикальной скорости в воздухе — двойной прыжок const AIR_JUMP_TIME = 0.6 # Длительность анимации двойного прыжка (сальто) const HEAD_PITCH_SHARE = 0.7 # Какую долю наклона головы игрока повторяет модель var id := 0 var color := Color.WHITE var target_pos := Vector3.ZERO var target_yaw := 0.0 var pitch := 0.0 var flags := 0 var velocity := Vector3.ZERO # Сглаженная скорость по сетевым пакетам, м/с var anim: AnimationPlayer var skeleton: Skeleton3D var head_bone := -1 var _last_msec := 0 var _raw_vy := 0.0 var _air_time := 0.0 var _air_jump_left := 0.0 var _current := "" func setup(peer_id: int, pos: Vector3) -> void: id = peer_id name = "Ghost_%d" % id position = pos target_pos = pos color = Color.from_hsv(fmod(id * 0.618034, 1.0), 0.7, 1.0) _last_msec = Time.get_ticks_msec() var model := (load(MODEL_PATH) as PackedScene).instantiate() as Node3D model.position.y = FEET_OFFSET add_child(model) skeleton = model.find_child("Skeleton3D", true, false) as Skeleton3D anim = model.find_child("AnimationPlayer", true, false) as AnimationPlayer head_bone = skeleton.find_bone("Head") for n in LOOPED: anim.get_animation(n).loop_mode = Animation.LOOP_LINEAR anim.callback_mode_process = AnimationMixer.ANIMATION_CALLBACK_MODE_PROCESS_MANUAL # кадры двигаем сами (см. _process) _paint_glow(model) var label := Label3D.new() label.text = "Игрок %d" % (id % 1000) label.modulate = color label.billboard = BaseMaterial3D.BILLBOARD_ENABLED label.no_depth_test = true label.position = Vector3(0, 1.5, 0) label.pixel_size = 0.005 add_child(label) # Материал «Glow» модели заменяем своим — с цветом игрока (общий материал модели остался бы одним на всех) func _paint_glow(model: Node) -> void: var mat := StandardMaterial3D.new() mat.albedo_color = Color.BLACK mat.emission_enabled = true mat.emission = color mat.emission_energy_multiplier = GLOW_ENERGY for mi: MeshInstance3D in model.find_children("*", "MeshInstance3D", true, false): for s in mi.mesh.get_surface_count(): var m := mi.mesh.surface_get_material(s) if m != null and m.resource_name == "Glow": mi.set_surface_override_material(s, mat) func set_target(pos: Vector3, yaw: float, new_pitch: float, new_flags: int) -> void: var now := Time.get_ticks_msec() var dt := (now - _last_msec) / 1000.0 _last_msec = now if dt > 0.005: var raw := (pos - target_pos) / dt velocity = velocity.lerp(raw, 0.5) # Двойной прыжок: уже летим какое-то время и вертикальная скорость резко выросла if (new_flags & Net.FLAG_AIR) != 0 and _air_time > 0.15 and raw.y - _raw_vy > AIR_JUMP_KICK: _air_jump_left = AIR_JUMP_TIME _raw_vy = raw.y target_pos = pos target_yaw = yaw pitch = new_pitch flags = new_flags # Какая анимация подходит текущему состоянию func _choose(speed: float) -> String: if Net.dead_peers.has(id): return "death" if (flags & Net.FLAG_MANTLE) != 0: return "mantle" if (flags & Net.FLAG_DASH) != 0: return "dash" if (flags & Net.FLAG_WALLRUN) != 0: return "wallrun_R" if (flags & Net.FLAG_WALL_RIGHT) != 0 else "wallrun_L" if (flags & Net.FLAG_AIR) != 0: if _air_jump_left > 0.0: return "air_jump" return "jump" if velocity.y > 0.5 else "fall" if (flags & Net.FLAG_SLIDING) != 0: # присед и подкат приходят одним флагом if speed > SLIDE_SPEED: return "slide" return "crouch_walk" if speed > STAND_SPEED else "crouch_idle" if speed > RUN_SPEED: return "run" return "walk" if speed > STAND_SPEED else "idle" func _process(delta: float) -> void: var t := 1.0 - exp(-SMOOTHING * delta) position = position.lerp(target_pos, t) rotation.y = lerp_angle(rotation.y, target_yaw, t) _air_time = _air_time + delta if (flags & Net.FLAG_AIR) != 0 else 0.0 _air_jump_left = maxf(_air_jump_left - delta, 0.0) var speed := Vector2(velocity.x, velocity.z).length() var wanted := _choose(speed) if wanted != _current: _current = wanted anim.play(wanted, BLEND) anim.speed_scale = clampf(speed / REF_SPEED[wanted], 0.5, 2.0) if REF_SPEED.has(wanted) else 1.0 anim.advance(delta) # Голова повторяет взгляд игрока вверх-вниз (только пока анимация идёт: иначе наклон копился бы с кадра на кадр) if head_bone >= 0 and anim.is_playing(): var q := skeleton.get_bone_pose_rotation(head_bone) var tilt := clampf(pitch * HEAD_PITCH_SHARE, -0.9, 0.9) skeleton.set_bone_pose_rotation(head_bone, q * Quaternion(Vector3.RIGHT, tilt))