Unity杂谈:敌人视野感知的实现

文章写于2016-1-31,后有修改。

本文为本人原创,转载请注明。

以下为正文

……………………………………………………………………………………………………………………………………

 

游戏AI常常分为三大部分——感知层、分析层(包含记忆层)、决策层。

在早期的游戏中,AI只有分析层和决策层,虽然效果也是不错的,但是缺乏了真实感,直到《神偷》在感知层做出了巨大的突破后,游戏才逐渐变得真实有趣。

其中最有意思的是敌人视野的感知,比如说《盟军敢死队》。(童年回忆)

本文目的在于用Unity来实现一个观测敌人的炮台,做出类似盟军敢死队中敌人视野范围的效果(见下图):

视野范围感知的基本原理:

在Unity中,用一个Sphere Collider与角度差的计算来模拟敌人的视角。

当敌人进入Sphere Collider中后,进行角度差计算,如果目标与自己的角度差小于自己的视角,并且朝目标发射一条射线没有其他物体阻挡时,判断为看见敌人。完成后的场景图如下:

 

难点是射线碰撞的运用:

RaycastHit hitInfo;
if(Physics.Raycast(this.transform.position+Vector3.up,vec,out hitInfo,20)){
    GameObject gameObj = hitInfo.collider.gameObject;
    if(gameObj.tag == "Player"){
    //相关操作
    }
}

//若射线与物体产生碰撞,则整个表达式为true,否则为false
//起点为this.transform.position+Vector3.up
//方向为vec,这里的vec是指向目标的向量
//out hitInfo碰撞如果返回true,hitInfo将包含碰到器碰撞的更多信息。
//20为射线的长度
//注意:最后还有一个LayerMask参数未写进去,用于只选定Layermask层内的碰撞器,其它层内碰撞器忽略。

 

 

 

然后是敌人(炮台)的状态机设计图:

 

这里要注意的是,敌人有两种模式,正常模式和警戒模式。

警戒模式中,敌人的视野范围和视角都会变得更大,同时视角灯光会呈现黄色。

警戒模式在进入攻击模式时开启,再次回到正常模式时才会关闭。

由以上的情况,C#代码分为几大板块:

①角度差计算:计算目标与自身的角度差。

②目标是否可见的判断:判断目标是否在自身视野范围内。

③状态机:每种状态的切换以及相应要做的动作。

④目标进入与离开感知范围:捕捉目标GameObject或者释放目标GameObject

 

接下来是完整的C#代码:(当时技术较菜,直接用 switch 来写了)

 

using UnityEngine;
using System.Collections;

public class Enemy01Rewrite : MonoBehaviour
{

    //声音
    AudioSource m_audio;
    public AudioClip m_Shot;
    public AudioClip m_EnterAlert;
    public AudioClip m_ExitAlert;
    public AudioClip m_ExitWait;

    //功能
    GameObject Target = null;
    float DeltaShotTime = 0.5f;//攻击间隔
    float AngleSpeed = 90f;//转动速度
    float PerceiveRadius = 8f;//感知范围
    float AlertTime = 10f;//警报时间
    float WaitTime = 4f;//失去目标等待时间
    float LightRange = 0f;//灯光距离(在Start()中根据SpotLight参数设定)
    float LightIntensity = 0f;//灯光亮度(在Start()中根据SpotLight参数设定)
    float ViewAngle = 60f;//视野角度

    bool isInView = false;//是否在扇形视野范围内
    bool isRayCast = false;//是否通过射线能看到
    bool isAlert = false;//是否警报
    bool isWait = false;//是否等待

    float TargetAngle = 0;
    float SelfAngle = 0;
    float AngleDiff = 0;
    float MinAngle = 3f;

    public GameObject m_Light;
    public GameObject m_Bullet;
    public GameObject m_Launcher;
    int state = 0;
    float time_shot = 0;
    float time_alert = 0;
    float time_wait = 0;

    ///
    //角度差计算函数
    ///

    void CalculateAngle()
    {
        if (Target != null)
        {
            float AtanAngle = (Mathf.Atan((Target.transform.position.z - this.transform.position.z) /
            (Target.transform.position.x - this.transform.position.x))
            * 180.0f / 3.14159f);
            //Debug.Log (this.transform.rotation.eulerAngles+"   "+AtanAngle);

            //1象限角度转换
            if ((Target.transform.position.z - this.transform.position.z) > 0
               &&
            (Target.transform.position.x - this.transform.position.x) > 0
               )
            {
                TargetAngle = 90f - AtanAngle;
                //Debug.Log ("象限1 "+TargetAngle);
            }

            //2象限角度转换
            if ((Target.transform.position.z - this.transform.position.z) <= 0
               &&
            (Target.transform.position.x - this.transform.position.x) > 0
               )
            {
                TargetAngle = 90f + -AtanAngle;
                //Debug.Log ("象限2 "+TargetAngle);
            }

            //3象限角度转换
            if ((Target.transform.position.z - this.transform.position.z) <= 0
               &&
            (Target.transform.position.x - this.transform.position.x) <= 0
               )
            {
                TargetAngle = 90f - AtanAngle + 180f;
                //Debug.Log ("象限3 "+TargetAngle);
            }

            //4象限角度转换
            if ((Target.transform.position.z - this.transform.position.z) > 0
               &&
            (Target.transform.position.x - this.transform.position.x) <= 0
               )
            {
                TargetAngle = 270f + -AtanAngle;
                //Debug.Log ("象限4 "+TargetAngle);
            }


            //调整TargetAngle
            float OriginTargetAngle = TargetAngle;
            if (Mathf.Abs(TargetAngle + 360 - this.transform.rotation.eulerAngles.y)
               <
            Mathf.Abs(TargetAngle - this.transform.rotation.eulerAngles.y)
               )
            {
                TargetAngle += 360f;
            }
            if (Mathf.Abs(TargetAngle - 360 - this.transform.rotation.eulerAngles.y)
               <
            Mathf.Abs(TargetAngle - this.transform.rotation.eulerAngles.y)
               )
            {
                TargetAngle -= 360f;
            }

            //输出角度差
            AngleDiff = Mathf.Abs(TargetAngle - this.transform.rotation.eulerAngles.y);
            Debug.Log("角度差:" + TargetAngle + "(" + OriginTargetAngle + ")-" + this.transform.rotation.eulerAngles.y + "=" + AngleDiff);
        }
    }

    //
    //感知视野的相关计算 判断isRayCast和isInView
    //

    void JudgeView()
    {

        //感知角度相关计算
        if (Target != null)
        {
            //指向玩家的向量计算
            Vector3 vec = new Vector3(Target.transform.position.x - this.transform.position.x,
                                    0f,
                                    Target.transform.position.z - this.transform.position.z);

            //射线碰撞判断
            RaycastHit hitInfo;
            if (Physics.Raycast(this.transform.position + Vector3.up, vec, out
                               hitInfo, 20))
            {
                GameObject gameObj = hitInfo.collider.gameObject;
                //Debug.Log("Object name is " + gameObj.name);
                if (gameObj.tag == "Player")//当射线碰撞目标为boot类型的物品 ,执行拾取操作
                {
                    //Debug.Log("Seen!");
                    isRayCast = true;
                }
                else
                {
                    isRayCast = false;
                }
            }

            //画出碰撞线
            Debug.DrawLine(this.transform.position, hitInfo.point, Color.red, 1);
            //视野中的射线碰撞判断结束

            //视野范围判断
            //物体在范围角度内,警戒模式下范围为原来1.5倍
            if (AngleDiff * 2 <
               (isAlert ? ViewAngle * 1.5f : ViewAngle)
               )
            {
                isInView = true;
            }
            else
            {
                isInView = false;
            }
            //Debug.Log ("角度差 "+AngleDiff);

        }

    }



    // Use this for initialization

    void Start()
    {
        LightRange = m_Light.GetComponent<Light>().range;
        LightIntensity = m_Light.GetComponent<Light>().intensity;
        this.GetComponent<SphereCollider>().radius = PerceiveRadius;
        m_audio = this.GetComponent<AudioSource>();

    }

    // Update is called once per frame

    void Update()
    {

        //Debug.Log("state:" + state + " time_alert:" + time_alert);

        if (isAlert)
        {

            //警戒模式
            m_Light.GetComponent<Light>().range = LightRange * 2f;
            m_Light.GetComponent<Light>().color = new Color(1f, 1f, 0);
            m_Light.GetComponent<Light>().intensity = LightIntensity * 2f;
            m_Light.GetComponent<Light>().spotAngle = ViewAngle * 1.5f;
            this.GetComponent<SphereCollider>().radius = PerceiveRadius * 1.5f;

        }
        else
        {

            //正常模式
            m_Light.GetComponent<Light>().range = LightRange;
            m_Light.GetComponent<Light>().color = new Color(1f, 1f, 1f);
            m_Light.GetComponent<Light>().intensity = LightIntensity;
            m_Light.GetComponent<Light>().spotAngle = ViewAngle;
            this.GetComponent<SphereCollider>().radius = PerceiveRadius;

        }

        //计算角度差

        CalculateAngle();

        //感知视野判断(判断isRayCast与isInView)

        JudgeView();

        //状态机 共4个状态

        switch (state)
        {

            //状态1:正常模式——旋转扫视

            case 0:
                this.transform.Rotate(new Vector3(0, 1f, 0f), Time.deltaTime * AngleSpeed);

                //发现敌人 进入攻击模式

                if (isRayCast && isInView)
                {
                    if (!isAlert) m_audio.PlayOneShot(m_EnterAlert);
                    isAlert = true;
                    time_wait = 0;
                    state = 1;
                }
                break;

            //状态2:攻击模式——朝目标发射子弹

            case 1:
                //根据角度跟踪Target
                if (this.transform.rotation.eulerAngles.y < TargetAngle)
                {
                    if (AngleDiff >= MinAngle)
                    {
                        //顺时针旋转
                        this.transform.Rotate(new Vector3(0, 1f, 0f), Time.deltaTime * AngleSpeed);
                    }
                }
                else
                {
                    if (AngleDiff >= MinAngle)
                    {
                        //逆时针旋转
                        this.transform.Rotate(new Vector3(0, -1f, 0f), Time.deltaTime * AngleSpeed);
                    }
                }

                //子弹发射计时 子弹发射间隔
                time_shot += Time.deltaTime * 1;
                if (time_shot >= DeltaShotTime)
                {
                    Instantiate(m_Bullet, m_Launcher.transform.position, m_Launcher.transform.rotation);
                    time_shot = 0;
                    m_audio.PlayOneShot(m_Shot);
                }

                //敌人离开视野 进入等待模式

                if (!isRayCast)
                {
                    isWait = true;
                    time_wait = 0;
                    state = 2;
                }
                break;

            //状态3:敌人离开可见区域——等待

            case 2:

                //进入旋转警戒模式 计时
                time_wait += Time.deltaTime * 1;

                if (time_wait >= WaitTime)
                {
                    m_audio.PlayOneShot(m_ExitWait);
                    time_alert = 0;
                    isWait = false;
                    state = 3;
                }

                if (isRayCast && isInView)
                {
                    if (!isAlert) m_audio.PlayOneShot(m_EnterAlert);
                    isAlert = true;
                    time_wait = 0;
                    state = 1;
                }
                break;

            //状态4:警戒模式——旋转扫视

            case 3:

                //回到正常模式 计时
                time_alert += Time.deltaTime * 1;

                if (time_alert >= AlertTime)
                {
                    m_audio.PlayOneShot(m_ExitAlert);
                    time_alert = 0;
                    isAlert = false;
                    state = 0;
                }

                //发现敌人 进入攻击模式

                if (isRayCast && isInView)
                {
                    if (!isAlert) m_audio.PlayOneShot(m_EnterAlert);
                    isAlert = true;
                    time_wait = 0;
                    state = 1;
                }
                this.transform.Rotate(new Vector3(0, 1f, 0f), Time.deltaTime * AngleSpeed);
                break;
            default:
                break;
        }
    }

    //玩家进入感知层

    void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.tag == "Player")
        {
            Target = other.gameObject;
            //提前计算角度差
            CalculateAngle();
            time_shot = 0;
        }
    }

    //玩家进入视野

    void OnTriggerStay(Collider other)
    {
        if (other.gameObject.tag == "Player")
        {
            if (Target == null)
            {
                Target = other.gameObject;
            }
        }
    }

    //玩家离开感知层

    void OnTriggerExit(Collider other)
    {
        if (other.gameObject.tag == "Player")
        {
            Target = null;
            isInView = false;
            isRayCast = false;
        }
    }
}

 

 

 

  • 10
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值