161 lines
6.6 KiB
C#
161 lines
6.6 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace _Project.Scripts.PlayerUtils
|
||
{
|
||
public class Projectile : MonoBehaviour
|
||
{
|
||
private Vector3 _direction;
|
||
private float _speed;
|
||
private float _damage;
|
||
private float _maxDistance;
|
||
private float _radius;
|
||
private bool _pierce;
|
||
private bool _firedByPlayer;
|
||
private GameObject _owner;
|
||
private LayerMask _hitMask;
|
||
private GameObject _impactPrefab;
|
||
|
||
private float _traveledDistance;
|
||
private readonly HashSet<IDamageable> _hitTargets = new HashSet<IDamageable>();
|
||
|
||
// Фабрика: создаёт красивый светящийся снаряд со шлейфом прямо из кода!
|
||
public static void Spawn(
|
||
Vector3 position,
|
||
Vector3 direction,
|
||
BodyProfile profile,
|
||
bool firedByPlayer,
|
||
GameObject owner,
|
||
LayerMask hitMask,
|
||
GameObject impactPrefab = null)
|
||
{
|
||
var go = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
||
go.name = firedByPlayer ? "PlayerProjectile" : "EnemyProjectile";
|
||
go.transform.position = position;
|
||
go.transform.localScale = Vector3.one * profile.projectileSize;
|
||
|
||
// Убираем стандартный коллайдер, так как проверяем полёт точным SphereCast
|
||
var col = go.GetComponent<Collider>();
|
||
if (col != null) Destroy(col);
|
||
|
||
// Делаем снаряд ярко светящимся (хорошо видно в полёте для уворотов!)
|
||
var rend = go.GetComponent<Renderer>();
|
||
if (rend != null)
|
||
{
|
||
var mat = new Material(Shader.Find("Sprites/Default"));
|
||
mat.color = profile.projectileColor;
|
||
rend.material = mat;
|
||
rend.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||
}
|
||
|
||
// Добавляем красивый шлейф (трассер) за снарядом
|
||
var trail = go.AddComponent<TrailRenderer>();
|
||
trail.time = 0.18f;
|
||
trail.startWidth = profile.projectileSize * 0.85f;
|
||
trail.endWidth = 0f;
|
||
trail.material = new Material(Shader.Find("Sprites/Default"));
|
||
trail.startColor = profile.projectileColor;
|
||
trail.endColor = new Color(profile.projectileColor.r, profile.projectileColor.g, profile.projectileColor.b, 0f);
|
||
trail.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||
|
||
var proj = go.AddComponent<Projectile>();
|
||
proj._direction = direction.normalized;
|
||
proj._speed = profile.projectileSpeed;
|
||
proj._damage = firedByPlayer ? profile.damage : profile.damage * 0.5f;
|
||
proj._maxDistance = profile.weaponRange;
|
||
proj._radius = profile.projectileSize * 0.45f;
|
||
proj._pierce = profile.pierceTargets;
|
||
proj._firedByPlayer = firedByPlayer;
|
||
proj._owner = owner;
|
||
proj._hitMask = hitMask;
|
||
proj._impactPrefab = impactPrefab;
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
// Снаряды летят в мировом времени (Time.deltaTime), поэтому в форме Духа
|
||
// они замедляются, позволяя игроку красиво уворачиваться от них!
|
||
float step = _speed * Time.deltaTime;
|
||
if (step <= 0.0001f) return;
|
||
|
||
if (_pierce)
|
||
{
|
||
// Пробивной снаряд Снайпера
|
||
var hits = Physics.SphereCastAll(transform.position, _radius, _direction, step, _hitMask, QueryTriggerInteraction.Ignore);
|
||
foreach (var hit in hits)
|
||
{
|
||
if (ShouldIgnore(hit.collider)) continue;
|
||
|
||
var dmg = hit.collider.GetComponentInParent<IDamageable>();
|
||
if (dmg != null)
|
||
{
|
||
if (_hitTargets.Add(dmg))
|
||
{
|
||
dmg.TakeDamage(_damage);
|
||
SpawnImpact(hit);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Упёрлись в стену/пол — уничтожаем снаряд
|
||
SpawnImpact(hit);
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Обычный снаряд: до первого попадания
|
||
if (Physics.SphereCast(transform.position, _radius, _direction, out RaycastHit hit, step, _hitMask, QueryTriggerInteraction.Ignore))
|
||
{
|
||
if (!ShouldIgnore(hit.collider))
|
||
{
|
||
var dmg = hit.collider.GetComponentInParent<IDamageable>();
|
||
if (dmg != null)
|
||
{
|
||
dmg.TakeDamage(_damage);
|
||
}
|
||
else if (hit.rigidbody != null)
|
||
{
|
||
hit.rigidbody.AddForceAtPosition(_direction * (_damage * 8f), hit.point, ForceMode.Impulse);
|
||
}
|
||
|
||
SpawnImpact(hit);
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
transform.position += _direction * step;
|
||
_traveledDistance += step;
|
||
|
||
if (_traveledDistance >= _maxDistance)
|
||
{
|
||
Destroy(gameObject);
|
||
}
|
||
}
|
||
|
||
private bool ShouldIgnore(Collider col)
|
||
{
|
||
if (_owner != null && (col.gameObject == _owner || col.transform.root.gameObject == _owner))
|
||
return true;
|
||
|
||
// Чтобы враги случайно не расстреливали друг друга в спину, их снаряды бьют только игрока и стены
|
||
if (!_firedByPlayer && col.GetComponentInParent<EnemyAI>() != null)
|
||
return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
private void SpawnImpact(RaycastHit hit)
|
||
{
|
||
if (_impactPrefab != null)
|
||
{
|
||
var imp = Instantiate(_impactPrefab, hit.point + hit.normal * 0.01f, Quaternion.LookRotation(hit.normal));
|
||
Destroy(imp, 2.5f);
|
||
}
|
||
}
|
||
}
|
||
} |