202 lines
5.3 KiB
C#
202 lines
5.3 KiB
C#
|
|
using System.Collections;
|
||
|
|
using UnityEngine;
|
||
|
|
|
||
|
|
public class Turret : MonoBehaviour
|
||
|
|
{
|
||
|
|
[SerializeField] Transform turretBase; // horizontal rotation
|
||
|
|
[SerializeField] Transform cannon; // vertical rotation
|
||
|
|
[SerializeField] Transform[] barrels;
|
||
|
|
[SerializeField] float rotationSpeed = 5f;
|
||
|
|
[SerializeField] float turretRange = 10f;
|
||
|
|
[SerializeField] float fireRate = 1f;
|
||
|
|
[SerializeField] int damage = 10;
|
||
|
|
[SerializeField] bool isEnabled = true;
|
||
|
|
[SerializeField] float smoothTime = 0.2f;
|
||
|
|
[SerializeField] float projectileSpeed = 30f;
|
||
|
|
[SerializeField] GameObject hitVFXPrefab;
|
||
|
|
private Quaternion baseStartRot;
|
||
|
|
private Quaternion cannonStartRot;
|
||
|
|
float baseYawVelocity;
|
||
|
|
float cannonPitchVelocity;
|
||
|
|
|
||
|
|
Transform target;
|
||
|
|
float fireCooldown;
|
||
|
|
|
||
|
|
void Awake()
|
||
|
|
{
|
||
|
|
baseStartRot = turretBase.localRotation;
|
||
|
|
cannonStartRot = cannon.localRotation;
|
||
|
|
|
||
|
|
}
|
||
|
|
void Update()
|
||
|
|
{
|
||
|
|
if (!isEnabled) return;
|
||
|
|
|
||
|
|
FindTarget();
|
||
|
|
|
||
|
|
if (target == null)
|
||
|
|
{
|
||
|
|
ResetTurret();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
RotateTurret();
|
||
|
|
HandleShoot();
|
||
|
|
}
|
||
|
|
|
||
|
|
void FindTarget()
|
||
|
|
{
|
||
|
|
Collider[] hits = Physics.OverlapSphere(transform.position, turretRange);
|
||
|
|
|
||
|
|
float closestDist = Mathf.Infinity;
|
||
|
|
Transform closest = null;
|
||
|
|
|
||
|
|
foreach (var hit in hits)
|
||
|
|
{
|
||
|
|
EnemyHealth enemy = hit.GetComponent<EnemyHealth>();
|
||
|
|
if (enemy == null) continue;
|
||
|
|
|
||
|
|
float dist = Vector3.Distance(transform.position, hit.transform.position);
|
||
|
|
if (dist < closestDist)
|
||
|
|
{
|
||
|
|
closestDist = dist;
|
||
|
|
closest = hit.transform;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
target = closest;
|
||
|
|
}
|
||
|
|
|
||
|
|
void RotateTurret()
|
||
|
|
{
|
||
|
|
Vector3 predicted = PredictTargetPosition();
|
||
|
|
Vector3 dir = predicted - turretBase.position;
|
||
|
|
|
||
|
|
// Horizontal rotation
|
||
|
|
Vector3 flatDir = new Vector3(dir.x, 0f, dir.z);
|
||
|
|
float targetYaw = Quaternion.LookRotation(flatDir).eulerAngles.y;
|
||
|
|
|
||
|
|
float newYaw = Mathf.SmoothDampAngle(
|
||
|
|
turretBase.eulerAngles.y,
|
||
|
|
targetYaw,
|
||
|
|
ref baseYawVelocity,
|
||
|
|
smoothTime
|
||
|
|
);
|
||
|
|
|
||
|
|
turretBase.rotation = Quaternion.Euler(0f, newYaw, 0f);
|
||
|
|
|
||
|
|
// Vertical rotation
|
||
|
|
float targetPitch = Quaternion.LookRotation(dir).eulerAngles.x;
|
||
|
|
|
||
|
|
float newPitch = Mathf.SmoothDampAngle(
|
||
|
|
cannon.localEulerAngles.x,
|
||
|
|
targetPitch,
|
||
|
|
ref cannonPitchVelocity,
|
||
|
|
smoothTime
|
||
|
|
);
|
||
|
|
|
||
|
|
cannon.localRotation = Quaternion.Euler(newPitch, 0f, 0f);
|
||
|
|
}
|
||
|
|
|
||
|
|
void HandleShoot()
|
||
|
|
{
|
||
|
|
fireCooldown -= Time.deltaTime;
|
||
|
|
if (fireCooldown > 0f) return;
|
||
|
|
|
||
|
|
fireCooldown = 1f / fireRate;
|
||
|
|
|
||
|
|
// 1. Play muzzle flashes on all barrels
|
||
|
|
foreach (var barrel in barrels)
|
||
|
|
{
|
||
|
|
var flash = barrel.GetComponentInChildren<ParticleSystem>();
|
||
|
|
flash?.Play();
|
||
|
|
|
||
|
|
StartCoroutine(BarrelRecoil(barrel));
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Raycast from cannon forward (or predicted aim point)
|
||
|
|
RaycastHit hit;
|
||
|
|
Vector3 fireOrigin = cannon.position;
|
||
|
|
Vector3 predicted = PredictTargetPosition();
|
||
|
|
Vector3 fireDirection = (predicted - fireOrigin).normalized;
|
||
|
|
|
||
|
|
if (Physics.Raycast(fireOrigin, fireDirection, out hit, turretRange))
|
||
|
|
{
|
||
|
|
// 4. Spawn hit VFX
|
||
|
|
if (hitVFXPrefab != null)
|
||
|
|
Instantiate(hitVFXPrefab, hit.point, Quaternion.identity);
|
||
|
|
|
||
|
|
// 5. Apply damage
|
||
|
|
EnemyHealth enemyHealth = hit.transform.GetComponent<EnemyHealth>();
|
||
|
|
enemyHealth?.TakeDamage(damage);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Activate()
|
||
|
|
{
|
||
|
|
isEnabled = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Deactivate()
|
||
|
|
{
|
||
|
|
isEnabled = false;
|
||
|
|
target = null;
|
||
|
|
}
|
||
|
|
Vector3 PredictTargetPosition()
|
||
|
|
{
|
||
|
|
Rigidbody rb = target.GetComponent<Rigidbody>();
|
||
|
|
if (rb == null) return target.position;
|
||
|
|
|
||
|
|
Vector3 targetPos = target.position;
|
||
|
|
Vector3 targetVel = rb.linearVelocity;
|
||
|
|
|
||
|
|
Vector3 dir = targetPos - cannon.position;
|
||
|
|
float distance = dir.magnitude;
|
||
|
|
|
||
|
|
float timeToHit = distance / projectileSpeed;
|
||
|
|
|
||
|
|
return targetPos + targetVel * timeToHit;
|
||
|
|
}
|
||
|
|
void ResetTurret()
|
||
|
|
{
|
||
|
|
// Smoothly return to idle rotation
|
||
|
|
float smooth = rotationSpeed / 5 * Time.deltaTime;
|
||
|
|
|
||
|
|
turretBase.localRotation = Quaternion.Lerp(
|
||
|
|
turretBase.localRotation,
|
||
|
|
baseStartRot,
|
||
|
|
smooth
|
||
|
|
);
|
||
|
|
|
||
|
|
cannon.localRotation = Quaternion.Lerp(
|
||
|
|
cannon.localRotation,
|
||
|
|
cannonStartRot,
|
||
|
|
smooth
|
||
|
|
);
|
||
|
|
}
|
||
|
|
IEnumerator BarrelRecoil(Transform barrel)
|
||
|
|
{
|
||
|
|
Vector3 startPos = barrel.localPosition;
|
||
|
|
Vector3 recoilPos = startPos + new Vector3(0f, 0f, -0.2f); // adjust recoil distance
|
||
|
|
|
||
|
|
float t = 0f;
|
||
|
|
float recoilTime = 0.05f;
|
||
|
|
|
||
|
|
// Move backward
|
||
|
|
while (t < 1f)
|
||
|
|
{
|
||
|
|
t += Time.deltaTime / recoilTime;
|
||
|
|
barrel.localPosition = Vector3.Lerp(startPos, recoilPos, t);
|
||
|
|
yield return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Move forward
|
||
|
|
t = 0f;
|
||
|
|
while (t < 1f)
|
||
|
|
{
|
||
|
|
t += Time.deltaTime / recoilTime;
|
||
|
|
barrel.localPosition = Vector3.Lerp(recoilPos, startPos, t);
|
||
|
|
yield return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|