using Unity.Netcode; using UnityEngine; /// /// Висит на префабе КЛОНА. Воспроизводит запись: двигает трансформ по сэмплам /// и повторяет выстрелы той же логикой, что PlayerShoot. /// Вся логика — на сервере, на клиенты позиция уезжает через серверный NetworkTransform. /// public class GhostPlayer : NetworkBehaviour { [Header("Combat (как у PlayerShoot)")] [SerializeField] private float damageAmount = 10f; [SerializeField] private float range = 100f; [SerializeField] private GameObject hitEffectPrefab; [Header("Visuals")] [SerializeField] private Color ghostColor = new Color(0.5f, 0.5f, 0.5f, 1f); private GhostRecording recording; private int shotIdx; private bool finished; private Collider[] ownColliders; private PlayerHealth health; /// Кому засчитывать вход в яму / фраги — ClientId оригинального игрока. public ulong OriginalOwnerId => recording != null ? recording.ownerClientId : ulong.MaxValue; public override void OnNetworkSpawn() { // Красим клона на всех клиентах, чтобы отличался от живых игроков foreach (MeshRenderer _renderer in GetComponentsInChildren()) _renderer.material.color = ghostColor; } /// Вызывается на сервере сразу после NetworkObject.Spawn(). public void Init(GhostRecording _recording) { recording = _recording; shotIdx = 0; finished = false; ownColliders = GetComponentsInChildren(); health = GetComponent(); // Init(true): мы на сервере. Без этого TakeDamage у клона молча не работает health.Init(true); } /// Тик приходит от GhostRoundManager — общий счётчик для всех клонов. public void Step(int _tick) { if (!IsServer || recording == null || finished) return; // Клона убили — замирает (визуал/коллайдеры выключил PlayerHealth своим ClientRpc) if (health != null && health.isDead.Value) return; // Запись кончилась (в том раунде игрок умер/раунд был короче) — исчезаем if (_tick >= recording.positions.Count) { finished = true; if (NetworkObject.IsSpawned) NetworkObject.Despawn(); return; } transform.position = recording.positions[_tick]; transform.rotation = recording.rotations[_tick]; while (shotIdx < recording.shots.Count && recording.shots[shotIdx].tick <= _tick) { ReplayShot(recording.shots[shotIdx]); shotIdx++; } } // Та же логика, что PlayerShoot.ShootServerRpc — стреляет по ТЕКУЩЕМУ миру private void ReplayShot(GhostShotEvent _shot) { if (Physics.Raycast(_shot.origin, _shot.direction, out RaycastHit _hit, range)) { if (System.Array.Exists(ownColliders, c => c == _hit.collider)) return; if (_hit.collider.TryGetComponent(out PlayerHealth _targetHealth)) _targetHealth.TakeDamage((int)damageAmount); SpawnHitEffectClientRpc(_hit.point); } } [ClientRpc] private void SpawnHitEffectClientRpc(Vector3 _hitPosition) { if (hitEffectPrefab != null) Instantiate(hitEffectPrefab, _hitPosition, Quaternion.identity); } }