Files
OGG/Assets/Scripts/HumanoidEnemy.cs

544 lines
18 KiB
C#

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("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;
// Internal
private Transform player;
private CharacterController controller;
private EnemyHealth health;
private Vector3 spawnPoint;
private Vector3 patrolTarget;
private float nextAttackTime;
private float nextShootTime;
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<CharacterController>();
health = GetComponent<EnemyHealth>();
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj == null) 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;
canShoot = true;
shootRange = 16f;
shootDamage = 8f;
shootCooldown = 1.4f;
shootAccuracyAngle = 6f;
if (health != null) health.maxHealth = 80f;
break;
case EnemyType.Brute:
moveSpeed = 2.5f;
attackDamage = 25f;
attackCooldown = 2f;
chaseRange = 18f;
canShoot = false; // Brute is pure melee
if (health != null) health.maxHealth = 200f;
break;
case EnemyType.Runner:
moveSpeed = 8f;
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;
}
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);
if (distToPlayer <= chaseRange)
isAggro = true;
if (isAggro)
{
if (distToPlayer <= attackRange)
{
// Close enough to punch
MeleeAttack();
}
else if (canShoot && distToPlayer <= shootRange)
{
// In shoot range — strafe / stand and fire
FacePlayer();
ShootAtPlayer();
}
else
{
ChasePlayer();
}
}
else
{
Patrol();
}
// Gravity
if (controller.isGrounded && verticalVelocity < 0f)
verticalVelocity = -2f;
verticalVelocity += gravity * Time.deltaTime;
controller.Move(Vector3.up * verticalVelocity * Time.deltaTime);
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()
{
Vector3 direction = (player.position - transform.position);
direction.y = 0f;
direction.Normalize();
controller.Move(direction * moveSpeed * Time.deltaTime);
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);
}
// ─── Melee ───
void MeleeAttack()
{
FacePlayer();
if (Time.time >= nextAttackTime)
{
nextAttackTime = Time.time + attackCooldown;
StartCoroutine(PunchAnimation());
Player playerHealth = player.GetComponent<Player>();
if (playerHealth != null)
{
playerHealth.health -= attackDamage;
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()
{
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);
break;
case EnemyType.Brute:
scale = 1.3f;
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);
break;
default:
shirtColor = Color.gray;
break;
}
bodyRoot = new GameObject("BodyRoot").transform;
bodyRoot.SetParent(transform);
bodyRoot.localPosition = Vector3.zero;
bodyRoot.localRotation = Quaternion.identity;
// 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, keepCollider: true);
// 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),
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, keepCollider: true);
// 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, keepCollider: true);
rightArm = CreatePart("RightArm", bodyRoot,
new Vector3(0.35f, 1.15f, 0f) * scale,
new Vector3(0.15f, 0.55f, 0.15f) * scale,
skinColor, PrimitiveType.Cube, keepCollider: true);
// 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, keepCollider: true);
rightLeg = CreatePart("RightLeg", bodyRoot,
new Vector3(0.12f, 0.4f, 0f) * scale,
new Vector3(0.18f, 0.6f, 0.18f) * scale,
pantsColor, PrimitiveType.Cube, keepCollider: true);
controller.height = 1.9f * scale;
controller.radius = 0.3f * scale;
controller.center = new Vector3(0f, 0.95f * scale, 0f);
}
/// <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;
part.transform.SetParent(parent);
part.transform.localPosition = localPos;
part.transform.localScale = localScale;
part.transform.localRotation = Quaternion.identity;
if (!keepCollider)
{
Collider col = part.GetComponent<Collider>();
if (col != null) Destroy(col);
}
Renderer rend = part.GetComponent<Renderer>();
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;
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);
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
{
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;
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;
}
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;
}
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()
{
Gizmos.color = new Color(1f, 0f, 0f, 0.15f);
Gizmos.DrawWireSphere(transform.position, chaseRange);
Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f);
Gizmos.DrawWireSphere(transform.position, attackRange);
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);
}
}