added headshots and enemy's shooting

This commit is contained in:
2026-02-18 00:40:22 +00:00
parent 27f2931ce1
commit e1df65bce2
6 changed files with 554 additions and 57 deletions

View File

@@ -0,0 +1,10 @@
using UnityEngine;
/// <summary>
/// Attach this to an enemy's head GameObject.
/// When the player shoots it, SimpleGun checks for this component and instakills.
/// </summary>
public class EnemyHeadHitbox : MonoBehaviour
{
[HideInInspector] public EnemyHealth enemyHealth;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 23988a1164d49b7498b3b4dad22c92f4

View File

@@ -15,10 +15,18 @@ public class HumanoidEnemy : MonoBehaviour
public float attackRange = 2f;
public float gravity = -20f;
[Header("Combat")]
[Header("Melee Combat")]
public float attackDamage = 10f;
public float attackCooldown = 1f;
[Header("Ranged Combat")]
public bool canShoot = true;
public float shootRange = 18f; // within this range (but outside attackRange) the enemy shoots
public float shootDamage = 8f;
public float shootCooldown = 1.5f;
public float bulletSpeed = 40f; // visual tracer speed
public float shootAccuracyAngle = 5f; // random spread in degrees
[Header("Patrol (optional)")]
public float patrolRadius = 8f;
public float patrolWaitTime = 2f;
@@ -30,6 +38,7 @@ public class HumanoidEnemy : MonoBehaviour
private Vector3 spawnPoint;
private Vector3 patrolTarget;
private float nextAttackTime;
private float nextShootTime;
private float verticalVelocity;
private float patrolWaitTimer;
private bool isAggro = false;
@@ -49,8 +58,8 @@ public class HumanoidEnemy : MonoBehaviour
controller = GetComponent<CharacterController>();
health = GetComponent<EnemyHealth>();
// Find player
GameObject playerObj = GameObject.Find("Player");
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj == null) playerObj = GameObject.Find("Player");
if (playerObj != null)
player = playerObj.transform;
@@ -70,6 +79,11 @@ public class HumanoidEnemy : MonoBehaviour
attackDamage = 10f;
attackCooldown = 1.2f;
chaseRange = 20f;
canShoot = true;
shootRange = 16f;
shootDamage = 8f;
shootCooldown = 1.4f;
shootAccuracyAngle = 6f;
if (health != null) health.maxHealth = 80f;
break;
@@ -78,6 +92,7 @@ public class HumanoidEnemy : MonoBehaviour
attackDamage = 25f;
attackCooldown = 2f;
chaseRange = 18f;
canShoot = false; // Brute is pure melee
if (health != null) health.maxHealth = 200f;
break;
@@ -86,6 +101,11 @@ public class HumanoidEnemy : MonoBehaviour
attackDamage = 8f;
attackCooldown = 0.6f;
chaseRange = 30f;
canShoot = true;
shootRange = 22f;
shootDamage = 6f;
shootCooldown = 0.8f;
shootAccuracyAngle = 10f; // less accurate but fast
if (health != null) health.maxHealth = 50f;
break;
}
@@ -101,16 +121,26 @@ public class HumanoidEnemy : MonoBehaviour
float distToPlayer = Vector3.Distance(transform.position, player.position);
// Aggro check
if (distToPlayer <= chaseRange)
isAggro = true;
if (isAggro)
{
if (distToPlayer <= attackRange)
Attack();
{
// Close enough to punch
MeleeAttack();
}
else if (canShoot && distToPlayer <= shootRange)
{
// In shoot range — strafe / stand and fire
FacePlayer();
ShootAtPlayer();
}
else
{
ChasePlayer();
}
}
else
{
@@ -123,8 +153,16 @@ public class HumanoidEnemy : MonoBehaviour
verticalVelocity += gravity * Time.deltaTime;
controller.Move(Vector3.up * verticalVelocity * Time.deltaTime);
// Animate
AnimateModel(isAggro && distToPlayer > attackRange);
bool isWalking = isAggro && distToPlayer > attackRange && !(canShoot && distToPlayer <= shootRange);
AnimateModel(isWalking);
}
void FacePlayer()
{
Vector3 direction = (player.position - transform.position);
direction.y = 0f;
if (direction != Vector3.zero)
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 8f);
}
void ChasePlayer()
@@ -135,7 +173,6 @@ public class HumanoidEnemy : MonoBehaviour
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);
}
@@ -171,37 +208,106 @@ public class HumanoidEnemy : MonoBehaviour
return spawnPoint + new Vector3(randomCircle.x, 0f, randomCircle.y);
}
void Attack()
// ─── Melee ───
void MeleeAttack()
{
// Face the player
Vector3 direction = (player.position - transform.position);
direction.y = 0f;
if (direction != Vector3.zero)
transform.rotation = Quaternion.LookRotation(direction);
FacePlayer();
if (Time.time >= nextAttackTime)
{
nextAttackTime = Time.time + attackCooldown;
Debug.Log($"{gameObject.name} attacks for {attackDamage} damage!");
// Punch animation
StartCoroutine(PunchAnimation());
// Damage the player via Player.health
Player playerHealth = player.GetComponent<Player>();
if (playerHealth != null)
{
playerHealth.health -= attackDamage;
Debug.Log($"[Enemy] Player health: {playerHealth.health}");
Debug.Log($"[Enemy] Melee hit! Player health: {playerHealth.health}");
}
}
}
// ─── Shooting ───
void ShootAtPlayer()
{
if (Time.time < nextShootTime) return;
nextShootTime = Time.time + shootCooldown;
StartCoroutine(ShootAnimation());
// Aim at player's centre-ish (chest height) with some spread
Vector3 aimOrigin = headTransform != null
? headTransform.position
: transform.position + Vector3.up * 1.4f;
Vector3 aimTarget = player.position + Vector3.up * 0.9f; // aim for chest
Vector3 aimDir = (aimTarget - aimOrigin).normalized;
// Apply accuracy spread
aimDir = Quaternion.Euler(
Random.Range(-shootAccuracyAngle, shootAccuracyAngle),
Random.Range(-shootAccuracyAngle, shootAccuracyAngle),
0f) * aimDir;
RaycastHit hit;
if (Physics.Raycast(aimOrigin, aimDir, out hit, shootRange * 1.5f))
{
// Spawn a visual tracer
StartCoroutine(SpawnBulletTracer(aimOrigin, hit.point));
Player playerHealth = hit.transform.GetComponentInParent<Player>();
if (playerHealth != null)
{
playerHealth.health -= shootDamage;
Debug.Log($"[Enemy] Shot hit player! Player health: {playerHealth.health}");
}
}
else
{
// Missed — still show tracer going into the distance
StartCoroutine(SpawnBulletTracer(aimOrigin, aimOrigin + aimDir * shootRange));
}
}
IEnumerator SpawnBulletTracer(Vector3 from, Vector3 to)
{
GameObject tracer = GameObject.CreatePrimitive(PrimitiveType.Cube);
tracer.name = "EnemyBulletTracer";
Destroy(tracer.GetComponent<Collider>());
float length = Vector3.Distance(from, to);
tracer.transform.localScale = new Vector3(0.02f, 0.02f, Mathf.Min(length, 0.3f));
Renderer r = tracer.GetComponent<Renderer>();
Material mat = new Material(Shader.Find("Standard"));
mat.color = new Color(1f, 0.9f, 0.3f);
// Make it emissive so it's visible
mat.EnableKeyword("_EMISSION");
mat.SetColor("_EmissionColor", new Color(1f, 0.8f, 0.1f) * 2f);
r.material = mat;
float travelTime = length / bulletSpeed;
float elapsed = 0f;
while (elapsed < travelTime)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / travelTime);
Vector3 pos = Vector3.Lerp(from, to, t);
tracer.transform.position = pos;
tracer.transform.rotation = Quaternion.LookRotation((to - from).normalized);
yield return null;
}
Destroy(tracer);
}
// ─── Procedural Humanoid Model ───
void BuildHumanoidModel()
{
// Sizing based on type
float scale = 1f;
Color skinColor = new Color(0.6f, 0.45f, 0.35f);
Color shirtColor;
@@ -211,16 +317,16 @@ public class HumanoidEnemy : MonoBehaviour
{
case EnemyType.Grunt:
scale = 1f;
shirtColor = new Color(0.4f, 0.25f, 0.2f); // brown shirt
shirtColor = new Color(0.4f, 0.25f, 0.2f);
break;
case EnemyType.Brute:
scale = 1.3f;
shirtColor = new Color(0.5f, 0.1f, 0.1f); // red shirt
shirtColor = new Color(0.5f, 0.1f, 0.1f);
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
shirtColor = new Color(0.2f, 0.35f, 0.2f);
break;
default:
shirtColor = Color.gray;
@@ -232,13 +338,17 @@ public class HumanoidEnemy : MonoBehaviour
bodyRoot.localPosition = Vector3.zero;
bodyRoot.localRotation = Quaternion.identity;
// Head
// Head — kept with a collider and EnemyHeadHitbox for headshots
headTransform = CreatePart("Head", bodyRoot,
new Vector3(0f, 1.65f, 0f) * scale,
new Vector3(0.3f, 0.3f, 0.3f) * scale,
skinColor, PrimitiveType.Sphere);
skinColor, PrimitiveType.Sphere, keepCollider: true);
// Eyes (two small dark spheres)
// Wire up head hitbox
EnemyHeadHitbox headHitbox = headTransform.gameObject.AddComponent<EnemyHeadHitbox>();
headHitbox.enemyHealth = health;
// Eyes
CreatePart("LeftEye", headTransform,
new Vector3(-0.08f, 0.03f, 0.12f),
new Vector3(0.08f, 0.08f, 0.05f),
@@ -253,39 +363,38 @@ public class HumanoidEnemy : MonoBehaviour
torsoTransform = CreatePart("Torso", bodyRoot,
new Vector3(0f, 1.15f, 0f) * scale,
new Vector3(0.45f, 0.6f, 0.25f) * scale,
shirtColor, PrimitiveType.Cube);
shirtColor, PrimitiveType.Cube, keepCollider: true);
// Left Arm
// Arms
leftArm = CreatePart("LeftArm", bodyRoot,
new Vector3(-0.35f, 1.15f, 0f) * scale,
new Vector3(0.15f, 0.55f, 0.15f) * scale,
skinColor, PrimitiveType.Cube);
skinColor, PrimitiveType.Cube, keepCollider: true);
// 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);
skinColor, PrimitiveType.Cube, keepCollider: true);
// Left Leg
// Legs
leftLeg = CreatePart("LeftLeg", bodyRoot,
new Vector3(-0.12f, 0.4f, 0f) * scale,
new Vector3(0.18f, 0.6f, 0.18f) * scale,
pantsColor, PrimitiveType.Cube);
pantsColor, PrimitiveType.Cube, keepCollider: true);
// 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);
pantsColor, PrimitiveType.Cube, keepCollider: true);
// 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)
/// <param name="keepCollider">True to leave the collider on so raycasts can hit this part.</param>
Transform CreatePart(string name, Transform parent, Vector3 localPos, Vector3 localScale, Color color,
PrimitiveType shape = PrimitiveType.Cube, bool keepCollider = false)
{
GameObject part = GameObject.CreatePrimitive(shape);
part.name = name;
@@ -294,9 +403,11 @@ public class HumanoidEnemy : MonoBehaviour
part.transform.localScale = localScale;
part.transform.localRotation = Quaternion.identity;
// Remove collider from body parts (CharacterController handles collision)
Collider col = part.GetComponent<Collider>();
if (col != null) Destroy(col);
if (!keepCollider)
{
Collider col = part.GetComponent<Collider>();
if (col != null) Destroy(col);
}
Renderer rend = part.GetComponent<Renderer>();
rend.material = new Material(Shader.Find("Standard"));
@@ -317,7 +428,6 @@ public class HumanoidEnemy : MonoBehaviour
float limbSwing = Mathf.Sin(animTimer) * 30f;
// Arms swing opposite to legs
if (leftArm != null)
leftArm.localRotation = Quaternion.Euler(limbSwing, 0f, 0f);
if (rightArm != null)
@@ -327,7 +437,6 @@ public class HumanoidEnemy : MonoBehaviour
if (rightLeg != null)
rightLeg.localRotation = Quaternion.Euler(limbSwing, 0f, 0f);
// Slight torso bob
if (torsoTransform != null)
{
Vector3 torsoPos = torsoTransform.localPosition;
@@ -338,7 +447,6 @@ public class HumanoidEnemy : MonoBehaviour
}
else
{
// Return to idle pose
animTimer = 0f;
if (leftArm != null)
leftArm.localRotation = Quaternion.Slerp(leftArm.localRotation, Quaternion.identity, Time.deltaTime * 5f);
@@ -355,7 +463,6 @@ public class HumanoidEnemy : MonoBehaviour
{
if (rightArm == null) yield break;
// Wind up
float timer = 0f;
float punchDuration = 0.15f;
@@ -367,7 +474,6 @@ public class HumanoidEnemy : MonoBehaviour
yield return null;
}
// Snap back
timer = 0f;
while (timer < punchDuration)
{
@@ -380,19 +486,57 @@ public class HumanoidEnemy : MonoBehaviour
rightArm.localRotation = Quaternion.identity;
}
IEnumerator ShootAnimation()
{
if (rightArm == null) yield break;
float timer = 0f;
float duration = 0.1f;
// Raise arm to aim
while (timer < duration)
{
timer += Time.deltaTime;
float t = timer / duration;
rightArm.localRotation = Quaternion.Euler(Mathf.Lerp(0f, -70f, t), 0f, 0f);
yield return null;
}
yield return new WaitForSeconds(0.05f);
// Recoil kick back
rightArm.localRotation = Quaternion.Euler(-60f, 0f, 0f);
yield return new WaitForSeconds(0.08f);
// Return
timer = 0f;
while (timer < duration * 2f)
{
timer += Time.deltaTime;
float t = timer / (duration * 2f);
rightArm.localRotation = Quaternion.Euler(Mathf.Lerp(-70f, 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
if (canShoot)
{
Gizmos.color = new Color(1f, 1f, 0f, 0.2f);
Gizmos.DrawWireSphere(Application.isPlaying ? transform.position : transform.position, shootRange);
}
Gizmos.color = new Color(0f, 0.5f, 1f, 0.15f);
Gizmos.DrawWireSphere(Application.isPlaying ? spawnPoint : transform.position, patrolRadius);
}

View File

@@ -209,9 +209,20 @@ public class SimpleGun : MonoBehaviour
Debug.Log($"Hit: {hit.transform.name}");
SpawnImpactEffect(hit.point, hit.normal);
EnemyHealth enemy = hit.transform.GetComponent<EnemyHealth>();
if (enemy != null)
enemy.TakeDamage(damage);
// Head hitbox = one shot kill
EnemyHeadHitbox head = hit.transform.GetComponent<EnemyHeadHitbox>();
if (head != null && head.enemyHealth != null)
{
Debug.Log("HEADSHOT! Instakill.");
head.enemyHealth.TakeDamage(head.enemyHealth.maxHealth * 10f);
}
else
{
// Body shot — check the hit object AND walk up to parent for EnemyHealth
EnemyHealth enemy = hit.transform.GetComponentInParent<EnemyHealth>();
if (enemy != null)
enemy.TakeDamage(damage);
}
}
}

View File

@@ -37,13 +37,15 @@ public class WeaponBob : MonoBehaviour
bobTimer += Time.deltaTime * bobFrequency * multiplier;
float bobX = Mathf.Sin(bobTimer) * bobHorizontalAmplitude * multiplier;
float bobY = Mathf.Sin(bobTimer * 2f) * bobVerticalAmplitude * multiplier;
// Abs(Sin) means the weapon bounces UP on every step (left and right)
// instead of looping in a circle/figure-8
float bobY = Mathf.Abs(Mathf.Sin(bobTimer)) * bobVerticalAmplitude * multiplier;
transform.localPosition = originalLocalPos + new Vector3(bobX, bobY, 0f);
}
else
{
bobTimer = 0f;
// Don't reset bobTimer so the phase doesn't snap when you start moving again
transform.localPosition = Vector3.Lerp(
transform.localPosition,
originalLocalPos,