129 lines
5.2 KiB
C#
129 lines
5.2 KiB
C#
using Unity.Netcode;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// Висит на префабе КЛОНА. Воспроизводит запись: двигает трансформ по сэмплам
|
||
/// и повторяет выстрелы той же логикой, что PlayerShoot.
|
||
/// Вся логика — на сервере, на клиенты позиция уезжает через серверный NetworkTransform.
|
||
/// </summary>
|
||
public class GhostPlayer : NetworkBehaviour
|
||
{
|
||
[Header("Combat (как у PlayerShoot)")]
|
||
[SerializeField] private float damageAmount = 10f;
|
||
[SerializeField] private float range = 100f;
|
||
[SerializeField] private GameObject hitEffectPrefab;
|
||
|
||
[Header("Visuals")]
|
||
[Tooltip("Тусклый цвет СВОЕГО клона (клон локального игрока)")]
|
||
[SerializeField] private Color ownGhostColor = new Color(0f, 0f, 0.5f, 1f);
|
||
[Tooltip("Тусклый цвет ЧУЖОГО клона (клон соперника)")]
|
||
[SerializeField] private Color enemyGhostColor = new Color(0.5f, 0f, 0f, 1f);
|
||
|
||
// ClientId оригинального игрока. Синхронизируется на клиентов для окраски
|
||
// (recording живёт только на сервере, поэтому цвет по нему клиент не узнает).
|
||
private NetworkVariable<ulong> ownerId = new NetworkVariable<ulong>(ulong.MaxValue);
|
||
|
||
private GhostRecording recording;
|
||
private int shotIdx;
|
||
private bool finished;
|
||
|
||
private Collider[] ownColliders;
|
||
private PlayerHealth health;
|
||
private PlayerShield shield;
|
||
|
||
/// <summary> Кому засчитывать вход в яму / фраги — ClientId оригинального игрока. </summary>
|
||
public ulong OriginalOwnerId => ownerId.Value;
|
||
|
||
public override void OnNetworkSpawn()
|
||
{
|
||
ownerId.OnValueChanged += OnOwnerChanged;
|
||
ApplyColor();
|
||
}
|
||
|
||
public override void OnNetworkDespawn()
|
||
{
|
||
ownerId.OnValueChanged -= OnOwnerChanged;
|
||
}
|
||
|
||
private void OnOwnerChanged(ulong _previous, ulong _current) => ApplyColor();
|
||
|
||
// Свой клон — тусклый синий, чужой — тусклый красный (как живые игроки, но тускло)
|
||
private void ApplyColor()
|
||
{
|
||
bool _isMine = NetworkManager.Singleton != null
|
||
&& ownerId.Value == NetworkManager.Singleton.LocalClientId;
|
||
|
||
Color _color = _isMine ? ownGhostColor : enemyGhostColor;
|
||
|
||
foreach (MeshRenderer _renderer in GetComponentsInChildren<MeshRenderer>())
|
||
_renderer.material.color = _color;
|
||
}
|
||
|
||
/// <summary> Вызывается на сервере сразу после NetworkObject.Spawn(). </summary>
|
||
public void Init(GhostRecording _recording)
|
||
{
|
||
recording = _recording;
|
||
shotIdx = 0;
|
||
finished = false;
|
||
|
||
ownerId.Value = _recording.ownerClientId;
|
||
|
||
ownColliders = GetComponentsInChildren<Collider>();
|
||
|
||
health = GetComponent<PlayerHealth>();
|
||
shield = GetComponent<PlayerShield>();
|
||
// Init(true): мы на сервере. Без этого TakeDamage у клона молча не работает
|
||
health.Init(true);
|
||
}
|
||
|
||
/// <summary> Тик приходит от GhostRoundManager — общий счётчик для всех клонов. </summary>
|
||
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];
|
||
|
||
if (shield != null && _tick < recording.shieldStates.Count)
|
||
shield.SetShieldState(recording.shieldStates[_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;
|
||
|
||
PlayerHealth _targetHealth = _hit.collider.GetComponentInParent<PlayerHealth>();
|
||
if (_targetHealth != null)
|
||
_targetHealth.TakeDamage((int)damageAmount);
|
||
|
||
SpawnHitEffectClientRpc(_hit.point);
|
||
}
|
||
}
|
||
|
||
[ClientRpc]
|
||
private void SpawnHitEffectClientRpc(Vector3 _hitPosition)
|
||
{
|
||
if (hitEffectPrefab != null)
|
||
Instantiate(hitEffectPrefab, _hitPosition, Quaternion.identity);
|
||
}
|
||
} |