Added UI and more

This commit is contained in:
2026-07-02 17:04:29 +03:00
parent eca9be620a
commit 8ac38208ae
190 changed files with 555483 additions and 101 deletions
@@ -0,0 +1,96 @@
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")]
[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;
/// <summary> Кому засчитывать вход в яму / фраги — ClientId оригинального игрока. </summary>
public ulong OriginalOwnerId => recording != null ? recording.ownerClientId : ulong.MaxValue;
public override void OnNetworkSpawn()
{
// Красим клона на всех клиентах, чтобы отличался от живых игроков
foreach (MeshRenderer _renderer in GetComponentsInChildren<MeshRenderer>())
_renderer.material.color = ghostColor;
}
/// <summary> Вызывается на сервере сразу после NetworkObject.Spawn(). </summary>
public void Init(GhostRecording _recording)
{
recording = _recording;
shotIdx = 0;
finished = false;
ownColliders = GetComponentsInChildren<Collider>();
health = GetComponent<PlayerHealth>();
// 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];
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);
}
}