Files
Learning-UnityNGO/Assets/CONTENT/FEATURES/DEV-TEST/Scripts/Clone/GhostPlayer.cs
T

129 lines
5.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}