Unity3D學習(五):實現一個簡單的視覺感知

前言數組

在不少第一人稱或者第三人稱射擊遊戲的單人模式中,玩家的樂趣每每來源於和各式各樣的AI敵人的戰鬥。而戰鬥的爆發不少時候是由於這些AI在「看見」玩家後就會當即作出反應,好比開火、呼叫同伴、躲藏或者逃跑等。ide

因此這些AI究竟是如何探測,或者說」看到「玩家位置的?優化

別人的例子this

參考了知乎 給貓看的遊戲AI實戰(二)視覺感知初步 這篇文章。

這篇文章中,原做者讓玩家站在敵人的角度來探測目標,它經過向正前方必定扇形區域發射一堆射線來探測目標的位置,以下圖:spa

 這種方法雖然實現起來比較簡單,但它主要有兩個弊端:3d

1.同一時間內發射大量的射線,對遊戲自己的優化來講很很差,容易形成卡頓。code

2.若是要探測的物體比較小,甚至比兩條射線之間的間隔還小,那麼射線是沒法探測到這個物體的。orm

另外一種解決思路對象

原文的評論中,有人提到能夠基於探測者自身構建一個球體來探測周圍的物體。blog

所以咱們能夠用Unity自帶的Sphere觸發器或者Physics裏的OverLaps來構建一個球體探測區域,以下圖:

 這裏我用的是Overlaps,代碼以下:

玩家,即探測者

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour {
    public float moveSpeed;    //移動速度
    public float EyeViewDistance; //視野距離
    public float viewAngle = 120f; //視野角度

    private Rigidbody rb;
    private Collider[] SpottedEnemies; //附近的敵人
    // Use this for initialization
    void Start () {
        rb = GetComponent<Rigidbody>();
    }


    private void FixedUpdate()
    {
        DetectEnemy();
    }

    // Update is called once per frame
    void Update ()
    {
        //AutoMove();
        MoveAndTurn();
        Debug.DrawLine(transform.position, transform.forward * 100, Color.red); //紅色射線,面對的方向
    }


    void AutoMove()  //向面對的方向自動移動
    {
        transform.position += transform.forward * moveSpeed * Time.deltaTime;
    }

    void MoveAndTurn()  //玩家移動
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hitInfo = new RaycastHit();
        //shoot a ray from cam to mouse position which is only detected by gameobject with "Plane" layer.
        Physics.Raycast(ray, out hitInfo, 100, LayerMask.GetMask("Plane"));
        if (hitInfo.collider != null)
        {
            transform.LookAt(new Vector3(hitInfo.point.x, transform.position.y, hitInfo.point.z));
        }
        rb.velocity = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).normalized * moveSpeed;
    }

    void DetectEnemy()  //探測敵人
    {
        //OverlapSphere內的敵人
        SpottedEnemies = Physics.OverlapSphere(transform.position, EyeViewDistance, LayerMask.GetMask("Enemy"));
        for(int i = 0;i < SpottedEnemies.Length;i++) //檢測每個敵人是否在視野區中
        {
            Vector3 EnemyPosition = SpottedEnemies[i].transform.position; //敵人的位置

            //Debug.Log(transform.forward + " 面對的方向");
            //Debug.Log("夾角爲:" + Vector3.Angle(transform.forward, EnemyPosition - transform.position));

            Debug.DrawRay(transform.position, EnemyPosition - transform.position, Color.yellow); //玩家位置到敵人位置的向量
            if (Vector3.Angle(transform.forward, EnemyPosition - transform.position) <= viewAngle/2)  //這個敵人是否在視野內
            {
                //若是在視野內
                RaycastHit info = new RaycastHit();
                int layermask = LayerMask.GetMask("Enemy", "Obstacles"); //指定射線碰撞的對象
                Physics.Raycast(transform.position, EnemyPosition - transform.position, out info,EyeViewDistance,layermask); //向敵人位置發射射線
                Debug.Log(info.collider.gameObject.name);
                if(info.collider == SpottedEnemies[i]) //若是途中無其餘障礙物,那麼射線就會碰撞到敵人
                {
                    DiscoveredEnemy(SpottedEnemies[i]);
                }
            }
        }
    }

    void DiscoveredEnemy(Collider Enemy) //發現敵人
    {
        //Do something
        Debug.Log("發現敵軍:" + Enemy.gameObject.name);
        Enemy.GetComponent<Enemy>().BeDiscovered();
    }
}

SpottedEnemies是一個Collider數組,我用它來保存這一幀當中處於OverlapSphere造成的球體區域的全部敵人對象,(LayerMask可讓Overlaps的球體只和指定layer的對象發生交互)。而後計算玩家面對的方向和探測到的目標方向的夾角Vector3.Angle(transform.forward, EnemyPosition - transform.position),以下圖:

forwar向量表明玩家面朝的方向,v1表明探測到的物體相對於玩家位置的方向,紅色扇形區域表明玩家的視野範圍。那麼計算這兩個向量的夾角,而後判斷下這個夾角是否小於扇形區夾角的一半(即探測目標是否在玩家視野內)就好了。若是夾角小於視野夾角的一半,那麼咱們再向目標位置發射一根射線,而後看下射線碰撞到的物體是不是目標對象就行,由於若是玩家和目標之間有障礙物的話,那麼射線是會被障礙物擋下來的(也就是說玩家的"視野"被障礙物"遮擋"了)。

Enemy,被探測的目標

這裏設定被探測的目標脫離玩家視野必定時間後從新進入隱形狀態。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour {
    public float HideCoolDown = 0.3f; //隱藏本身的冷卻時間
    public float AppearTime;   //最後一次被發現的時刻
    private MeshRenderer mr;
    // Use this for initialization
    void Awake()
    {
        mr = GetComponent<MeshRenderer>();
    }

    private void Start()
    {
        mr.enabled = false;
    }

    // Update is called once per frame
    void Update ()
    {
        if (Time.time - AppearTime < HideCoolDown) 
            return;
        if (mr.enabled)
            mr.enabled = false;
    }

    public void BeDiscovered() //被發現了
    {
        mr.enabled = true;
        AppearTime = Time.time;
    }
}

最後實現的效果圖以下:

這種方法就不用發射大量的射線,並且只會對進入球形探測區域且處於視野範圍內的物體發射射線,而且也避免了小物體沒法被探測的bug。

 

參考資料

知乎:U3d開發中大部分事件都是用數學進行計算斷定的嗎? 最高票答案  做者:Meta42

貓看的遊戲AI實戰(二)視覺感知初步    做者:馬遙

相關文章
相關標籤/搜索