这一篇是上一个DFS地图项目的后续。又学到了如何制作追踪式的AI和管理波数。
首先,我们先创建一个玩家,并且将它的移动写好。

移动脚本(俯视角度移动,看向鼠标方向)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class PlayerContol : LivingEntity
{
private Rigidbody rb;
private Vector3 moveInput;
[SerializeField] private float moveSpeed;
// private void Start() {
// rb = GetComponent<Rigidbody>();
// }
protected override void Start() {
base.Start();
rb = GetComponent<Rigidbody>();
}
private void Update() {
moveInput = new Vector3(Input.GetAxis("Horizontal"),0f,Input.GetAxis("Vertical"));
LookAtCursor();
}
private void FixedUpdate() {
rb.MovePosition(rb.position + moveInput * moveSpeed * Time.deltaTime);//这种看起来更连续
//rb.velocity = moveInput.normalized * moveSpeed;
}
private void LookAtCursor()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
Plane plane = new Plane(Vector3.up,Vector3.zero);
float distToGround;
if(plane.Raycast(ray,out distToGround))
{
Vector3 point = ray.GetPoint(distToGround);
Vector3 rightPoint = new Vector3(point.x,transform.position.y,point.z);
transform.LookAt(rightPoint);
}
}
}
PS:
1.有一个小细节,在上一篇DFS洪水填充算法中也有提及。就是刚体的移动方法使用velocity还是MovePosition()方法。上一篇中由于MovePosition()会造成穿墙(因为刚体碰撞其实是一个回推的过程,改变Position很容易就让刚体忽略这个回推,所以会穿墙,个人理解),所以使用了velocity。但是在这里,经过我测试之后,觉得MovePositon方法在视觉效果上感觉更连贯?不过这两个方法实际上都可以。
接下来我们设计敌人和玩家的基类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
//人物和敌人的基类
//C#也不支持多继承
public class LivingEntity : MonoBehaviour,IDamageable
{
public float maxHealth;//最大生命值
protected float health;//当前生命值,protected让子类可以访问这个变量
protected bool isDead;//是否死亡
//无论是玩家人物还是敌人都会阵亡,而阵亡是一个“事件”
//事件的简略声明格式:private/public + event key + Delagate + eventName(on + Foo)
public event Action onDeath;
// private void Start()
// {
// health = maxHealth;
// }
protected virtual void Start()
{
health = maxHealth;
}
//事件的触发是由拥有者的内部逻辑触发的
protected void Die()
{
isDead = true;
Destroy(gameObject);
if(onDeath != null)
{
onDeath();
//或者写成OnDeath.Invoke();
}
}
//这对敌人和人物的受到攻击的逻辑是相同的
//触发OnDeath事件时:
//人物:GameOver,敌人取消追击
//敌人:判断剩余数量是否为0
public void TakenDamage(float _damageAmount)
{
health -= _damageAmount;
if(health <=0 && isDead == false)
{
Die();
}
}
}
接口IDamageable:
public interface IDamageable//设计接口,最好用public
{
//所有的都是public,C#里不用写出来
void TakenDamage(float _damageAmount);
}
//接口其实是由C++抽象类进化而来,并且是纯抽象类,一个方法都没实现的那种
一些细节我都写在了代码之中,这里需要注意的是这个设计思路。比如说为什么把受到攻击的这个事件写成接口,因为场景中不是只有敌人和玩家会受到攻击,之后的墙体可能也会。而C#是单继承机制,所以你写成类内部的方法的话,类与类之间的关系可能就比较复杂了。因此,这里设计成为接口是最好的。
另外的事件订阅也是我新学到的东西,这个是和后面的敌人生成一起使用的,每当有敌人生成就订阅到这个Ondeath()事件上。虽然我还没有完全弄明白,但是我现在猜想这个事件是为了动态更新一些数据而存在。
然后是敌人的类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]//自动添加上导航组件
public class Enemy : LivingEntity
{
private NavMeshAgent navMeshAgent;
private Transform target;
[SerializeField] private float UpdateRate;
// private void Start() {
// navMeshAgent = GetComponent<NavMeshAgent>();
// //判定玩家是否还存活
// if(GameObject.FindGameObjectWithTag("Player") != null)
// {
// target = GameObject.FindGameObjectWithTag("Player").transform;
// }
// StartCoroutine(UpdatePath());
// }
protected override void Start()//使用重写的方法
{
base.Start();
navMeshAgent = GetComponent<NavMeshAgent>();
//判定玩家是否还存活
if(GameObject.FindGameObjectWithTag("Player") != null)
{
target = GameObject.FindGameObjectWithTag("Player").transform;
}
StartCoroutine(UpdatePath());
}
private void Update() {
//锲而不舍类AI
//navMeshAgent.SetDestination(target.position);//用SetDestination设置导航目标
}
//摸鱼类AI,使用协程实现
IEnumerator UpdatePath()
{
while(target!=null)
{
Vector3 preTargetPos = new Vector3(target.position.x,0,target.position.z);
navMeshAgent.SetDestination(preTargetPos);
yield return new WaitForSeconds(UpdateRate);
}
}
}
要点不算多,重写了继承自基类的Start方法,然后使用协程做了一个延迟,这样AI不会立即追踪玩家。然后追踪的方法使用的是NavMesh导航下的SetDestination()方法。
然后是子弹类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
[SerializeField] private float shootSpeed;
[SerializeField] private float damage = 1.0f;
[SerializeField] private float lifetime;
public LayerMask collisionLayerMask;//这一层之外的游戏对象不会被这个射线检测到
private void Start() {
Destroy(gameObject,lifetime);//生成十秒之后自动清除
}
private void Update() {
transform.Translate(Vector3.forward * shootSpeed * Time.deltaTime);
CheckCollision();//检测是否碰撞
}
private void CheckCollision(){
Ray ray = new Ray(transform.position,transform.forward);
RaycastHit hitInfo;//结构体类型:射线击中目标的具体信息
if(Physics.Raycast(ray,out hitInfo,shootSpeed*Time.deltaTime,collisionLayerMask,QueryTriggerInteraction.Collide))
{
//击中了敌人
HitEnemy(hitInfo);
}
}
private void HitEnemy(RaycastHit _hitInfo)
{
//IDamageable damageable = _hitInfo.collider.GetComponent<IDamageable>();//得到这个接口组件
IDamageable damageable = _hitInfo.collider.GetComponent<LivingEntity>();
if(damageable!=null)//击中的物体含有这个组件
{
damageable.TakenDamage(damage);
}
Destroy(gameObject);//这么写的话基本上是碰撞到就会销毁这个物体
}
}
这个子弹的判定是否碰撞很有意思,是从子弹上发出射线来检测是否有和其它物体碰撞。并且使用了一个遮罩层来屏蔽其它的物体造成干扰。我在想为什么不直接使用刚体的碰撞检测,如果有大佬知道的话,请务必赐教。
另外,贴上Physics.Raycast的完整参数(我也是第一次完整的使用这个函数)

最后就是自定义敌人波数生成了,使用了一个class来定义每一波的信息,用一个数组保存这些信息,然后动态检测每一波敌人剩下的数量就可以了。
//也可以使用Struct来定义波数信息
[System.Serializable]//让其在窗口中可视化更改
public class Wave
{
public int enemyNum;//每一波敌人的总数
public float timeBtwSpawn;//每一波敌人当中,前后敌人出现的时间间隔
}
using UnityEngine;
public class Spawner : MonoBehaviour
{
public GameObject enemyPrefab;//敌人的预制体
public Wave[] waves;//总共有多少波敌人
[Header("Just For Check!!!")]
[SerializeField]private Wave currentWave;//当前第几波
[SerializeField]private int currentIndex;//当前波数的索引
public int waitSpawnNum;//这一波还剩下多少敌人
public int spawnAliveNum;//这一波的敌人还存活了多少个,剩余0时切换到下一波
public float nextSpawnTime;//敌人生成间隔
private void Start()
{
NextWave();
}
private void NextWave()
{
currentIndex++;
if(currentIndex -1 <waves.Length)
{
currentWave = waves[currentIndex -1];//得到第一波
waitSpawnNum = currentWave.enemyNum;
spawnAliveNum = currentWave.enemyNum;
}
}
private void Update()
{
if(waitSpawnNum > 0 && Time.time > nextSpawnTime)
{
waitSpawnNum--;//等待生成的敌人-1
GameObject spawnEnemy = Instantiate(enemyPrefab,transform.position,Quaternion.identity);
//每当生成新的敌人,就要将这个敌人的“阵亡事件处理器”,订阅到事件onDeath上
spawnEnemy.GetComponent<Enemy>().onDeath += EnemyDeath;
nextSpawnTime = Time.time + currentWave.timeBtwSpawn;
}
}
//当敌人阵亡时,aliveNum-1,当spawnAliveNum = 0的时候,下一波
private void EnemyDeath()
{
spawnAliveNum--;
if(spawnAliveNum <=0)
{
NextWave();
}
}
}
6127

被折叠的 条评论
为什么被折叠?



