Unity太空避障Demo总结

发布于:2024-06-27 ⋅ 阅读:(182) ⋅ 点赞:(0)

太空避障:主要是实现飞机躲避子弹
面板基类、音乐类、排行榜类、json等等都和上一篇Unity坦克迷宫Demo总结一样,太空避障主要是对四元数的练习和使用
1.选择飞机面板
(1)通过左右按钮对显示的模型进行切换
(2)通过点击鼠标左键可以实现对飞机的拖动 观察飞机的整体
知识点
(1)四元数 * 四元数:会得到一个新的四元数,大多数用于旋转量相乘,相当于旋转(这个旋转的坐标是物体自身的坐标系)
(2)四元数 * 向量:会得到一个新的向量,对一个向量进行一定旋转,相当于旋转向量

public class SelectPanel : BasePanel<SelectPanel>
{
    public Button btnRight;
    public Button btnLeft;
    public Button btnStart;
    public Button btnClose;

    //标识当前已经创建的飞机 当切换时要删除该飞机
    private GameObject nowAirObj;
    //飞机的父物体位置
    public Transform airPos;

    //飞机的属性列表 通过隐藏和显示GameObject实现
    public GameObject[] hpList;
    public GameObject[] speedList;
    public GameObject[] voluemeList;

    public override void Init()
    {
        //在GameDataManager中保存了当前选中的飞机id 默认从0开始 点右按钮++ 左按钮-- 还有对边界的判断
        btnRight.onClick.AddListener(() =>
        {
            ++GameDataManager.Instance.nowSelectAir;
            if(GameDataManager.Instance.nowSelectAir > GameDataManager.Instance.airData.Count - 1)
                GameDataManager.Instance.nowSelectAir = 0;
            ChangAir();
        });
        btnLeft.onClick.AddListener(() =>
        {
            --GameDataManager.Instance.nowSelectAir;
            if (GameDataManager.Instance.nowSelectAir < 0)
                GameDataManager.Instance.nowSelectAir = GameDataManager.Instance.airData.Count - 1;
            ChangAir();
        });
        btnStart.onClick.AddListener(() =>
        {
            SceneManager.LoadScene("GameScene");
        });
        btnClose.onClick.AddListener(() =>
        {
            BeginPanel.Instance.ShowPanel();
            HidePanel();
        });
        HidePanel();
    }

    //重写了隐藏和显示方法 这样就可以同时实现一些逻辑
    public override void ShowPanel()
    {
        base.ShowPanel();
        GameDataManager.Instance.nowSelectAir = 0;
        ChangAir();
    }

    public override void HidePanel()
    {
        base.HidePanel();
        DestroyObj();
    }
    public void ChangAir()
    {
        //删除上个飞机 创建新飞机 并记录
        DestroyObj();
        AirData airData = GameDataManager.Instance.airData[GameDataManager.Instance.nowSelectAir];
        nowAirObj = Instantiate(Resources.Load<GameObject>(airData.res));
        nowAirObj.transform.SetParent(airPos.transform, false);

        for(int i = 0; i < hpList.Length;i++)
        {
            hpList[i].SetActive(i < airData.hp);
            speedList[i].SetActive(i < airData.speed);
            voluemeList[i].SetActive(i < airData.volume);
        }
    }

    public void DestroyObj()
    {
        if(nowAirObj != null)
        {
            Destroy(nowAirObj);
        }
    }

    private float time;
    //点中可以进行拖动
    public bool isSelect;
    private void Update()
    {
        //让飞机上下缓缓飞(展示作用)通过sin函数 乘内测的数代表飞的频率 乘外侧的数代表飞的位移
        //基于世界坐标 飞机的展示面会倾斜 基于自己坐标是错的
        time += Time.deltaTime;
        this.airPos.Translate(Vector3.up * Mathf.Sin(time) * 0.001f,Space.World);

        //鼠标左键实现拖动 通过射线检测 将飞机的旋转角度以y旋转 度数为Mouse X * 10度
        if(Input.GetMouseButtonDown(0))
        {
            if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), 1000, 1 << LayerMask.NameToLayer("Air")))
            {
                isSelect = true;
            }
        }
        if(Input.GetMouseButtonUp(0))
        {
            isSelect = false;
        }
        if(isSelect && Input.GetMouseButton(0))
        {
            airPos.rotation *= Quaternion.AngleAxis(Input.GetAxis("Mouse X") * 10,Vector3.up);
        }
    }
}

2.玩家类(飞机超出屏幕问题未解决)
(1)当飞机左右移动时,需要有一定的倾斜,停止时恢复
(2)玩家类需要受伤 死亡 移动方法
知识点
(1)射线检测时销毁碰撞体(子弹)

public class PlayerObj : MonoBehaviour
{
    //写成一个单例类
    private static PlayerObj instance;
    public static PlayerObj Instance => instance;

    //maxHp代表最大血量 nowHp代表变量
    public int nowHp;
    public int maxHp;

    public int moveSpeed;
    public int rotaSpeed;

    //是否死亡
    public bool isDead;

    //目标四元数旋转角度
    Quaternion targetQ;

    private float hValue;
    private float vValue;

    //世界坐标的点 没有去屏幕外的点
    private Vector3 frontPos = new Vector3(-41,0,23);
    private Vector3 frontPos2 = new Vector3(41, 0, -23);
    //世界坐标转换为屏幕坐标的点
    private Vector3 nowPos;

    private void Awake()
    {
        instance = this;
    }
    void Update()
    {
        if (isDead)
            return;

        //GetAxisRaw 不是渐变 -1 1两个数
        //ad
        hValue = Input.GetAxisRaw("Horizontal");
        //ws
        vValue = Input.GetAxisRaw("Vertical");

        //为0时说明静止
        if (hValue == 0)
            targetQ = Quaternion.identity;
        else
            //不为0时 按左键值会小于0 右键值会大于0 决定一下旋转的方向
            //可以得到一个目标的四元数
            targetQ = hValue < 0 ? Quaternion.AngleAxis(20, Vector3.forward) : Quaternion.AngleAxis(-20, Vector3.forward);

        //趋近于目标四元数
        this.transform.rotation = Quaternion.Slerp(this.transform.rotation, targetQ,Time.deltaTime * rotaSpeed);
        //正常移动
        this.transform.Translate(Vector3.forward * vValue * Time.deltaTime * moveSpeed);
        //左右移动不能使用自身坐标系 越动越会往下掉 要使用世界
        this.transform.Translate(Vector3.right * hValue * Time.deltaTime * moveSpeed,Space.World);
        //当前坐标的屏幕坐标点
        this.nowPos = Camera.main.WorldToScreenPoint(this.transform.position);
        //******************临时判断屏幕边缘的逻辑 当分辨率变化时会出错 暂未解决******************
        if (nowPos.x <= 0)
        {
            //只管理x 不管理yz
            this.transform.position = new Vector3(frontPos.x, this.transform.position.y, this.transform.position.z);
        }
        if (nowPos.y <= 0)
        {
            this.transform.position = new Vector3(this.transform.position.x, this.transform.position.y, frontPos2.z);
        }
        if(nowPos.x >= Screen.width)
        {
            this.transform.position = new Vector3(frontPos2.x, this.transform.position.y, this.transform.position.z);
        }
        if (nowPos.y >= Screen.height)
        {
            this.transform.position = new Vector3(this.transform.position.x, this.transform.position.y, frontPos.z);
        }

        //获取碰到的对象的信息
        RaycastHit hit;
        if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hit, 1000, 1 << LayerMask.NameToLayer("Bullet")))
        {
            //通过hit可以得到他身上所有的信息
            BulletObj bullet = hit.transform.GetComponent<BulletObj>();
            bullet.Dead();
        }
    }
    public void Wound()
    {
        this.nowHp -= 1;
        Debug.Log("当前血量" + nowHp);
        GamePanel.Instance.ChangHp(nowHp);
        if(nowHp <= 0)
        {
            nowHp = 0;
            Dead();
        }
    }
    public void Dead()
    {
        isDead = true;
        EndPanel.Instance.ShowPanel();
    }
}

3.子弹类
(1)子弹的初始化数据 移动方式 生命周期 销毁以及碰撞检测

public class BulletObj : MonoBehaviour
{
    private BulletData data;

    private float time;

    /// <summary>
    /// 一个初始化方法 创建子弹时调用 传入目标数据
    /// </summary>
    /// <param name="id"></param>
    public void Init(int id)
    {
        data = GameDataManager.Instance.bulletDatas[id - 1];
        //生命周期 到一定时间自动销毁子弹
        Invoke("DelayDestroy", data.destroyTime);
    }
    //startPos代表起始位置 下面做四元数匀速运动用的
    private void Awake()
    {
        startPos = this.transform;
    }
    //销毁子弹 创建特效
    public void Dead()
    {
        GameObject effObj = Instantiate(Resources.Load<GameObject>(data.effRes));
        effObj.transform.position = this.transform.position;
        Destroy(effObj,0.5f);
        Destroy(this.gameObject);
    }

    //碰撞检测
    public void OnTriggerEnter(Collider other)
    {
        if(other.CompareTag("Player"))
        {
            if(!PlayerObj.Instance.isDead)
            {
                PlayerObj obj = other.GetComponent<PlayerObj>();
                obj.Wound();
                Dead();
            }
            else
            {
                this.Dead();
            }
        }
    }

    //不确定子弹会不会提前被移除掉 所以使用延迟函数 当子弹已经被移除的时候 就不会执行了
    public void DelayDestroy()
    {
        GameObject effObj = Instantiate(Resources.Load<GameObject>(data.effRes));
        effObj.transform.position = this.transform.position;
        Destroy(effObj, 0.5f);
        Destroy(this.gameObject);
    }
    //时间标识 做匀速运动用
    private float uniform;
    private Transform startPos;
    // Update is called once per frame
    void Update()
    {
        //共同的特征 面朝向移动
        this.transform.Translate(Vector3.forward * data.forwardSpeed * Time.deltaTime);
        //1 直线运动
        //2 曲线运动
        //3 左抛物线
        //4 右抛物线
        //5 跟踪
        switch (data.type)
        {
            case 2:
            //sin里面的值 决定 左右变化的频率
            //sin后面的值 决定 左右位移的多少
                time += Time.deltaTime;
                this.transform.Translate(Vector3.right * Mathf.Sin(time * data.rightSpeed) * data.rotaSpeed * Time.deltaTime);
                break;
            case 3:
                this.transform.rotation *= Quaternion.AngleAxis(-data.rotaSpeed * Time.deltaTime,Vector3.up);
                break;
            case 4:
                this.transform.rotation *= Quaternion.AngleAxis(data.rotaSpeed * Time.deltaTime, Vector3.up);
                break;
            case 5:
                //先快后慢 出现问题 "子弹无线接近目标旋转位置过远" 那要看一下是不是子弹的位置没有归0
                //这里使用匀速
                uniform += Time.deltaTime;
                Quaternion q = Quaternion.LookRotation(PlayerObj.Instance.transform.position - this.transform.position);
                this.transform.rotation = Quaternion.Slerp(this.startPos.rotation,q, uniform * data.rotaSpeed);
                break;
        }

    }
}

4.炮口点
(1)炮口的位置为上方三点 中间左右两点 下方三点
(2)炮口的初始角度(为了方便开火点四元数的计算)
(3)上下左右四个点的旋转角度是180度,四个角旋转角度都是90度,这样子弹才能合理地发射在屏幕内

public enum Type_PointType
{
    topLeft,
    topRight,
    topCenter,
    Left,
    Right,
    bottomLeft,
    bottomRight,
    bottomCenter
}
public class PointObj : MonoBehaviour
{
    public Type_PointType type = new Type_PointType();

    private Vector3 pos;

    //炮口的初始方向
    private Vector3 initPos;

    //炮口去拿到配置表中的数据 方便使用
    private int num;
    private float offset;
    private float delay;

    //全部的子弹数据
    List<BulletData> bullets;
    //单个开火数据
    public FireData fire;
    //单个子弹数据
    public BulletData bullet;
    // Update is called once per frame
    void Update()
    {
        //初始化各个炮口的位置
        UpdatePos();
        //每次发射子弹的随机数 以及旋转角度的设置
        Reset();
        //发射子弹
        Fire();
    }

    public void UpdatePos()
    {
        //我们需要找到在z这一层的切面 因为其他子弹也需要在这一层
        //先根据玩家的世界坐标找到屏幕坐标的z是多少
        //然后设置世界坐标的z给pos
        //最后用屏幕转为世界坐标 让子弹出现的位置都在这一层即可
        pos.z = 40;
        switch(type)
        {
            case Type_PointType.topLeft:
                pos.x = 0;
                pos.y = Screen.height;
                initPos = Vector3.right;
                break;
            case Type_PointType.topRight:
                pos.x = Screen.width;
                pos.y = Screen.height;
                initPos = Vector3.left;
                break;
            case Type_PointType.topCenter:
                pos.x = Screen.width/2;
                pos.y = Screen.height;
                initPos = Vector3.right;
                break;
            case Type_PointType.Left:
                pos.x = 0;
                pos.y = Screen.height/2;
                initPos = Vector3.right;
                break;
            case Type_PointType.Right:
                pos.x = Screen.width;
                pos.y = Screen.height/2;
                initPos = Vector3.left;
                break;
            case Type_PointType.bottomLeft:
                pos.x = 0;
                pos.y = 0;
                initPos = Vector3.right;
                break;
            case Type_PointType.bottomRight:
                pos.x = Screen.width;
                pos.y = 0;
                initPos = Vector3.left;
                break;
            case Type_PointType.bottomCenter:
                pos.x = Screen.width/2;
                pos.y = 0;
                initPos = Vector3.right;
                break;
        }
        this.transform.position = Camera.main.ScreenToWorldPoint(pos);
    }
    //旋转的角度
    private float angle;
    /// <summary>
    /// 
    /// 随机数 将子弹的四种移动方式和开火的两种方式随机排列组合 想实现的功能看个人
    /// bullet 四种类型1.直线 2.曲线 3.左抛物线 4.右抛物线 5.跟踪  
    /// fire 两种类型1.连发 2.散弹
    /// </summary>
    public void Reset()
    {
        //终止条件的判断 如果子弹间隔时间和数量没有发完 不能从新随机
        if (offset != 0 && num != 0)
            return;
        if(fire != null)
        {
            //delay代表每组子弹的间隔时间
            delay -= Time.deltaTime;
            if(delay > 0)
            {
                return;
            }
        }
        //拿到全部的开火数据 随机选择开火方式
        List<FireData> fires = GameDataManager.Instance.fireData;
        fire = fires[Random.Range(0, fires.Count)];
        //本地拿到初始值
        this.num = fire.num;
        this.offset = fire.offset;
        this.delay = fire.delay;
        //拿到全部的子弹数据 随机选择子弹移动方式
        bullets = GameDataManager.Instance.bulletDatas;
        bullet = bullets[Random.Range(0, bullets.Count)];

        //1.左上角和右下角发射右抛物线
        //2.左下角和右上角发射左抛物线
        //3.type = 3 是左抛物线 type = 4 是右抛物线
        if(fire.type == 2)
        {
            //2类型是散弹 需要随机角度
            switch (type)
            {
                case Type_PointType.topLeft:
                case Type_PointType.bottomLeft:
                case Type_PointType.topRight:
                case Type_PointType.bottomRight:
                    angle = 90 / num;
                    break;

                case Type_PointType.topCenter:
                case Type_PointType.Left:
                case Type_PointType.Right:
                case Type_PointType.bottomCenter:
                    angle = 180 / num;
                    break;
            }
        }
    }
    private Vector3 newRot;
    /// <summary>
    /// 开火 个人决定开火口能发射怎样的子弹
    /// 核心点:根据不同的开火方式 去创建子弹 以及子弹特效 子弹的位置 子弹的旋转角度
    /// </summary>
    public void Fire()
    {
        if (type == Type_PointType.topLeft || type == Type_PointType.bottomRight)
        {
            bullet.type = 4;
        }
        if (type == Type_PointType.topRight || type == Type_PointType.bottomLeft)
        {
            bullet.type = 3;
        }
        //间隔时间和数量归0 不会进入循环
        if (offset == 0 && num == 0)
            return;
        //每个子弹的判断放到最前面
        offset -= Time.deltaTime;
        //当间隔时间大于0 还在等待期 
        if (offset > 0)
            return;
        GameObject go;
        switch (fire.type)
        {
            case 1:
                go = Instantiate(Resources.Load<GameObject>(bullet.bulletRes));
                go.GetComponent<BulletObj>().Init(bullet.id);
                go.transform.position = this.transform.position;
                go.transform.rotation = Quaternion.LookRotation(PlayerObj.Instance.transform.position - this.transform.position);
                //子弹减少
                --num;
                //当子弹发射完成后 间隔时间归0 就不会再次进入循环 没发射完就重置间隔时间
                offset = num == 0 ? 0 : fire.offset;
                break;
            case 2:
                //散弹不能发射曲线子弹
                if (bullet.type != 2)
                {
                    for (int i = 0; i < num; i++)
                    {
                        go = Instantiate(Resources.Load<GameObject>(bullet.bulletRes));
                        go.GetComponent<BulletObj>().Init(bullet.id);
                        go.transform.position = this.transform.position;
                        //四元数乘向量 得到新的向量
                        //每次旋转i * 20 
                        newRot = Quaternion.AngleAxis(i * angle, Vector3.up) * initPos;
                        //新的方向给四元数 最后赋值给角度即可
                        go.transform.rotation = Quaternion.LookRotation(newRot);
                    }
                    offset = num = 0;
                }
                else
                {
                    Reset();
                }
                break;
        }
    }
}

5.Main调用
(1)最后通过Main放置在游戏场景,当进入场景时,根据选择的id创建飞机,初始化变量,更新界面的ui,其实放在GamePanel中也是一样的,都可以


网站公告

今日签到

点亮在社区的每一天
去签到