Episode 07
Some Loose Ends——一些待解决的问题
解决了Enemy攻击Player造成伤害,优化了Projectile在Enemy内部生成无法造成伤害以及Projectile在Enemy边界的问题,补充了相关注释,下一步可以利用TakeHit(float damage, RaycastHit hit)形成粒子效果。
Enemy
using System.Collections;
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class Enemy : LivingEnitity
{
public enum State { Idle, Chasing, Attacking };//定义状态枚举类型
State currentState;//定义当前状态
NavMeshAgent pathfinder;//定义导航网格代理
Transform target;//定义目标位置
LivingEnitity targetEnity;//定义目标
Material skinmaterial;//定义外皮材质
Color originalColour;//定义初始颜色
float attackDistanceThreshold = .5f;//攻击距离阈值
float timeBetweenAttacks = 1;//攻击间隔时间
float damage = 1;
float nextAttackTime;
float myCollisionRadius;//enemy的碰撞半径
float targetCollisionRadius;//player的碰撞半径
bool hasTarget;//是否存在目标
protected override void Start()
{
base.Start();
//追踪Player
pathfinder = GetComponent<NavMeshAgent>();
skinmaterial = GetComponent<Renderer>().material;
originalColour = skinmaterial.color;
if (GameObject.FindGameObjectWithTag("Player") != null)
{
currentState = State.Chasing;
target = GameObject.FindGameObjectWithTag("Player").transform;
hasTarget = true;//目标存在
targetEnity = target.GetComponent<LivingEnitity>();
targetEnity.OnDeath += OnTargetDeath;//添加OnTargetDeath方法,当health归0时调用。
myCollisionRadius = GetComponent<CapsuleCollider>().radius;
targetCollisionRadius = GetComponent<CapsuleCollider>().radius;
StartCoroutine(UpdatePath());
}
}
void OnTargetDeath()
{
hasTarget = false;
currentState = State.Idle;
}
void Update()
{
if (hasTarget)
{
if (Time.time > nextAttackTime)
{
float sqrDstToTarget = (target.position - transform.position).sqrMagnitude;
//间隔距离小于攻击距离阈值
if (sqrDstToTarget < Mathf.Pow(attackDistanceThreshold + myCollisionRadius + targetCollisionRadius, 2))
{
nextAttackTime = Time.time + timeBetweenAttacks;
StartCoroutine(Attack());
}
}
}
}
IEnumerator Attack()
{
currentState = State.Attacking;
pathfinder.enabled = false;//取消追逐
Vector3 orginalPosition = transform.position;//初始位置
Vector3 dirToTarget = (target.position - transform.position).normalized;//攻击方向
Vector3 attackPosition = target.position - dirToTarget * myCollisionRadius;//攻击到的位置
float attackSpeed = 3;
float percent = 0;
skinmaterial.color = Color.red;
bool hasAppliedDamage = false;
while (percent <= 1)
{
//percent为0.5时候完成了攻击
if (percent >= 0.5 && !hasAppliedDamage)
{
hasAppliedDamage = true;
targetEnity.TakeDamage(damage);
}
percent += Time.deltaTime * attackSpeed;
float interpolation = (-Mathf.Pow(percent, 2) + percent) * 4;
//线性位置变换,使攻击动作平滑
transform.position = Vector3.Lerp(orginalPosition, attackPosition, interpolation);
yield return null;
}
skinmaterial.color = originalColour;
currentState = State.Chasing;
pathfinder.enabled = true;
}
//不在Update里面使用SetDestination()防止计算开销过大
IEnumerator UpdatePath()
{
float refreshRate = .25f;//0.25s计算一次
while (hasTarget)
{
if (currentState == State.Chasing)
{
Vector3 dirToTarget = (target.position - transform.position).normalized;
//追逐player间隔一点距离
Vector3 targetPosition = target.position - dirToTarget * (myCollisionRadius + targetCollisionRadius + attackDistanceThreshold / 2);
//防止Die()后继续调用SetDestination()报错
if (!dead)
{
pathfinder.SetDestination(targetPosition);
}
}
yield return new WaitForSeconds(refreshRate);
}
}
}
LivingEntity
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LivingEnitity : MonoBehaviour, IDamageable
{
public float startingHealth;
protected float health;
protected bool dead;
public event System.Action OnDeath;
//赋初始生命值
protected virtual void Start()
{
health = startingHealth;
}
//被击中后health值减少到0,调用Die()
public void TakeHit(float damage, RaycastHit hit)
{
TakeDamage(damage);
}
public void TakeDamage(float damage)
{
health -= damage;
if (health <= 0 && !dead)
{
Die();
}
}
protected void Die()
{
dead = true;
if (OnDeath != null)
{
OnDeath();
}
Destroy(gameObject);
}
}
Projectile
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
//设置渲染层级
public LayerMask collisionMask;
float speed = 10;//移动速度
float damage = 1;//伤害
float lifetime = 3;//生命周期
float skinWidth = .1f;//EnemySkin宽度
private void Start()
{
//生命周期结束后销毁Projectile
Destroy(gameObject, lifetime);
//解决Projectile在Enemy内部生成时Bug
Collider[] initialCollisions = Physics.OverlapSphere(transform.position, .1f, collisionMask);
if (initialCollisions.Length > 0)
{
OnHitObject(initialCollisions[0]);
}
}
public void SetSpeed(float newSpeed)
{
speed = newSpeed;
}
void Update()
{
float moveDistance = Time.deltaTime * speed;
//判断是否发生碰撞
CheckCollisions(moveDistance);
//Projectile移动
transform.Translate(Vector3.forward * moveDistance);
}
void CheckCollisions(float moveDistance)
{
Ray ray = new Ray(transform.position, transform.forward);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, moveDistance + skinWidth, collisionMask, QueryTriggerInteraction.Collide))
{
OnHitObject(hit);
}
}
//生成在Enemy外,传入射线击中的物体后销毁
void OnHitObject(RaycastHit hit)
{
IDamageable damageableObject = hit.collider.GetComponent<IDamageable>();
if (damageableObject != null)
{
damageableObject.TakeHit(damage, hit);
}
Destroy(gameObject);
}
//生成在Enemy内,传入收集器的第一个物体销毁
void OnHitObject(Collider collider)
{
IDamageable damageableObject = collider.GetComponent<IDamageable>();
if (damageableObject != null)
{
damageableObject.TakeDamage(damage);
}
Destroy(gameObject);
}
}
Physics.OverlapSphere(Vector3 position, float radius, int layerMask):函数一旦被调用,将会返回以参数1为原点和参数2为半径的球体内“满足一定条件”的碰撞体集合,此时我们把这个球体称为 3D相交球。
IDamageable
using UnityEngine;
public interface IDamageable
{
void TakeHit(float damage, RaycastHit hit);
void TakeDamage(float damage);
}
拓展: