3D游戏编程 大作业 逃生

本文介绍了一个基于Unity3D的追逐逃生游戏,玩家需躲避巡逻兵追捕并收集钥匙。游戏采用学长的框架,添加了视野感知和动画修改。巡逻兵通过圆形碰撞器和射线检测感知玩家,存在视野角度限制。游戏尚无自动寻路功能,期待后续完善。文章还涉及了场景设计、控制器实现和代码解析。
摘要由CSDN通过智能技术生成

前言

这次的作业是在智能巡逻兵的基础上,改的一个新游戏。本来第七次作业想要效仿学长的3d大作,没想到各种fsm,欧拉角的应用之类的看得我发蒙;之后照着抄也是抄出一堆bug,不得已只好随便敷衍一下,做个简单的游戏。
现在有时间来做后,大作业依旧抄不明白在学长的框架上进行了规则,应用的修改,和视野可视化的添加。
但比较遗憾的是,没有自动寻路,地图不敢设置得太复杂,由衷希望后来的学弟能在这上面补齐功能,实现完整的追逐逃生游戏。

Asset文件:github
参考博客:学长博客

一、游戏规则

玩家需要躲避巡逻兵的追捕,集齐环境中的所有钥匙,最终逃生。
巡逻兵会沿一定轨迹行动,追击视野中的玩家。
玩家一旦被追上,立即死亡。

二、具体实现

由于本次框架基本与学长的一致,部分细节不在一一展示。

1.人物

动画器

在这里插入图片描述
动画器里,我删掉了后跳,额外增加了拾取,攻击与死亡的动画。这里本来还想加一个守卫原地警戒的动作,但找不到好的动画,只好不了了之。具体细节在学长博客可以了解,我就不班门弄斧了。

玩家模型

![在这里插入图片描述](https://img-blog.csdnimg.cn/965e36f3b2bb426cb4fb03c88cd19207.png

守卫模型(视野可视化详解)

在这里插入图片描述

视野感知的原理

首先,我先介绍一下视野感知的原理:(参考)
第一步,我们开启圆形的碰撞器(Sphere Collider),这可以理解为敌人的感知范围
第二步,一旦有玩家进入这个范围,便计算正前方与玩家区域的角度差,即视角
第三步,从敌人所在部分发射一条射线,射线如果可以不受障碍物影响击中敌人,即可见
感知范围间,一定视角内的可见玩家,就可以被敌人感知,如图。
在这里插入图片描述

模型解析

然后我们来拆解一下这个守卫模型
大的父对象Guard,搭载守卫的数据,动画,碰撞体等
在这里插入图片描述
ybot是直接套的人物模型,什么都不加,而且这里不搭载动画,因为父对象已经搭载了。
在这里插入图片描述
然后是view,需要额外添加碰撞器与视野的脚本。
在这里插入图片描述
最后是我们可视化的视野,Spot light: 聚光灯,类似手电筒(参考:
光源组件1
光源组件2)。
这里面主要注意以下属性:
Color 光的颜色。
Range 范围,控制点光源和聚光灯照射的范围距离,平行光没有这个属性。
Spot Angle 角度,控制聚光灯圆锥张角的大小,只有聚光灯有这个属性。
Intensity 强度 ,控制光照的强度。
在这里插入图片描述

代码解析(GuardView)

所用属性

	GameObject Target = null;
    public bool isInView = false;//是否在扇形视野范围内
    public bool isRayCast = false;//是否通过射线能看到
    public bool isAlert = false;//是否警报
    float PerceiveRadius = 8f;//感知范围
    // float AlertTime = 10f;//警报时间
    // float WaitTime = 4f;//失去目标等待时间
    float LightRange = 0f;//灯光距离(在Start()中根据SpotLight参数设定)
    float LightIntensity = 0f;//灯光亮度(在Start()中根据SpotLight参数设定)
    float ViewAngle = 60f;//视野角度
 
    float TargetAngle = 0;
    float AngleDiff = 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)
        {
            Debug.Log(Target);
            //指向玩家的向量计算
            Vector3 vec = new Vector3(Target.transform.position.x - this.transform.position.x,
                                    0f,
                                    Target.transform.position.z - this.transform.position.z);
            Debug.Log(this.transform.position);
            Debug.Log(Target.transform.position);
            Debug.Log(vec);
            
            //射线碰撞判断
            RaycastHit hitInfo;
            if (Physics.Raycast(this.transform.position, vec, out
                               hitInfo,20,LayerMask.GetMask("Player")|LayerMask.GetMask("Ground")))
            {
                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);
 
        }
 
    }

更新状态

    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(0.784f, 0.317f, 0.203f,1f);
            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(0.257f, 0.745f, 0.108f,1f);
            m_Light.GetComponent<Light>().intensity = LightIntensity;
            m_Light.GetComponent<Light>().spotAngle = ViewAngle;
            this.GetComponent<SphereCollider>().radius = PerceiveRadius;
 
        }

        //计算角度差
 
        CalculateAngle();
 
        //感知视野判断(判断isRayCast与isInView)
 
        JudgeView();
    }

进行感知

    //玩家进入感知层
 
    void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.tag == "Player")
        {
            Target = other.gameObject;
            //提前计算角度差
            CalculateAngle();
        }
    }
 
    //玩家进入视野
 
    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;
        }
    }
注意事项

实际上在运行游戏时,很容易发送碰撞事故,如:玩家在守卫身体上跳跃,守卫感知不到地面,探测射线卡在守卫体内等。这里就需要对Layer(层layer) 、LayerMask (遮罩层)进行修改(参考:特定碰撞),实现特定碰撞。一般有两种方式,第一种是直接在设置中声明:
在这里插入图片描述
另一种是在代码中声明:
我的层次是这样的在这里插入图片描述
这里拿射线探测举例,只探测玩家与墙壁即可:

RaycastHit hitInfo;
if (Physics.Raycast(this.transform.position, vec, out
       hitInfo,20,LayerMask.GetMask("Player")|LayerMask.GetMask("Ground")))

2.场景

守卫是沿矩形运动的,图也设置得比较简单。
在这里插入图片描述
其中,我除了学长使用的区域触发器,我额外加了门与钥匙的探测器和代码
在这里插入图片描述
在这里插入图片描述

3.控制器

常见的mvc模板就不再放出,这里展示一下总控制器和事务控制器。

FirstSceneController

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

public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController {
    public GuardFactory guard_factory;                               //巡逻者工厂
    public GuardActionManager action_manager;                        //运动管理器
    public int playerSign = -1;                                      //当前玩家所处哪个格子
    public GameObject player;                                        //玩家
    public UserGUI gui;                                              //交互界面

    private List<GameObject> guards;                                 //场景中巡逻者列表
    private int game_status = 0;                                  //游戏状态 0:游戏中 ; 1:游戏失败; 2:游戏胜利
    public int keynum = 0;                                          //钥匙数量           
    public bool tryget = false;                                     //正在尝试获得钥匙

    
    void Awake() {
        SSDirector director = SSDirector.GetInstance();
        director.CurrentScenceController = this;
        guard_factory = Singleton<GuardFactory>.Instance;
        action_manager = gameObject.AddComponent<GuardActionManager>() as GuardActionManager;
        gui = gameObject.AddComponent<UserGUI>() as UserGUI;
        LoadResources();
    }

    void Update() {
        for (int i = 0; i < guards.Count; i++) {
            guards[i].gameObject.GetComponent<GuardData>().playerSign = playerSign;
        }
    }


    public void LoadResources() {
        Instantiate(Resources.Load<GameObject>("Prefabs/Road"));
        player = Instantiate(
            Resources.Load("Prefabs/Player"), 
            new Vector3(-5, 8, 28), Quaternion.identity) as GameObject;
        guards = guard_factory.GetPatrols();

        for (int i = 0; i < guards.Count; i++) {
            action_manager.GuardPatrol(guards[i]);
        }
    }

    public int GetGamestatus() {
        return game_status;
    }

    public void Restart() {
        SceneManager.LoadScene("Scenes/mySence");
    }

    void OnEnable() {
        GameEventManager.ExitChange += PlayerExit;
        GameEventManager.GameoverChange += Gameover;
        GameEventManager.GetKeyChange += getkey;
        GameEventManager.GetKeyingChange += getkeying;
    }
    void OnDisable() {
        GameEventManager.ExitChange -= PlayerExit;
        GameEventManager.GameoverChange -= Gameover;
        GameEventManager.GetKeyChange -= getkey;
        GameEventManager.GetKeyingChange -= getkeying;
    }
    //游戏失败
    void Gameover() {
        game_status = 1;
    }
    //逃生
    void PlayerExit() {
        if(keynum == 2)game_status = 2;
    }
    //获得钥匙
    bool getkey(){
        if(tryget){
            keynum++;
            tryget = false;
            return true;
        }
        return false;
    }
    //玩家尝试拾取
    void getkeying(bool trying){
        tryget = trying;
    }
}

GameEventManager

public class GameEventManager : MonoBehaviour {
    public delegate void ExitEvent();
    public static event ExitEvent ExitChange;
    
    public delegate void GameoverEvent();
    public static event GameoverEvent GameoverChange;
    public delegate void GetKeyingEvent(bool trying);
    public static event GetKeyingEvent GetKeyingChange;
    public delegate bool GetKeyEvent();
    public static event GetKeyEvent GetKeyChange;

    public void PlayerExit() {
        if (ExitChange != null) {
            ExitChange();
        }
    }

    public void PlayerGameover(){
        if (GameoverChange != null) {
            GameoverChange();
        }
    }

    public void PlayerGetkeying(bool trying){
        if (GetKeyingChange != null) {
            GetKeyingChange(trying);
        }
    }
    public bool PlayerGetkey(){
        if (GetKeyChange != null) {
            return GetKeyChange();
        }
        return false;
    }
}

三、视频与效果图

在这里插入图片描述

逃生

四、总结

这位学长的基础真的很扎实,框架简洁干净又优雅,在学他框架的同时,我几乎把本学期前面学的东西全都复习了一次。最后,虽然大部分我是在学长的基础里修改的,但我也粗略地学会了动画的使用,也对游戏做出了一定的改进,对这次作业,本人还是很知足的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值