421 lines
13 KiB
C#
421 lines
13 KiB
C#
using UnityEngine;
|
|
|
|
public class SimpleGun : MonoBehaviour
|
|
{
|
|
[Header("Gun Settings")]
|
|
public float damage = 25f;
|
|
public float range = 100f;
|
|
public float fireRate = 8f;
|
|
public int maxAmmo = 30;
|
|
public int currentAmmo;
|
|
public bool isAutomatic = true;
|
|
|
|
[Header("Recoil Settings")]
|
|
public float recoilKickback = 0.08f;
|
|
public float recoilKickUp = 0.04f;
|
|
public float recoilRecoverySpeed = 12f;
|
|
|
|
[Header("Weapon Bob Settings")]
|
|
public float bobFrequency = 10f;
|
|
public float bobHorizontalAmplitude = 0.05f;
|
|
public float bobVerticalAmplitude = 0.03f;
|
|
public float sprintBobMultiplier = 1.5f;
|
|
public float bobReturnSpeed = 6f;
|
|
|
|
[Header("References")]
|
|
public Camera fpsCam;
|
|
|
|
// Muzzle flash
|
|
private GameObject muzzleFlashObj;
|
|
private Light muzzleLight;
|
|
private float muzzleFlashTimer = 0f;
|
|
private float muzzleFlashDuration = 0.05f;
|
|
|
|
// Impact
|
|
private static int maxDecals = 30;
|
|
private static GameObject[] decalPool;
|
|
private static int decalIndex = 0;
|
|
|
|
// Recoil
|
|
private Vector3 originalLocalPos;
|
|
private Vector3 recoilOffset;
|
|
|
|
// Bob
|
|
private float bobTimer = 0f;
|
|
private Vector3 bobOffset;
|
|
private CharacterController playerController;
|
|
|
|
// Fire timing
|
|
private float nextTimeToFire = 0f;
|
|
|
|
void Start()
|
|
{
|
|
currentAmmo = maxAmmo;
|
|
|
|
if (fpsCam == null)
|
|
fpsCam = Camera.main;
|
|
if (fpsCam == null)
|
|
fpsCam = GetComponentInParent<Camera>();
|
|
|
|
playerController = GetComponentInParent<CharacterController>();
|
|
originalLocalPos = transform.localPosition;
|
|
|
|
// Strip colliders from all children so they don't block the CharacterController
|
|
StripChildColliders();
|
|
|
|
// NUCLEAR OPTION: Remove ANY collider on this object or children
|
|
Collider[] allColliders = GetComponentsInChildren<Collider>();
|
|
foreach (Collider col in allColliders)
|
|
{
|
|
if (!(col is CharacterController))
|
|
{
|
|
Debug.LogWarning($"Found collider on {col.gameObject.name} - REMOVING IT!");
|
|
Destroy(col);
|
|
}
|
|
}
|
|
|
|
// Only create a procedural gun if there's no existing model geometry
|
|
if (GetComponentsInChildren<MeshRenderer>().Length == 0)
|
|
CreateSimpleGunModel();
|
|
|
|
CreateMuzzleFlash();
|
|
InitDecalPool();
|
|
}
|
|
|
|
void StripChildColliders()
|
|
{
|
|
// Only strip colliders from direct children of the gun - never destroy CharacterControllers
|
|
foreach (Transform child in transform)
|
|
{
|
|
Collider col = child.GetComponent<Collider>();
|
|
if (col != null && !(col is CharacterController))
|
|
{
|
|
Destroy(col);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
// Shooting
|
|
if (isAutomatic ? Input.GetButton("Fire1") : Input.GetButtonDown("Fire1"))
|
|
{
|
|
if (Time.time >= nextTimeToFire)
|
|
{
|
|
nextTimeToFire = Time.time + 1f / fireRate;
|
|
Shoot();
|
|
}
|
|
}
|
|
|
|
// Reload
|
|
if (Input.GetKeyDown(KeyCode.R))
|
|
Reload();
|
|
|
|
// Recover from recoil
|
|
recoilOffset = Vector3.Lerp(recoilOffset, Vector3.zero, Time.deltaTime * recoilRecoverySpeed);
|
|
|
|
// Weapon bob
|
|
UpdateBob();
|
|
|
|
// Apply combined position: original + recoil + bob
|
|
transform.localPosition = originalLocalPos + recoilOffset + bobOffset;
|
|
|
|
// Muzzle flash timer
|
|
if (muzzleFlashTimer > 0f)
|
|
{
|
|
muzzleFlashTimer -= Time.deltaTime;
|
|
if (muzzleFlashTimer <= 0f)
|
|
{
|
|
if (muzzleFlashObj != null) muzzleFlashObj.SetActive(false);
|
|
if (muzzleLight != null) muzzleLight.enabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UpdateBob()
|
|
{
|
|
if (playerController == null)
|
|
{
|
|
bobOffset = Vector3.zero;
|
|
return;
|
|
}
|
|
|
|
Vector3 horizontalVelocity = new Vector3(playerController.velocity.x, 0f, playerController.velocity.z);
|
|
bool isMoving = horizontalVelocity.magnitude > 0.5f && playerController.isGrounded;
|
|
|
|
if (isMoving)
|
|
{
|
|
float multiplier = Input.GetKey(KeyCode.LeftShift) ? sprintBobMultiplier : 1f;
|
|
bobTimer += Time.deltaTime * bobFrequency * multiplier;
|
|
|
|
float bobX = Mathf.Sin(bobTimer) * bobHorizontalAmplitude * multiplier;
|
|
float bobY = Mathf.Sin(bobTimer * 2f) * bobVerticalAmplitude * multiplier;
|
|
bobOffset = new Vector3(bobX, bobY, 0f);
|
|
}
|
|
else
|
|
{
|
|
bobTimer = 0f;
|
|
bobOffset = Vector3.Lerp(bobOffset, Vector3.zero, Time.deltaTime * bobReturnSpeed);
|
|
}
|
|
}
|
|
|
|
void Shoot()
|
|
{
|
|
if (currentAmmo <= 0)
|
|
{
|
|
Debug.Log("Out of ammo! Press R to reload.");
|
|
return;
|
|
}
|
|
|
|
currentAmmo--;
|
|
|
|
// Muzzle flash
|
|
ShowMuzzleFlash();
|
|
|
|
// Recoil kick
|
|
ApplyRecoil();
|
|
|
|
// Screen shake
|
|
if (CameraShake.Instance != null)
|
|
CameraShake.Instance.Shake(0.06f, 0.08f);
|
|
|
|
// Raycast
|
|
if (fpsCam == null) return;
|
|
|
|
RaycastHit hit;
|
|
Vector3 origin = fpsCam.transform.position;
|
|
Vector3 direction = fpsCam.transform.forward;
|
|
|
|
if (Physics.Raycast(origin, direction, out hit, range))
|
|
{
|
|
Debug.Log($"Hit: {hit.transform.name}");
|
|
SpawnImpactEffect(hit.point, hit.normal);
|
|
|
|
EnemyHealth enemy = hit.transform.GetComponent<EnemyHealth>();
|
|
if (enemy != null)
|
|
enemy.TakeDamage(damage);
|
|
}
|
|
}
|
|
|
|
void ApplyRecoil()
|
|
{
|
|
recoilOffset += new Vector3(
|
|
Random.Range(-0.005f, 0.005f),
|
|
Random.Range(0f, recoilKickUp),
|
|
-recoilKickback
|
|
);
|
|
}
|
|
|
|
// ─── Shader Helper ───
|
|
|
|
Material CreateUnlitMaterial(Color color)
|
|
{
|
|
string[] shaderNames = {
|
|
"Unlit/Color",
|
|
"Universal Render Pipeline/Unlit",
|
|
"Hidden/InternalErrorShader"
|
|
};
|
|
|
|
Shader shader = null;
|
|
foreach (string name in shaderNames)
|
|
{
|
|
shader = Shader.Find(name);
|
|
if (shader != null) break;
|
|
}
|
|
|
|
if (shader == null)
|
|
return new Material(Shader.Find("Standard")) { color = color };
|
|
|
|
Material mat = new Material(shader);
|
|
mat.color = color;
|
|
return mat;
|
|
}
|
|
|
|
Material CreateParticleMaterial(Color color)
|
|
{
|
|
string[] shaderNames = {
|
|
"Particles/Standard Unlit",
|
|
"Universal Render Pipeline/Particles/Unlit",
|
|
"Unlit/Color",
|
|
"Hidden/InternalErrorShader"
|
|
};
|
|
|
|
Shader shader = null;
|
|
foreach (string name in shaderNames)
|
|
{
|
|
shader = Shader.Find(name);
|
|
if (shader != null) break;
|
|
}
|
|
|
|
Material mat = new Material(shader);
|
|
mat.color = color;
|
|
return mat;
|
|
}
|
|
|
|
// ─── Muzzle Flash ───
|
|
|
|
void CreateMuzzleFlash()
|
|
{
|
|
muzzleFlashObj = GameObject.CreatePrimitive(PrimitiveType.Quad);
|
|
muzzleFlashObj.name = "MuzzleFlash";
|
|
muzzleFlashObj.transform.SetParent(transform);
|
|
muzzleFlashObj.transform.localPosition = new Vector3(0f, -0.05f, 0.75f);
|
|
muzzleFlashObj.transform.localScale = new Vector3(0.15f, 0.15f, 0.15f);
|
|
Destroy(muzzleFlashObj.GetComponent<Collider>());
|
|
|
|
Renderer r = muzzleFlashObj.GetComponent<Renderer>();
|
|
r.material = CreateUnlitMaterial(new Color(1f, 0.85f, 0.3f));
|
|
|
|
GameObject lightObj = new GameObject("MuzzleLight");
|
|
lightObj.transform.SetParent(muzzleFlashObj.transform);
|
|
lightObj.transform.localPosition = Vector3.zero;
|
|
muzzleLight = lightObj.AddComponent<Light>();
|
|
muzzleLight.type = LightType.Point;
|
|
muzzleLight.color = new Color(1f, 0.8f, 0.3f);
|
|
muzzleLight.range = 8f;
|
|
muzzleLight.intensity = 3f;
|
|
muzzleLight.enabled = false;
|
|
|
|
muzzleFlashObj.SetActive(false);
|
|
}
|
|
|
|
void ShowMuzzleFlash()
|
|
{
|
|
if (muzzleFlashObj == null) return;
|
|
|
|
muzzleFlashObj.SetActive(true);
|
|
if (muzzleLight != null) muzzleLight.enabled = true;
|
|
muzzleFlashTimer = muzzleFlashDuration;
|
|
|
|
float angle = Random.Range(0f, 360f);
|
|
muzzleFlashObj.transform.localRotation = Quaternion.Euler(0f, 0f, angle);
|
|
float scale = Random.Range(0.1f, 0.2f);
|
|
muzzleFlashObj.transform.localScale = new Vector3(scale, scale, scale);
|
|
if (muzzleLight != null) muzzleLight.intensity = Random.Range(2f, 4f);
|
|
}
|
|
|
|
// ─── Impact Effects ───
|
|
|
|
void InitDecalPool()
|
|
{
|
|
if (decalPool != null) return;
|
|
|
|
decalPool = new GameObject[maxDecals];
|
|
for (int i = 0; i < maxDecals; i++)
|
|
{
|
|
GameObject decal = GameObject.CreatePrimitive(PrimitiveType.Quad);
|
|
decal.name = "BulletDecal";
|
|
decal.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
|
|
Destroy(decal.GetComponent<Collider>());
|
|
|
|
Renderer dr = decal.GetComponent<Renderer>();
|
|
dr.material = CreateUnlitMaterial(new Color(0.05f, 0.05f, 0.05f));
|
|
|
|
decal.SetActive(false);
|
|
decalPool[i] = decal;
|
|
}
|
|
}
|
|
|
|
void SpawnImpactEffect(Vector3 position, Vector3 normal)
|
|
{
|
|
if (decalPool == null) return;
|
|
|
|
GameObject decal = decalPool[decalIndex % maxDecals];
|
|
decalIndex++;
|
|
|
|
decal.SetActive(true);
|
|
decal.transform.position = position + normal * 0.005f;
|
|
decal.transform.rotation = Quaternion.LookRotation(-normal);
|
|
float s = Random.Range(0.06f, 0.12f);
|
|
decal.transform.localScale = new Vector3(s, s, s);
|
|
|
|
SpawnSparks(position, normal);
|
|
}
|
|
|
|
void SpawnSparks(Vector3 position, Vector3 normal)
|
|
{
|
|
GameObject sparksObj = new GameObject("ImpactSparks");
|
|
sparksObj.transform.position = position;
|
|
sparksObj.transform.rotation = Quaternion.LookRotation(normal);
|
|
|
|
ParticleSystem ps = sparksObj.AddComponent<ParticleSystem>();
|
|
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
|
|
|
var main = ps.main;
|
|
main.duration = 0.1f;
|
|
main.loop = false;
|
|
main.playOnAwake = false;
|
|
main.startLifetime = new ParticleSystem.MinMaxCurve(0.1f, 0.3f);
|
|
main.startSpeed = new ParticleSystem.MinMaxCurve(3f, 8f);
|
|
main.startSize = new ParticleSystem.MinMaxCurve(0.015f, 0.04f);
|
|
main.startColor = new Color(1f, 0.8f, 0.3f);
|
|
main.maxParticles = 12;
|
|
main.gravityModifier = 2f;
|
|
main.simulationSpace = ParticleSystemSimulationSpace.World;
|
|
|
|
var emission = ps.emission;
|
|
emission.enabled = true;
|
|
emission.SetBursts(new ParticleSystem.Burst[] {
|
|
new ParticleSystem.Burst(0f, 6, 12)
|
|
});
|
|
emission.rateOverTime = 0;
|
|
|
|
var shape = ps.shape;
|
|
shape.shapeType = ParticleSystemShapeType.Cone;
|
|
shape.angle = 35f;
|
|
shape.radius = 0.01f;
|
|
|
|
ParticleSystemRenderer psr = sparksObj.GetComponent<ParticleSystemRenderer>();
|
|
psr.material = CreateParticleMaterial(new Color(1f, 0.9f, 0.4f));
|
|
|
|
ps.Play();
|
|
Destroy(sparksObj, 0.5f);
|
|
}
|
|
|
|
// ─── Procedural Gun Model (only if no children exist) ───
|
|
|
|
void CreateSimpleGunModel()
|
|
{
|
|
GameObject body = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|
body.transform.SetParent(transform);
|
|
body.transform.localPosition = new Vector3(0, -0.1f, 0.3f);
|
|
body.transform.localScale = new Vector3(0.1f, 0.15f, 0.4f);
|
|
body.transform.localRotation = Quaternion.identity;
|
|
Destroy(body.GetComponent<Collider>());
|
|
body.GetComponent<Renderer>().material.color = new Color(0.2f, 0.2f, 0.2f);
|
|
|
|
GameObject barrel = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
|
barrel.transform.SetParent(transform);
|
|
barrel.transform.localPosition = new Vector3(0, -0.05f, 0.6f);
|
|
barrel.transform.localScale = new Vector3(0.03f, 0.15f, 0.03f);
|
|
barrel.transform.localRotation = Quaternion.Euler(90, 0, 0);
|
|
Destroy(barrel.GetComponent<Collider>());
|
|
barrel.GetComponent<Renderer>().material.color = new Color(0.1f, 0.1f, 0.1f);
|
|
|
|
GameObject handle = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|
handle.transform.SetParent(transform);
|
|
handle.transform.localPosition = new Vector3(0, -0.25f, 0.15f);
|
|
handle.transform.localScale = new Vector3(0.08f, 0.2f, 0.1f);
|
|
handle.transform.localRotation = Quaternion.Euler(15, 0, 0);
|
|
Destroy(handle.GetComponent<Collider>());
|
|
handle.GetComponent<Renderer>().material.color = new Color(0.3f, 0.2f, 0.1f);
|
|
}
|
|
|
|
void Reload()
|
|
{
|
|
Debug.Log("Reloading...");
|
|
currentAmmo = maxAmmo;
|
|
}
|
|
|
|
void OnGUI()
|
|
{
|
|
GUIStyle ammoStyle = new GUIStyle();
|
|
ammoStyle.fontSize = 28;
|
|
ammoStyle.fontStyle = FontStyle.Bold;
|
|
ammoStyle.normal.textColor = currentAmmo > 5 ? Color.white : Color.red;
|
|
|
|
string ammoText = $"{currentAmmo} / {maxAmmo}";
|
|
GUI.Label(new Rect(Screen.width - 180, Screen.height - 55, 160, 40), ammoText, ammoStyle);
|
|
}
|
|
}
|