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`