From 50968843e5fad0a601ca91f0208b36be9b2b23ea Mon Sep 17 00:00:00 2001 From: rapid Date: Wed, 11 Feb 2026 16:21:45 +0000 Subject: [PATCH] added gun models and enemy spawner --- Assets/Scenes/SampleScene.unity | 188 +++++++++- Assets/Scripts/CameraShake.cs | 53 +++ Assets/Scripts/CameraShake.cs.meta | 2 + Assets/Scripts/EnemyHealth.cs | 93 +++++ Assets/Scripts/EnemyHealth.cs.meta | 2 + Assets/Scripts/EnemySpawner.cs | 81 ++++ Assets/Scripts/EnemySpawner.cs.meta | 2 + Assets/Scripts/FirstPersonController.cs | 87 +++-- Assets/Scripts/HumanoidEnemy.cs | 394 ++++++++++++++++++++ Assets/Scripts/HumanoidEnemy.cs.meta | 2 + Assets/Scripts/RemoveDuplicateGuns.cs | 53 +++ Assets/Scripts/RemoveDuplicateGuns.cs.meta | 2 + Assets/Scripts/SimpleGun.cs | 413 ++++++++++++++++++--- Assets/Scripts/WeaponBob.cs | 54 +++ Assets/Scripts/WeaponBob.cs.meta | 2 + SETUP_GUIDE.md | 133 ++----- 16 files changed, 1372 insertions(+), 189 deletions(-) create mode 100644 Assets/Scripts/CameraShake.cs create mode 100644 Assets/Scripts/CameraShake.cs.meta create mode 100644 Assets/Scripts/EnemyHealth.cs create mode 100644 Assets/Scripts/EnemyHealth.cs.meta create mode 100644 Assets/Scripts/EnemySpawner.cs create mode 100644 Assets/Scripts/EnemySpawner.cs.meta create mode 100644 Assets/Scripts/HumanoidEnemy.cs create mode 100644 Assets/Scripts/HumanoidEnemy.cs.meta create mode 100644 Assets/Scripts/RemoveDuplicateGuns.cs create mode 100644 Assets/Scripts/RemoveDuplicateGuns.cs.meta create mode 100644 Assets/Scripts/WeaponBob.cs create mode 100644 Assets/Scripts/WeaponBob.cs.meta diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index a529013..16a1a0a 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -281,7 +281,7 @@ Transform: m_GameObject: {fileID: 153407402} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalPosition: {x: 11.9, y: -9.03, z: 3.11} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: @@ -300,13 +300,13 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 6b87887b68361bb4dbb1327a9bb7aa82, type: 3} m_Name: m_EditorClassIdentifier: - walkSpeed: 5 + walkSpeed: 8 runSpeed: 10 jumpHeight: 2 gravity: -9.81 mouseSensitivity: 2 maxLookAngle: 90 - playerCamera: {fileID: 0} + playerCamera: {fileID: 2083332238} --- !u!114 &153407406 MonoBehaviour: m_ObjectHideFlags: 0 @@ -349,6 +349,52 @@ MeshCollider: m_Convex: 0 m_CookingOptions: 30 m_Mesh: {fileID: -4926770370004109585, guid: 1b4afb54fbf7b8c46908373caec5cfcf, type: 3} +--- !u!1 &431786309 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 431786311} + - component: {fileID: 431786310} + m_Layer: 0 + m_Name: GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &431786310 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 431786309} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 180c6606991a4b64f812f9713907fd0c, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::EnemySpawner + enemyType: 0 + healthOverride: 0 +--- !u!4 &431786311 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 431786309} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -6.61223, y: -16.71, z: 5.89} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &705507993 GameObject: m_ObjectHideFlags: 0 @@ -676,6 +722,52 @@ MeshCollider: m_Convex: 0 m_CookingOptions: 30 m_Mesh: {fileID: 6626584385738647817, guid: 1b4afb54fbf7b8c46908373caec5cfcf, type: 3} +--- !u!1 &1167301493 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1167301495} + - component: {fileID: 1167301494} + m_Layer: 0 + m_Name: GameObject (2) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1167301494 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1167301493} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 180c6606991a4b64f812f9713907fd0c, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::EnemySpawner + enemyType: 2 + healthOverride: 0 +--- !u!4 &1167301495 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1167301493} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 14.85, y: -0, z: -2.86} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1001 &1188990073 PrefabInstance: m_ObjectHideFlags: 0 @@ -860,6 +952,52 @@ MeshCollider: m_Convex: 1 m_CookingOptions: 30 m_Mesh: {fileID: 0} +--- !u!1 &1908691865 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1908691867} + - component: {fileID: 1908691866} + m_Layer: 0 + m_Name: GameObject (1) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1908691866 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1908691865} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 180c6606991a4b64f812f9713907fd0c, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::EnemySpawner + enemyType: 1 + healthOverride: 0 +--- !u!4 &1908691867 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1908691865} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 19.42, y: -0, z: 10.85739} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1914211071 GameObject: m_ObjectHideFlags: 0 @@ -869,6 +1007,8 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 1914211072} + - component: {fileID: 1914211073} + - component: {fileID: 1914211074} m_Layer: 0 m_Name: Gun m_TagString: Untagged @@ -891,6 +1031,45 @@ Transform: m_Children: [] m_Father: {fileID: 2083332236} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1914211073 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1914211071} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e348b4e172ba23d45960c0071cc09b1f, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::SimpleGun + damage: 25 + range: 100 + fireRate: 0.5 + maxAmmo: 30 + currentAmmo: 0 + isAutomatic: 1 + recoilKickback: 0.08 + recoilKickUp: 0.04 + recoilRecoverySpeed: 12 + bobFrequency: 10 + bobHorizontalAmplitude: 0.05 + bobVerticalAmplitude: 0.03 + sprintBobMultiplier: 1.5 + bobReturnSpeed: 6 + fpsCam: {fileID: 0} +--- !u!114 &1914211074 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1914211071} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9ad5c2eb6d6ecad47af3a531cd239ec5, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::CameraShake --- !u!1 &1929672446 stripped GameObject: m_CorrespondingSourceObject: {fileID: -3210541856918053980, guid: 1b4afb54fbf7b8c46908373caec5cfcf, type: 3} @@ -1121,3 +1300,6 @@ SceneRoots: - {fileID: 2129963323} - {fileID: 754218171} - {fileID: 58167676} + - {fileID: 431786311} + - {fileID: 1908691867} + - {fileID: 1167301495} diff --git a/Assets/Scripts/CameraShake.cs b/Assets/Scripts/CameraShake.cs new file mode 100644 index 0000000..1bbaa87 --- /dev/null +++ b/Assets/Scripts/CameraShake.cs @@ -0,0 +1,53 @@ +using UnityEngine; + +public class CameraShake : MonoBehaviour +{ + public static CameraShake Instance { get; private set; } + + private float shakeDuration = 0f; + private float shakeIntensity = 0f; + private float shakeFalloff = 1f; + + private Vector3 originalLocalPos; + private bool shaking = false; + + void Awake() + { + Instance = this; + originalLocalPos = transform.localPosition; + } + + void LateUpdate() + { + if (!shaking) return; + + if (shakeDuration > 0f) + { + Vector3 offset = Random.insideUnitSphere * shakeIntensity; + offset.z = 0f; // Keep shake on X/Y only + transform.localPosition = originalLocalPos + offset; + + shakeDuration -= Time.deltaTime; + shakeIntensity = Mathf.Lerp(shakeIntensity, 0f, Time.deltaTime * shakeFalloff); + } + else + { + shaking = false; + shakeDuration = 0f; + transform.localPosition = originalLocalPos; + } + } + + /// + /// Trigger a camera shake. + /// + /// How long to shake (seconds) + /// How far to displace the camera + public void Shake(float duration, float intensity) + { + shakeDuration = duration; + shakeIntensity = intensity; + shakeFalloff = intensity / duration; + shaking = true; + } +} diff --git a/Assets/Scripts/CameraShake.cs.meta b/Assets/Scripts/CameraShake.cs.meta new file mode 100644 index 0000000..98be399 --- /dev/null +++ b/Assets/Scripts/CameraShake.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9ad5c2eb6d6ecad47af3a531cd239ec5 \ No newline at end of file diff --git a/Assets/Scripts/EnemyHealth.cs b/Assets/Scripts/EnemyHealth.cs new file mode 100644 index 0000000..75c9bf6 --- /dev/null +++ b/Assets/Scripts/EnemyHealth.cs @@ -0,0 +1,93 @@ +using UnityEngine; + +public class EnemyHealth : MonoBehaviour +{ + public float maxHealth = 100f; + public float currentHealth; + + [Header("Death Settings")] + public bool destroyOnDeath = true; + public float deathDelay = 0.1f; + + private bool isDead = false; + + void Start() + { + currentHealth = maxHealth; + } + + public void TakeDamage(float amount) + { + if (isDead) return; + + currentHealth -= amount; + Debug.Log($"{gameObject.name} took {amount} damage! HP: {currentHealth}/{maxHealth}"); + + // Flash red on hit + StartCoroutine(HitFlash()); + + if (currentHealth <= 0f) + { + Die(); + } + } + + System.Collections.IEnumerator HitFlash() + { + Renderer[] renderers = GetComponentsInChildren(); + if (renderers.Length == 0) yield break; + + // Store original colors + Color[] originalColors = new Color[renderers.Length]; + for (int i = 0; i < renderers.Length; i++) + originalColors[i] = renderers[i].material.color; + + // Flash all parts white-red + for (int i = 0; i < renderers.Length; i++) + renderers[i].material.color = new Color(1f, 0.3f, 0.3f); + + yield return new WaitForSeconds(0.08f); + + // Restore + if (!isDead) + { + for (int i = 0; i < renderers.Length; i++) + { + if (renderers[i] != null) + renderers[i].material.color = originalColors[i]; + } + } + } + + void Die() + { + isDead = true; + Debug.Log($"{gameObject.name} KILLED!"); + + if (destroyOnDeath) + { + // Quick death: disable collider immediately, destroy after delay + Collider col = GetComponent(); + if (col != null) col.enabled = false; + + // Optional: add a little "pop" by scaling down + StartCoroutine(DeathEffect()); + } + } + + System.Collections.IEnumerator DeathEffect() + { + float timer = 0f; + Vector3 startScale = transform.localScale; + + while (timer < deathDelay) + { + timer += Time.deltaTime; + float t = timer / deathDelay; + transform.localScale = Vector3.Lerp(startScale, Vector3.zero, t); + yield return null; + } + + Destroy(gameObject); + } +} diff --git a/Assets/Scripts/EnemyHealth.cs.meta b/Assets/Scripts/EnemyHealth.cs.meta new file mode 100644 index 0000000..6a7fde2 --- /dev/null +++ b/Assets/Scripts/EnemyHealth.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 13343cfbf2e477741a831a3db3c9cf80 \ No newline at end of file diff --git a/Assets/Scripts/EnemySpawner.cs b/Assets/Scripts/EnemySpawner.cs new file mode 100644 index 0000000..1393511 --- /dev/null +++ b/Assets/Scripts/EnemySpawner.cs @@ -0,0 +1,81 @@ +using UnityEngine; + +/// +/// Drop this anywhere in your scene to spawn a humanoid enemy at that position. +/// Configure the enemy type in the Inspector then hit Play. +/// Delete this script after you've set up your enemies if you prefer manual placement. +/// +public class EnemySpawner : MonoBehaviour +{ + public HumanoidEnemy.EnemyType enemyType = HumanoidEnemy.EnemyType.Grunt; + + [Header("Health Override (0 = use defaults)")] + public float healthOverride = 0f; + + void Start() + { + SpawnEnemy(); + } + + void SpawnEnemy() + { + GameObject enemyObj = new GameObject($"Enemy_{enemyType}"); + enemyObj.transform.position = transform.position; + enemyObj.transform.rotation = transform.rotation; + + // Add CharacterController (required by HumanoidEnemy) + CharacterController cc = enemyObj.AddComponent(); + cc.slopeLimit = 45f; + cc.stepOffset = 0.5f; + + // Add health + EnemyHealth health = enemyObj.AddComponent(); + + // Add enemy AI + model + HumanoidEnemy enemy = enemyObj.AddComponent(); + enemy.enemyType = enemyType; + + if (healthOverride > 0f) + { + health.maxHealth = healthOverride; + health.currentHealth = healthOverride; + } + } + + void OnDrawGizmos() + { + // Show enemy position in editor + switch (enemyType) + { + case HumanoidEnemy.EnemyType.Grunt: + Gizmos.color = new Color(0.8f, 0.5f, 0.2f, 0.8f); + DrawEnemyGizmo(1f); + break; + case HumanoidEnemy.EnemyType.Brute: + Gizmos.color = new Color(0.8f, 0.1f, 0.1f, 0.8f); + DrawEnemyGizmo(1.3f); + break; + case HumanoidEnemy.EnemyType.Runner: + Gizmos.color = new Color(0.2f, 0.7f, 0.2f, 0.8f); + DrawEnemyGizmo(0.9f); + break; + } + } + + void DrawEnemyGizmo(float scale) + { + Vector3 pos = transform.position; + + // Body + Gizmos.DrawCube(pos + Vector3.up * 1.15f * scale, new Vector3(0.45f, 0.6f, 0.25f) * scale); + // Head + Gizmos.DrawSphere(pos + Vector3.up * 1.65f * scale, 0.15f * scale); + // Legs + Gizmos.DrawCube(pos + Vector3.up * 0.4f * scale + Vector3.left * 0.12f, new Vector3(0.18f, 0.6f, 0.18f) * scale); + Gizmos.DrawCube(pos + Vector3.up * 0.4f * scale + Vector3.right * 0.12f, new Vector3(0.18f, 0.6f, 0.18f) * scale); + + // Direction arrow + Gizmos.color = Color.yellow; + Gizmos.DrawRay(pos + Vector3.up * scale, transform.forward * 1.5f); + } +} diff --git a/Assets/Scripts/EnemySpawner.cs.meta b/Assets/Scripts/EnemySpawner.cs.meta new file mode 100644 index 0000000..ae127af --- /dev/null +++ b/Assets/Scripts/EnemySpawner.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 180c6606991a4b64f812f9713907fd0c \ No newline at end of file diff --git a/Assets/Scripts/FirstPersonController.cs b/Assets/Scripts/FirstPersonController.cs index b3102fc..0d27634 100644 --- a/Assets/Scripts/FirstPersonController.cs +++ b/Assets/Scripts/FirstPersonController.cs @@ -3,8 +3,8 @@ using UnityEngine; public class FirstPersonController : MonoBehaviour { [Header("Movement Settings")] - public float walkSpeed = 8f; - public float runSpeed = 14f; + public float walkSpeed = 50f; // Boosted from 8f to overcome collision issues + public float runSpeed = 80f; // Boosted from 14f public float jumpHeight = 2.5f; public float gravity = -20f; @@ -23,66 +23,96 @@ public class FirstPersonController : MonoBehaviour void Start() { - Debug.Log( "Starting game" ); - // Get the CharacterController component - controller = GetComponent(); + Debug.Log("Starting game"); - // If no camera is assigned, try to find one - if (playerCamera == null) + // FORCE NORMAL TIME (in case something external changed it) + Time.timeScale = 1f; + + controller = GetComponent(); + + if (controller == null) { - playerCamera = GetComponentInChildren(); + Debug.LogError("FirstPersonController: No CharacterController found!"); + return; } - // Lock and hide the cursor + if (playerCamera == null) + playerCamera = GetComponentInChildren(); + Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } void Update() { - // Check if player is on the ground + if (controller == null) return; + isGrounded = controller.isGrounded; - // Reset vertical velocity when grounded if (isGrounded && velocity.y < 0) - { velocity.y = -2f; + + // ─── Movement via direct KeyCode ─── + float moveX = 0f; + float moveZ = 0f; + + if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow)) moveZ += 1f; + if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow)) moveZ -= 1f; + if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) moveX -= 1f; + if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) moveX += 1f; + + // DEBUG: Log once when W is first pressed + if (Input.GetKeyDown(KeyCode.W)) + { + Debug.Log($"[FPC DEBUG] W pressed | controller.enabled={controller.enabled} | position={transform.position} | isGrounded={isGrounded}"); + Debug.Log($"[FPC DEBUG] Time.timeScale={Time.timeScale} | Time.deltaTime={Time.deltaTime} | walkSpeed={walkSpeed}"); } - // Get movement input - float moveX = Input.GetAxis("Horizontal"); // A/D or Left/Right arrows - float moveZ = Input.GetAxis("Vertical"); // W/S or Up/Down arrows - - // Calculate movement direction Vector3 move = transform.right * moveX + transform.forward * moveZ; - // Determine current speed (run if Shift is held) + if (move.magnitude > 1f) + move.Normalize(); + float currentSpeed = Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed; - - // Move the character + + Vector3 posBefore = transform.position; controller.Move(move * currentSpeed * Time.deltaTime); - - // Jumping - if (Input.GetButtonDown("Jump") && isGrounded) + + // DEBUG: Log if we tried to move but didn't + if (move.magnitude > 0f && Input.GetKeyDown(KeyCode.W)) { - velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); + Vector3 posAfter = transform.position; + Debug.Log($"[FPC DEBUG] Move attempt: delta={move * currentSpeed * Time.deltaTime} | actualDelta={(posAfter - posBefore)} | controller.height={controller.height} | controller.radius={controller.radius}"); + + // Check what we're colliding with + Collider[] nearbyColliders = Physics.OverlapSphere(transform.position, controller.radius + 0.5f); + Debug.Log($"[FPC DEBUG] Found {nearbyColliders.Length} colliders near player"); + foreach (Collider col in nearbyColliders) + { + if (col != controller && !(col is CharacterController)) + Debug.LogWarning($"[FPC DEBUG] Nearby collider: {col.gameObject.name} on layer {LayerMask.LayerToName(col.gameObject.layer)}"); + } } - // Apply gravity + // Jumping + if (Input.GetKey(KeyCode.Space) && isGrounded) + velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); + + // Gravity velocity.y += gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); // Mouse look HandleMouseLook(); - // Press Escape to unlock cursor + // Escape to unlock cursor if (Input.GetKeyDown(KeyCode.Escape)) { Cursor.lockState = CursorLockMode.None; Cursor.visible = true; } - // Click to lock cursor again + // Click to re-lock cursor if (Input.GetMouseButtonDown(0) && Cursor.lockState == CursorLockMode.None) { Cursor.lockState = CursorLockMode.Locked; @@ -92,16 +122,13 @@ public class FirstPersonController : MonoBehaviour void HandleMouseLook() { - // Get mouse input float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity; float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity; - // Rotate camera up/down (pitch) xRotation -= mouseY; xRotation = Mathf.Clamp(xRotation, -maxLookAngle, maxLookAngle); playerCamera.transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f); - // Rotate player left/right (yaw) transform.Rotate(Vector3.up * mouseX); } } diff --git a/Assets/Scripts/HumanoidEnemy.cs b/Assets/Scripts/HumanoidEnemy.cs new file mode 100644 index 0000000..759c383 --- /dev/null +++ b/Assets/Scripts/HumanoidEnemy.cs @@ -0,0 +1,394 @@ +using UnityEngine; +using System.Collections; + +[RequireComponent(typeof(CharacterController))] +public class HumanoidEnemy : MonoBehaviour +{ + public enum EnemyType { Grunt, Brute, Runner } + + [Header("Enemy Type")] + public EnemyType enemyType = EnemyType.Grunt; + + [Header("Movement")] + public float moveSpeed = 5f; + public float chaseRange = 25f; + public float attackRange = 2f; + public float gravity = -20f; + + [Header("Combat")] + public float attackDamage = 10f; + public float attackCooldown = 1f; + + [Header("Patrol (optional)")] + public float patrolRadius = 8f; + public float patrolWaitTime = 2f; + + // Internal + private Transform player; + private CharacterController controller; + private EnemyHealth health; + private Vector3 spawnPoint; + private Vector3 patrolTarget; + private float nextAttackTime; + private float verticalVelocity; + private float patrolWaitTimer; + private bool isAggro = false; + + // Animation state + private Transform bodyRoot; + private Transform headTransform; + private Transform torsoTransform; + private Transform leftArm; + private Transform rightArm; + private Transform leftLeg; + private Transform rightLeg; + private float animTimer = 0f; + + void Start() + { + controller = GetComponent(); + health = GetComponent(); + + // Find player + GameObject playerObj = GameObject.Find("Player"); + if (playerObj != null) + player = playerObj.transform; + + spawnPoint = transform.position; + patrolTarget = GetRandomPatrolPoint(); + + ApplyTypeStats(); + BuildHumanoidModel(); + } + + void ApplyTypeStats() + { + switch (enemyType) + { + case EnemyType.Grunt: + moveSpeed = 4f; + attackDamage = 10f; + attackCooldown = 1.2f; + chaseRange = 20f; + if (health != null) health.maxHealth = 80f; + break; + + case EnemyType.Brute: + moveSpeed = 2.5f; + attackDamage = 25f; + attackCooldown = 2f; + chaseRange = 18f; + if (health != null) health.maxHealth = 200f; + break; + + case EnemyType.Runner: + moveSpeed = 8f; + attackDamage = 8f; + attackCooldown = 0.6f; + chaseRange = 30f; + if (health != null) health.maxHealth = 50f; + break; + } + + if (health != null) + health.currentHealth = health.maxHealth; + } + + void Update() + { + if (health != null && health.currentHealth <= 0f) return; + if (player == null) return; + + float distToPlayer = Vector3.Distance(transform.position, player.position); + + // Aggro check + if (distToPlayer <= chaseRange) + isAggro = true; + + if (isAggro) + { + if (distToPlayer <= attackRange) + Attack(); + else + ChasePlayer(); + } + else + { + Patrol(); + } + + // Gravity + if (controller.isGrounded && verticalVelocity < 0f) + verticalVelocity = -2f; + verticalVelocity += gravity * Time.deltaTime; + controller.Move(Vector3.up * verticalVelocity * Time.deltaTime); + + // Animate + AnimateModel(isAggro && distToPlayer > attackRange); + } + + void ChasePlayer() + { + Vector3 direction = (player.position - transform.position); + direction.y = 0f; + direction.Normalize(); + + controller.Move(direction * moveSpeed * Time.deltaTime); + + // Face the player + if (direction != Vector3.zero) + transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 8f); + } + + void Patrol() + { + float distToTarget = Vector3.Distance(transform.position, patrolTarget); + + if (distToTarget < 1.5f) + { + patrolWaitTimer += Time.deltaTime; + if (patrolWaitTimer >= patrolWaitTime) + { + patrolTarget = GetRandomPatrolPoint(); + patrolWaitTimer = 0f; + } + return; + } + + Vector3 direction = (patrolTarget - transform.position); + direction.y = 0f; + direction.Normalize(); + + controller.Move(direction * (moveSpeed * 0.4f) * Time.deltaTime); + + if (direction != Vector3.zero) + transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 4f); + } + + Vector3 GetRandomPatrolPoint() + { + Vector2 randomCircle = Random.insideUnitCircle * patrolRadius; + return spawnPoint + new Vector3(randomCircle.x, 0f, randomCircle.y); + } + + void Attack() + { + // Face the player + Vector3 direction = (player.position - transform.position); + direction.y = 0f; + if (direction != Vector3.zero) + transform.rotation = Quaternion.LookRotation(direction); + + if (Time.time >= nextAttackTime) + { + nextAttackTime = Time.time + attackCooldown; + Debug.Log($"{gameObject.name} attacks for {attackDamage} damage!"); + + // Punch animation + StartCoroutine(PunchAnimation()); + + // TODO: Hook into player health system when you have one + // player.GetComponent()?.TakeDamage(attackDamage); + } + } + + // ─── Procedural Humanoid Model ─── + + void BuildHumanoidModel() + { + // Sizing based on type + float scale = 1f; + Color skinColor = new Color(0.6f, 0.45f, 0.35f); + Color shirtColor; + Color pantsColor = new Color(0.2f, 0.2f, 0.25f); + + switch (enemyType) + { + case EnemyType.Grunt: + scale = 1f; + shirtColor = new Color(0.4f, 0.25f, 0.2f); // brown shirt + break; + case EnemyType.Brute: + scale = 1.3f; + shirtColor = new Color(0.5f, 0.1f, 0.1f); // red shirt + skinColor = new Color(0.5f, 0.35f, 0.3f); + break; + case EnemyType.Runner: + scale = 0.9f; + shirtColor = new Color(0.2f, 0.35f, 0.2f); // green shirt + break; + default: + shirtColor = Color.gray; + break; + } + + bodyRoot = new GameObject("BodyRoot").transform; + bodyRoot.SetParent(transform); + bodyRoot.localPosition = Vector3.zero; + bodyRoot.localRotation = Quaternion.identity; + + // Head + headTransform = CreatePart("Head", bodyRoot, + new Vector3(0f, 1.65f, 0f) * scale, + new Vector3(0.3f, 0.3f, 0.3f) * scale, + skinColor, PrimitiveType.Sphere); + + // Eyes (two small dark spheres) + CreatePart("LeftEye", headTransform, + new Vector3(-0.08f, 0.03f, 0.12f), + new Vector3(0.08f, 0.08f, 0.05f), + new Color(0.9f, 0.1f, 0.1f), PrimitiveType.Sphere); + + CreatePart("RightEye", headTransform, + new Vector3(0.08f, 0.03f, 0.12f), + new Vector3(0.08f, 0.08f, 0.05f), + new Color(0.9f, 0.1f, 0.1f), PrimitiveType.Sphere); + + // Torso + torsoTransform = CreatePart("Torso", bodyRoot, + new Vector3(0f, 1.15f, 0f) * scale, + new Vector3(0.45f, 0.6f, 0.25f) * scale, + shirtColor, PrimitiveType.Cube); + + // Left Arm + leftArm = CreatePart("LeftArm", bodyRoot, + new Vector3(-0.35f, 1.15f, 0f) * scale, + new Vector3(0.15f, 0.55f, 0.15f) * scale, + skinColor, PrimitiveType.Cube); + + // Right Arm + rightArm = CreatePart("RightArm", bodyRoot, + new Vector3(0.35f, 1.15f, 0f) * scale, + new Vector3(0.15f, 0.55f, 0.15f) * scale, + skinColor, PrimitiveType.Cube); + + // Left Leg + leftLeg = CreatePart("LeftLeg", bodyRoot, + new Vector3(-0.12f, 0.4f, 0f) * scale, + new Vector3(0.18f, 0.6f, 0.18f) * scale, + pantsColor, PrimitiveType.Cube); + + // Right Leg + rightLeg = CreatePart("RightLeg", bodyRoot, + new Vector3(0.12f, 0.4f, 0f) * scale, + new Vector3(0.18f, 0.6f, 0.18f) * scale, + pantsColor, PrimitiveType.Cube); + + // Adjust CharacterController to fit + controller.height = 1.9f * scale; + controller.radius = 0.3f * scale; + controller.center = new Vector3(0f, 0.95f * scale, 0f); + } + + Transform CreatePart(string name, Transform parent, Vector3 localPos, Vector3 localScale, Color color, PrimitiveType shape = PrimitiveType.Cube) + { + GameObject part = GameObject.CreatePrimitive(shape); + part.name = name; + part.transform.SetParent(parent); + part.transform.localPosition = localPos; + part.transform.localScale = localScale; + part.transform.localRotation = Quaternion.identity; + + // Remove collider from body parts (CharacterController handles collision) + Collider col = part.GetComponent(); + if (col != null) Destroy(col); + + Renderer rend = part.GetComponent(); + rend.material = new Material(Shader.Find("Standard")); + rend.material.color = color; + + return part.transform; + } + + // ─── Simple Limb Animation ─── + + void AnimateModel(bool isWalking) + { + if (bodyRoot == null) return; + + if (isWalking) + { + animTimer += Time.deltaTime * moveSpeed * 1.5f; + + float limbSwing = Mathf.Sin(animTimer) * 30f; + + // Arms swing opposite to legs + if (leftArm != null) + leftArm.localRotation = Quaternion.Euler(limbSwing, 0f, 0f); + if (rightArm != null) + rightArm.localRotation = Quaternion.Euler(-limbSwing, 0f, 0f); + if (leftLeg != null) + leftLeg.localRotation = Quaternion.Euler(-limbSwing, 0f, 0f); + if (rightLeg != null) + rightLeg.localRotation = Quaternion.Euler(limbSwing, 0f, 0f); + + // Slight torso bob + if (torsoTransform != null) + { + Vector3 torsoPos = torsoTransform.localPosition; + float scale = enemyType == EnemyType.Brute ? 1.3f : (enemyType == EnemyType.Runner ? 0.9f : 1f); + torsoPos.y = 1.15f * scale + Mathf.Abs(Mathf.Sin(animTimer * 2f)) * 0.03f; + torsoTransform.localPosition = torsoPos; + } + } + else + { + // Return to idle pose + animTimer = 0f; + if (leftArm != null) + leftArm.localRotation = Quaternion.Slerp(leftArm.localRotation, Quaternion.identity, Time.deltaTime * 5f); + if (rightArm != null) + rightArm.localRotation = Quaternion.Slerp(rightArm.localRotation, Quaternion.identity, Time.deltaTime * 5f); + if (leftLeg != null) + leftLeg.localRotation = Quaternion.Slerp(leftLeg.localRotation, Quaternion.identity, Time.deltaTime * 5f); + if (rightLeg != null) + rightLeg.localRotation = Quaternion.Slerp(rightLeg.localRotation, Quaternion.identity, Time.deltaTime * 5f); + } + } + + IEnumerator PunchAnimation() + { + if (rightArm == null) yield break; + + // Wind up + float timer = 0f; + float punchDuration = 0.15f; + + while (timer < punchDuration) + { + timer += Time.deltaTime; + float t = timer / punchDuration; + rightArm.localRotation = Quaternion.Euler(Mathf.Lerp(0f, -90f, t), 0f, 0f); + yield return null; + } + + // Snap back + timer = 0f; + while (timer < punchDuration) + { + timer += Time.deltaTime; + float t = timer / punchDuration; + rightArm.localRotation = Quaternion.Euler(Mathf.Lerp(-90f, 0f, t), 0f, 0f); + yield return null; + } + + rightArm.localRotation = Quaternion.identity; + } + + // ─── Gizmos ─── + + void OnDrawGizmosSelected() + { + // Chase range + Gizmos.color = new Color(1f, 0f, 0f, 0.15f); + Gizmos.DrawWireSphere(transform.position, chaseRange); + + // Attack range + Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); + Gizmos.DrawWireSphere(transform.position, attackRange); + + // Patrol radius + Gizmos.color = new Color(0f, 0.5f, 1f, 0.15f); + Gizmos.DrawWireSphere(Application.isPlaying ? spawnPoint : transform.position, patrolRadius); + } +} diff --git a/Assets/Scripts/HumanoidEnemy.cs.meta b/Assets/Scripts/HumanoidEnemy.cs.meta new file mode 100644 index 0000000..137170b --- /dev/null +++ b/Assets/Scripts/HumanoidEnemy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 363b918c62c4e944faca34713da9120e \ No newline at end of file diff --git a/Assets/Scripts/RemoveDuplicateGuns.cs b/Assets/Scripts/RemoveDuplicateGuns.cs new file mode 100644 index 0000000..8d1bd2c --- /dev/null +++ b/Assets/Scripts/RemoveDuplicateGuns.cs @@ -0,0 +1,53 @@ +using UnityEngine; + +/// +/// Attach this to your Player object and hit Play once to remove duplicate guns. +/// Delete this script after running it. +/// +public class RemoveDuplicateGuns : MonoBehaviour +{ + void Start() + { + Debug.Log("=== SEARCHING FOR DUPLICATE GUNS ==="); + + // Find all SimpleGun components in children + SimpleGun[] guns = GetComponentsInChildren(); + + Debug.Log($"Found {guns.Length} gun(s)"); + + if (guns.Length <= 1) + { + Debug.Log("Only one gun found - no duplicates to remove"); + return; + } + + // Keep the gun that's furthest from the player's feet (highest Y position) + SimpleGun highestGun = guns[0]; + float highestY = guns[0].transform.position.y; + + foreach (SimpleGun gun in guns) + { + Vector3 worldPos = gun.transform.position; + Debug.Log($"Gun at {gun.gameObject.name}: World Position = {worldPos}"); + + if (worldPos.y > highestY) + { + highestGun = gun; + highestY = worldPos.y; + } + } + + // Remove all guns except the highest one + foreach (SimpleGun gun in guns) + { + if (gun != highestGun) + { + Debug.LogWarning($"REMOVING duplicate gun: {gun.gameObject.name} at {gun.transform.position}"); + Destroy(gun.gameObject); + } + } + + Debug.Log($"Kept gun: {highestGun.gameObject.name} at {highestGun.transform.position}"); + Debug.Log("=== DUPLICATE REMOVAL COMPLETE ==="); + } +} diff --git a/Assets/Scripts/RemoveDuplicateGuns.cs.meta b/Assets/Scripts/RemoveDuplicateGuns.cs.meta new file mode 100644 index 0000000..aa47cae --- /dev/null +++ b/Assets/Scripts/RemoveDuplicateGuns.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ed4b2209a2bb458458ceb5e8a3ba5950 \ No newline at end of file diff --git a/Assets/Scripts/SimpleGun.cs b/Assets/Scripts/SimpleGun.cs index caf5f72..7069f40 100644 --- a/Assets/Scripts/SimpleGun.cs +++ b/Assets/Scripts/SimpleGun.cs @@ -5,81 +5,158 @@ public class SimpleGun : MonoBehaviour [Header("Gun Settings")] public float damage = 25f; public float range = 100f; - public float fireRate = 0.5f; + 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; - public ParticleSystem muzzleFlash; + // 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; - - // Find camera if not assigned + if (fpsCam == null) - { fpsCam = Camera.main; + if (fpsCam == null) + fpsCam = GetComponentInParent(); + + playerController = GetComponentInParent(); + 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(); + foreach (Collider col in allColliders) + { + if (!(col is CharacterController)) + { + Debug.LogWarning($"Found collider on {col.gameObject.name} - REMOVING IT!"); + Destroy(col); + } } - // Create simple gun visuals - CreateSimpleGunModel(); + // Only create a procedural gun if there's no existing model geometry + if (GetComponentsInChildren().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(); + if (col != null && !(col is CharacterController)) + { + Destroy(col); + } + } } void Update() { - // Shoot on left mouse button - if (Input.GetButton("Fire1") && Time.time >= nextTimeToFire) + // Shooting + if (isAutomatic ? Input.GetButton("Fire1") : Input.GetButtonDown("Fire1")) { - nextTimeToFire = Time.time + 1f / fireRate; - Shoot(); + if (Time.time >= nextTimeToFire) + { + nextTimeToFire = Time.time + 1f / fireRate; + Shoot(); + } } - // Reload on R key + // 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 CreateSimpleGunModel() + void UpdateBob() { - // Main body - 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()); // Remove collider - - // Set color - Renderer bodyRenderer = body.GetComponent(); - bodyRenderer.material.color = new Color(0.2f, 0.2f, 0.2f); // Dark gray + if (playerController == null) + { + bobOffset = Vector3.zero; + return; + } - // Barrel - 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()); - - Renderer barrelRenderer = barrel.GetComponent(); - barrelRenderer.material.color = new Color(0.1f, 0.1f, 0.1f); // Even darker + Vector3 horizontalVelocity = new Vector3(playerController.velocity.x, 0f, playerController.velocity.z); + bool isMoving = horizontalVelocity.magnitude > 0.5f && playerController.isGrounded; - // Handle - 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()); - - Renderer handleRenderer = handle.GetComponent(); - handleRenderer.material.color = new Color(0.3f, 0.2f, 0.1f); // Brown + 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() @@ -91,25 +168,239 @@ public class SimpleGun : MonoBehaviour } currentAmmo--; - Debug.Log($"Shot fired! Ammo: {currentAmmo}/{maxAmmo}"); - // Play muzzle flash if assigned - if (muzzleFlash != null) - { - muzzleFlash.Play(); - } + // Muzzle flash + ShowMuzzleFlash(); + + // Recoil kick + ApplyRecoil(); + + // Screen shake + if (CameraShake.Instance != null) + CameraShake.Instance.Shake(0.06f, 0.08f); + + // Raycast + if (fpsCam == null) return; - // Raycast from camera center RaycastHit hit; - if (Physics.Raycast(fpsCam.transform.position, fpsCam.transform.forward, out hit, range)) + 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); - // You can add hit detection for enemies here later - // Example: hit.transform.GetComponent()?.TakeDamage(damage); + EnemyHealth enemy = hit.transform.GetComponent(); + 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()); + + Renderer r = muzzleFlashObj.GetComponent(); + 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(); + 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()); + + Renderer dr = decal.GetComponent(); + 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(); + 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(); + 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()); + body.GetComponent().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()); + barrel.GetComponent().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()); + handle.GetComponent().material.color = new Color(0.3f, 0.2f, 0.1f); + } + void Reload() { Debug.Log("Reloading..."); @@ -118,10 +409,12 @@ public class SimpleGun : MonoBehaviour void OnGUI() { - // Simple ammo counter in bottom-right - GUI.color = Color.white; - GUI.Label(new Rect(Screen.width - 120, Screen.height - 40, 100, 30), - $"Ammo: {currentAmmo}/{maxAmmo}", - new GUIStyle() { fontSize = 20, normal = new GUIStyleState() { textColor = Color.white } }); + 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); } } diff --git a/Assets/Scripts/WeaponBob.cs b/Assets/Scripts/WeaponBob.cs new file mode 100644 index 0000000..e6e2b92 --- /dev/null +++ b/Assets/Scripts/WeaponBob.cs @@ -0,0 +1,54 @@ +using UnityEngine; + +public class WeaponBob : MonoBehaviour +{ + [Header("Bob Settings")] + public float bobFrequency = 10f; + public float bobHorizontalAmplitude = 0.05f; + public float bobVerticalAmplitude = 0.03f; + + [Header("Sprint Multiplier")] + public float sprintBobMultiplier = 1.5f; + + [Header("Smoothing")] + public float returnSpeed = 6f; + + private Vector3 originalLocalPos; + private float bobTimer = 0f; + private CharacterController controller; + + void Start() + { + originalLocalPos = transform.localPosition; + controller = GetComponentInParent(); + } + + void Update() + { + if (controller == null) return; + + // Check if the player is moving on the ground + Vector3 horizontalVelocity = new Vector3(controller.velocity.x, 0f, controller.velocity.z); + bool isMoving = horizontalVelocity.magnitude > 0.5f && controller.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; + + transform.localPosition = originalLocalPos + new Vector3(bobX, bobY, 0f); + } + else + { + bobTimer = 0f; + transform.localPosition = Vector3.Lerp( + transform.localPosition, + originalLocalPos, + Time.deltaTime * returnSpeed + ); + } + } +} diff --git a/Assets/Scripts/WeaponBob.cs.meta b/Assets/Scripts/WeaponBob.cs.meta new file mode 100644 index 0000000..4f943c4 --- /dev/null +++ b/Assets/Scripts/WeaponBob.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4b2ef1ad29b012344bd26f70130d3c2e \ No newline at end of file diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md index 32ea29a..136a0bd 100644 --- a/SETUP_GUIDE.md +++ b/SETUP_GUIDE.md @@ -1,105 +1,46 @@ -# LiDAR FPS Game - Setup Guide +# OGG Shooting System - Setup Guide -## Project Structure Created: -- **Assets/Scripts/** - Your C# scripts (including FirstPersonController.cs) -- **Assets/Models/LidarScans/** - Where you'll import your GLB files -- **Assets/Prefabs/** - For reusable game objects -- **Assets/Scenes/** - Your game levels +## Scene Hierarchy (how it should look) -## Step 1: Import Your GLB Files +``` +Player [CharacterController + FirstPersonController + SimpleCrosshair] + └── Camera [Camera + AudioListener + CameraShake + WeaponBob] + └── Gun [SimpleGun] +``` -1. Open Unity and your OGG project -2. Copy some GLB files from `K:\OGG\lidar\` to `K:\OGG\OGG\Assets\Models\LidarScans\` - - Start with one or two to test (e.g., "my bedroom.glb" or "studio old.glb") -3. Unity will automatically import them +## Step-by-step Setup -## Step 2: Install glTFast Package (for better GLB support) +### 1. Player object (already done) +- Has: CharacterController, FirstPersonController, SimpleCrosshair ✔ +- **Fix**: Drag the **Camera** child into the `Player Camera` field on FirstPersonController -1. In Unity, go to **Window → Package Manager** -2. Click the **"+"** button in top-left -3. Select **"Add package by name"** -4. Enter: `com.atteneder.gltfast` -5. Click **Add** +### 2. Camera object (child of Player) +- Has: Camera, AudioListener ✔ +- **Add Component**: `CameraShake` +- **Add Component**: `WeaponBob` -## Step 3: Create Your Player +### 3. Gun object (child of Camera) +- Currently empty! +- **Add Component**: `SimpleGun` +- The gun auto-creates its own primitive model and muzzle flash -1. In the Hierarchy, **Right-click → Create Empty** -2. Rename it to "Player" -3. With Player selected, click **Add Component** -4. Search for and add: **Character Controller** -5. Add Component again, search for: **First Person Controller** (our script) +### 4. Testing enemies +- Create any GameObject (e.g. a Cube) in the scene +- Add a **Collider** (Box Collider, etc.) +- **Add Component**: `EnemyHealth` +- Shoot it and watch it flash red and pop! -## Step 4: Set Up the Camera +## Controls +| Key | Action | +|-----|--------| +| Left Click (hold) | Shoot (automatic fire) | +| R | Reload | +| W/A/S/D | Move | +| Shift | Sprint | +| Space | Jump | -1. In Hierarchy, **Right-click on Player → Camera** -2. Position the camera: - - Set Position Y to about 1.6 (eye height) - - Reset X and Z to 0 -3. The FirstPersonController script should automatically find this camera - -## Step 5: Import a LiDAR Scan into Your Scene - -1. From Project window, navigate to **Assets/Models/LidarScans/** -2. Drag one of your GLB files into the Scene (Hierarchy) -3. Select the imported model in Hierarchy -4. In Inspector, click **Add Component → Mesh Collider** - - This allows the player to walk on it -5. You might need to adjust the scale: - - Try Scale: X=1, Y=1, Z=1 first - - If too big/small, adjust all values equally - -## Step 6: Position Your Player - -1. Select the Player in Hierarchy -2. In the Inspector, set Transform Position: - - Move the player above your scan (Y=2 or Y=3) - - Adjust X and Z to be inside the scan area -3. Make sure Character Controller settings are reasonable: - - Radius: 0.5 - - Height: 2 - - Center: Y=1 - -## Step 7: Test Your Game! - -1. Click the **Play** button at the top -2. **Controls:** - - **WASD** or **Arrow Keys** - Move - - **Mouse** - Look around - - **Left Shift** - Run - - **Space** - Jump - - **Escape** - Unlock cursor - -## Troubleshooting: - -### Player falls through the floor: -- Make sure your LiDAR model has a **Mesh Collider** component -- Check that the Mesh Collider is enabled (checkbox ticked) - -### Can't see anything: -- The camera might be inside geometry - move the Player position up (increase Y value) -- Check that the Camera is a child of Player and positioned at Y=1.6 - -### Movement feels weird: -- Adjust the Character Controller's Radius and Height -- Try different Walk/Run speeds in the FirstPersonController settings - -### GLB files won't import: -- Install the glTFast package (see Step 2) -- Alternatively, you can convert GLB to OBJ using Blender if needed - -## Next Steps: - -Once you have basic movement working, we can add: -- Crosshair UI -- Health system -- Shooting mechanics -- Enemies/targets -- Multiple level loading -- Lighting improvements for your LiDAR scans - -## Tips for LiDAR Scans: - -- Your scans might be very detailed - this can slow down the game -- You may need to optimize them later (reduce polygon count) -- Start with smaller scans (single rooms) before loading larger areas -- You can have multiple scans in one scene to create larger levels +## Tuning Tips +- **Fire rate**: `SimpleGun.fireRate` (8 = fast, 2 = slow) +- **Shake intensity**: `CameraShake.Shake()` is called with (duration, intensity) - tweak in SimpleGun +- **Bob feel**: Adjust `WeaponBob.bobFrequency` and amplitude values +- **Recoil punch**: `SimpleGun.recoilKickback` and `recoilKickUp`