【Unity】使用导航网格简单的制作AI(包括波数管理)

这一篇是上一个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();
        }
    }

}

### 如何在 Unity 中隐藏导航网格Unity 中,导航网格(NavMesh)主要用于 AI 的路径规划和寻路功能。默认情况下,导航网格会在场景视图中显示为蓝色覆盖层,以便开发者能够直观了解可行走区域的位置和范围。然而,在某些开发阶段或者为了减少视觉干扰,可能希望隐藏这些导航网格。 以下是关于如何隐藏导航网格的方法: #### 方法一:通过 Scene View 面板设置 可以通过调整 **Scene View** 面板中的选项来控制导航网格的可见性。具体操作如下: 1. 打开 Unity 编辑器并进入场景视图。 2. 在场景视图左下角找到 **Gizmos** 下拉菜单。 3. 展开 Gizmos 菜单后取消勾选 `Navigation` 或者其子项 `NavMesh`[^1]。 这样可以快速切换导航网格的显示状态而无需更改任何脚本或项目配置。 #### 方法二:禁用 Navigation 组件 如果需要更彻底地隐藏导航网格,可以选择临时禁用当前场景中的所有导航网格数据。这通常涉及以下步骤: 1. 选择包含导航网格烘焙数据的对象(通常是整个场景根对象)。 2. 查找该对象上的 **NavMesh Surface** 或其他相关组件。 3. 将这些组件暂时停用或将它们移除以停止渲染导航网格效果[^2]。 需要注意的是,这种方法会影响运行时的行为,因此仅建议用于调试目的而非最终发布版本。 #### 方法三:自定义编辑器工具 对于高级用户来说,还可以创建一个简单的 Editor Script 来动态管理导航网格可视化行为。例如下面这个例子展示了如何利用 C# 实现一键开关导航网格的功能: ```csharp using UnityEditor; using UnityEngine; public class ToggleNavMeshVisibility : MonoBehaviour { [MenuItem("Tools/Hide NavMesh")] static void HideNavMesh() { SceneView.lastActiveSceneView.inGameCamera.gameObject.GetComponent<NavMeshSurface>().enabled = false; } [MenuItem("Tools/Show NavMesh")] static void ShowNavMesh() { SceneView.lastActiveSceneView.inGameCamera.gameObject.GetComponent<NavMeshSurface>().enabled = true; } } ``` 上述代码片段提供两个菜单命令分别用来隐藏和恢复导航网格展示[^1]。 --- ### 注意事项 尽管以上方法可以帮助您有效管理和定制工作流程中的导航网格表现形式,请注意以下几点: - 修改导航网格属性不会影响实际游戏性能,因为只有当启用了特定平台支持(如 Vulkan 设置)之后才会真正加载对应资源文件[^3]。 - 如果计划导出成品应用至不同设备类型(比如启用 VR 功能),务必确认所做改动是否符合目标硬件需求以及用户体验标准。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值