中级_存档案例

存档案例

搭建游戏场景

  • 背景
  • 3*3建筑
  • 4个随机生成的怪物
  • 天花板/地板

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-erda7loL-1681389891115)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412085817097.png)]

简单的复制粘贴,没什么好说的

给每个墙装碰撞器

给每个都加上碰撞器,然后apply

创建背景板

3D游戏一般是创建一个plane作为背景板

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mYWn91rc-1681389891116)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412091114656.png)]

修改大小到足以占据整个屏幕

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xYqhqKew-1681389891117)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412091509074.png)]

通过材质球给背景材质,通过右边修改颜色

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CuCI4GYW-1681389891117)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412091707054.png)]

简单调整了下平行光和材质的颜色

给下枪

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JaAfVyoi-1681389891117)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412092046352.png)]

加更多灯光,创空物体,摆放所有灯光

创建点光源,每个方块里放一个[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TAf4LbHR-1681389891118)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412092713476.png)]

创建射击目标/怪物

先用一个隐形的Cube放在中间,不需要碰撞器也不需要渲染,只需要transform组件的坐标[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3xVYZtsa-1681389891118)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412134010442.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kI8ZFx9M-1681389891118)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412134258574.png)]

用状态机给蝙蝠加动画

添加Animation组件

拖拽赋值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8AqVj52R-1681389891119)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412134527201.png)]

添加碰撞器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c6QZhU1F-1681389891119)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412135102171.png)]

调整大小,没啥好说的

如法炮制另外四个怪物

控制怪物的随机生成

思路

不需要用实例化实现,直接控制是否禁用就好

先拿到怪物,注意在引擎内拖拽赋值

    //拿到怪物的数组
    public GameObject[] monsters;
关于死亡

在播放的时候需要关掉碰撞器,此时怪物是一个不可交互的状态;

然后动画播放结束后把物体禁用掉

随机激活方法
    //随机生成方法
    private void ActivateMonster()
    {
        //随机得到数组长度的任意一个索引
        int index = Random.Range(0, monsters.Length);

    }
拿到目前激活状态的怪物
    public GameObject activeMonster = null;
在激活方法里给activeMonster赋值并激活物品和碰撞器
        //激活怪物和组件
        activeMonster = monsters[index];
        activeMonster.SetActive(true);
        activeMonster.GetComponent<BoxCollider>().enabled = true;
注意
  • 游戏物品想激活使用 SetActive()

  • 组件想要激活使用 enabled

测试,在Start函数里调用激活方法
    private void Start()
    {
        ActivateMonster();
    }
通过遍历让怪物初始化
        //遍历数组 初始化
        foreach (GameObject monster in monsters)
        {
            monster.GetComponent<BoxCollider>().enabled = false;
            monster.SetActive(false);//保证数组里每个怪物在最开始都是被禁用的
        }

使用协程控制怪物的生命周期

什么是协程Coroutines

分部执行,遇到条件(yield return语句)时挂起,满足条件后才会继续执行后面的代码

样式
IEnumerator Alive Timer()
{
    yield retuen new WaitForSeconds(Randaom.Range(1,5))
        ActivateMonster();
}
 StartCoroutine(AliveTimer());
  • IEnumerator迭代器,返回expression值并保留当前位置,下次调用协程函数时从此处重新开始
  • yield return new WaitForSeconds()条件:等待X秒后执行
  • StartCoroutine() 调用协程
延时生成的实现

协程方法

    //协程,随机一个秒数延时执行
    IEnumerable AliveTimer()
    {
        yield return new WaitForSeconds(Random.Range(1, 5));
        //调用生成怪物方法
        ActivateMonster();
    }

调用协程

        //调用协程方法
        StartCoroutine(AliveTimer());
控制怪物的死亡时间

思路:设置几秒钟一个周期,怪物自动死亡然后又生成一个新怪物

继续用协程和迭代器实现,生成死亡时间

先写一个死亡方法

使用激活状态的怪物变成未激活状态

    //死亡方法
    private void DeActiveMonster()
    {
        //先判断activeMonster!=null,换言之,场上有激活
        if(activeMonster!= null)
        {
            //记下来就是把当前怪物设置为空,禁用怪物,禁用相关组件
            activeMonster = null;
            activeMonster.GetComponent<BoxCollider>().enabled = false;
            activeMonster.SetActive(false);
        }
    }
每次以不同时间调用死亡方法
    //死亡计时器
    IEnumerator DeathTimer()
    {
        yield return new WaitForSeconds(Random.Range(3, 5));//死亡倒计时
        DeActiveMonster();//倒计时结束后调用死亡方法
    }

调用死亡计时器的方法

注意:如果两个倒计时方法都在Start方法里面调用的话,这两个计时器是同时运行的,如果此时死亡方法的时间少于生成方法,就要出bug了,因为生成方法最大是5,所以死亡时间必须至少比5大才行

    IEnumerator AliveTimer()
    {

            yield return new WaitForSeconds(Random.Range(1, 5));
            //调用生成怪物方法
            ActivateMonster();

    }
改为在AliveTimer里面调用死亡方法
    //死亡方法
    private void DeActiveMonster()
    {
        //先判断activeMonster!=null,换言之,场上有激活
        if (activeMonster != null)
        {
            //记下来就是把当前怪物设置为空,禁用怪物,禁用相关组件
            activeMonster.GetComponent<BoxCollider>().enabled = false;
            activeMonster.SetActive(false);
            activeMonster = null;
        }
        StartCoroutine(AliveTimer());
    }
    //随机激活方法
    private void ActivateMonster()
    {
        //随机得到数组长度的任意一个索引
        int index = Random.Range(0, monsters.Length);
        //激活怪物和组件
        activeMonster = monsters[index];
        activeMonster.SetActive(true);
        activeMonster.GetComponent<BoxCollider>().enabled = true;
        StartCoroutine(DeathTimer());
    }

在随机生成的最后调用死亡方法,在死亡方法的最后调用生成方法,而start中的生成方法只调用了一次

设置手枪的旋转

思路:利用transform组件实现旋转,鼠标左右旋转就是x坐标改变,鼠标上下旋转就是y坐标改变

根据旋转可确定,枪的移动坐标均在60左右比较合适,只有最下面是例外,旋转到15左右就够了

设置旋转范围
    //枪的旋转范围
    public float maxYRotation = 120;
    public float minYRotation = 0;
    public float maxXRotation = 60;
    public float minXRotation = 0;
计时器,间隔射击

先拿到射击时间和计时器

    //射击时间和计时器
    private float shootTime = 1;
    private float shootTimer = 0;

    private void Update()
    {
        shootTimer += Time.deltaTime;
        if (shootTimer >= shootTime)
        {
            //TODO:可以射击

        }
用EulerAngles组件实现跟随

拿到鼠标位置

        //得到鼠标位置和比例
        float xPosPrecent = Input.mousePosition.x / Screen.width;
        float yPosPrecent = Input.mousePosition.y / Screen.height;

拿到移动的角度

//拿到旋转角度
        //因为上面是45,下面是15,所以需要做个加法处理
        float xAngle = -Mathf.Clamp(yPosPrecent * maxXRotation, minXRotation, maxXRotation)+15;
        float yAngle = Mathf.Clamp(xPosPrecent * maxYRotation, minYRotation, maxYRotation)-60;//得到最小值0的话,在这个基础上-60,就能向左旋转了,毕竟0在原点
    //角度给到枪的组件eulerAngles
    transform.eulerAngles = new Vector3(xAngle, yAngle, 0);//这样就能跟随摄像机移动了

控制子弹的生成

先用球体作为子弹

做个球,给材质

复制一个作为生成点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WQYMYKy0-1681389891119)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412163340464.png)]

这个生成点只需要transform的位置信息,其他组件都可以删掉,然后把这个给到枪的子物体

把子弹做成预制体
通过实例化动态生成子弹

先拿到子弹的预制体

    //子弹的预制体
    public GameObject bulletGO;
    public Transform firePosition;
射击方法

实例化生成子弹

        shootTimer += Time.deltaTime;
        if (shootTimer >= shootTime)
        {
            if (Input.GetMouseButtonDown(0))//按下鼠标左键
            {
                //定义一个子弹的物品,并在开火位置实例化生成子弹
                GameObject bulletCurrent = GameObject.Instantiate(bulletGO, firePosition.position, Quaternion.identity);
            }

        }

给子弹上刚体

在上完刚体之后给子弹施加力或者速度就可以飞了

控制子弹的移动

思路:通过施加力的方式

通过AddForce方法添加里

先拿到刚体组件,然后调用AddForce方法

            if (Input.GetMouseButtonDown(0))//按下鼠标左键
            {
                //定义一个子弹的物品,并在开火位置实例化生成子弹
                GameObject bulletCurrent = GameObject.Instantiate(bulletGO, firePosition.position, Quaternion.identity);
                //施加力 获得刚体组件 给子弹添加一个正前方向的力,达到让子弹向前运动的效果
                bulletCurrent.GetComponent<Rigidbody>().AddForce(transform.forward * 1000);
                shootTimer = 0;//在实例化子弹之后,把子弹的计时器归零
            }

设置手枪的动画与子弹的自动销毁

在代码里设置动画播放

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-30d3bC9u-1681389891120)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412165631279.png)]

现在枪的动画模块里关闭自动播放

                //先拿到枪的动画组件,然后用Play方法播放索引为0的动画
                gameObject.GetComponents<Animation>()[0].Play();

设置子弹自动销毁

思路:不管有没有碰撞到怪物,都有一个子弹自动销毁时间

    //用协程实现自动销毁
    private void Start()
    {
        StartCoroutine(Destoryer());

    }
    //计时迭代器
    IEnumerator Destoryer()
    {
        yield return new WaitForSeconds(3);
        Destroy(this.gameObject);
    }

子弹与怪物的碰撞

销毁子弹
    //碰撞销毁子弹
    private void OnCollisionEnter(Collision collision)
    {
        //通过Tag判断是否撞上了 Bullet
        if (collision.collider.tag == "Bullet")
        {
            //如果是子弹,把子弹销毁掉
            Destroy(collision.gameObject);
        }

    }
怪物播放死亡动画

先定义一个动画anim,然后让anim拿到动画组件

    public Animation anim;

    //在Awake里获得动画组件
    private void Awake()
    {
        anim = gameObject.GetComponent<Animation>();
    }

拿到两个动画

    //定义两个公共量去存放idle和die
    public AnimationClip idleClip;
    public AnimationClip dieClip;

初始为idle

        //设置动画初始是idle
        anim.clip = idleClip;

检测到和子弹的碰撞后,播放die

            //被子弹碰撞播放死亡动画
            anim.clip = dieClip;
            anim.Play();

把默认动画再禁用之后修改回idle状态,否则的话新出现的怪物直接就播放死亡动画,这部分代码卸载MonsterManager里

    //当怪物被禁用掉的时候,将默认的动画修改为idle状态
    private void OnDisable()
    {
        anim.clip = idleClip;
    }

怪物的死亡与刷新

播放死亡动画后在1s内销毁掉

因为手枪的射击间隔是1s,避免死亡之后还能打到,触发动画

不仅要在MonsterManager里面写,还要练习TargetManager

先关闭碰撞体
            //被击中后关闭碰撞体
            gameObject.GetComponent<BoxCollider>().enabled = false;
    //再写一个新的迭代器
    IEnumerator Deacitvate()
    {
        yield return new WaitForSeconds(0.5f);
        //是当前的怪物变成未激活状态,并且使整个循环重新开始
    }
    //通过射击调用的方法,会先停掉所有的协程
    public void UpdateMonsters()
    {
        if (activeMonster != null)
        {
            StopAllCoroutines();//停止所有的协程
            activeMonster.SetActive(false);//禁用掉怪物,因为怪物死亡的时候已经关闭了碰撞体,故这里不用重复操作了
            activeMonster = null;

            //重新开始设定生成计时器
            StartCoroutine(AliveTimer());

        }
    }
单例模式
    //做成单例模式
    public static TargetManager _instance;

为单例赋值

    //为单例赋值
    public void Awake()
    {
        _instance = this;//相当于这个instance是直接指向我们这个脚本的,在别处调用这个单例就相当于直接调用这个脚本
    }
在别的.cs下调用单例
    //再写一个新的迭代器
    IEnumerator Deacitvate()
    {
        yield return new WaitForSeconds(0.5f);
        //是当前的怪物变成未激活状态,并且使整个循环重新开始
        //使用单例
        TargetManager._instance.UpdateMonsters();
    }

制作统计得分的UI

创建画布

通过UI - text

选择2D模式方便观察

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ubPl6Okt-1681389891120)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412191605457.png)]

用脚本给Text赋值
先拿到两个UI
    //拿到两个Text
    public Text shootNumTet;
    public Text scoreTet;
分别写两个加分方法
    //射击数方法
    public void AddShootNum()
    {
        shootNum += 1;
    }
    //分数方法
    public void AddScore()
    {
        score += 1;
    }

拿到两个分数


    public int shootNum;
    public int score;
在Update方法里更新他们
    //更新这两个Text的内容
    public void Update()
    {
        //转换成字符串
        shootNumText.text = shootNumText.ToString();
        scoreText.text = scoreText.ToString();

    }
制作成单例模式
    //制作单例模式
    public static UIManager _instance;

    //给单例赋值
    private void Awake()
    {
        _instance = this;
    }
调用射击数单例模式
                //调用射击数的单例方法
                UIManager._instance.AddShootNum();
调用分数单例模式
            //撞击的时候分数加一,调用单例
            UIManager._instance.AddScore();

制作菜单UI

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2gpMztrm-1681389891120)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412193935549.png)]

在UI下创建一个panel

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CnASnrEC-1681389891121)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412194127556.png)]

在panel下创建image

缩放的时候按住alt键,就能围绕中心对称缩放

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AEkmdIVn-1681389891121)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412194357325.png)]

创建Button

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2wCsIsNG-1681389891121)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412195334463.png)]

暂停游戏和继续游戏

用bool值去定义是否暂停
    //定义一个暂停的bool值
    public bool isPaused = true;//开始的时候会先显示暂停菜单

拿到菜单的物品

    //拿到菜单的游戏物品
    public GameObject menuGO;
暂停方法
    //暂停方法
    private void Pause()
    {
        isPaused = true;
        menuGO.SetActive(true);//显示菜单的游戏物体
        //让后面的时间都暂停掉
        Time.timeScale = 0;
        Cursor.visible = true;//光标可见
    }
未暂停状态
//未暂停状态
//基本是跟上面反过来的
private void UnPause()
{
    isPaused = false;
    menuGO.SetActive(false);
    Time.timeScale = 1;
    Cursor.visible = false;
}
在游戏开始调用暂停方法

    //开局暂停
    private void Awake()
    {
        Pause();
    }
在Update方法里判断键盘输入
    //在Update方法里面判断键盘输入
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            Pause();
        }
    }
继续游戏的方法
//继续游戏
public void ContinueGame()
{
    UnPause();//把暂停状态变成未暂停状态
}
锁住枪的状态

先把GameManager设置成单例模式

    //把GameManager设置成单例模式
    public static GameManage _instance;

然后赋值

    private void Awake()
    {
        Pause();
        _instance = this;
    }

在GunManager里面调用单例


        //如果没暂停,下面这一大串才可以执行
        if (GameManage._instance.isPaused == false)
        {
            shootTimer += Time.deltaTime;
            if (shootTimer >= shootTime)
            {
                if (Input.GetMouseButtonDown(0))//按下鼠标左键
                {
                    //定义一个子弹的物品,并在开火位置实例化生成子弹
                    GameObject bulletCurrent = GameObject.Instantiate(bulletGO, firePosition.position, Quaternion.identity);
                    //施加力 获得刚体组件 给子弹添加一个正前方向的力,达到让子弹向前运动的效果
                    bulletCurrent.GetComponent<Rigidbody>().AddForce(transform.forward * 2500);
                    //先拿到枪的动画组件,然后用Play方法播放索引为0的动画
                    gameObject.GetComponents<Animation>()[0].Play();
                    shootTimer = 0;//在实例化子弹之后,把子弹的计时器归零
                                   //调用射击数的单例方法
                    UIManager._instance.AddShootNum();
                }



            }
            //得到鼠标位置和比例
            float xPosPrecent = Input.mousePosition.x / Screen.width;
            float yPosPrecent = Input.mousePosition.y / Screen.height;
            //拿到旋转角度
            //因为上面是45,下面是15,所以需要做个加法处理
            float xAngle = -Mathf.Clamp(yPosPrecent * maxXRotation, minXRotation, maxXRotation) + 15;
            float yAngle = Mathf.Clamp(xPosPrecent * maxYRotation, minYRotation, maxYRotation) - 60;//得到最小值0的话,在这个基础上-60,就能向左旋转了,毕竟0在原点

            //角度给到枪的组件eulerAngles
            transform.eulerAngles = new Vector3(xAngle, yAngle, 0);//这样就能跟随摄像机移动了
        }

新游戏和退出游戏

先创建一个空物体 Targets,用来存放所有Target[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZJleX38p-1681389891121)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230412202658570.png)]

只要每个格子里都放一组怪物就能做到全部随机

修改Update方法

如果激活的怪物等于空,就直接刷新

    public void UpdateMonsters()
    {
        StopAllCoroutines();//停止所有的协程
        if (activeMonster != null)
        {
            activeMonster.SetActive(false);//禁用掉怪物,因为怪物死亡的时候已经关闭了碰撞体,故这里不用重复操作了
            activeMonster = null;
        }
        //重新开始设定生成计时器
        StartCoroutine(AliveTimer());
    }

在GaeManager里写新游戏方法

先拿到所有怪物的数组

    //拿到所有怪物的数组
    public GameObject[] targetsGO;

循环遍历数组里的每一个

       //新游戏方法
    public void NewGame()
    {
        foreach(GameObject targetGO in targetsGO)
        {
            targetGO.GetComponent<TargetManager>().UpdateMonsters();
        }
        //调用UI的单例,让那两个数字都归零
        UIManager._instance.shootNum = 0;
        UIManager._instance.score = 0;
        UnPause();
    }

退出游戏

    //退出游戏的方法
    public void QuitGame()
    {
        Application.Quit();
    }

这个功能只有打包成游戏才有用

然而打包之后这段代码还是没有

换成以下代码有用

public void QuitGame()
{
    #if UNITY_EDITOR
        UnityEditor.EditorApplication.isPlaying = false;
    #else
        Application.Quit();
    #endif
}

添加音效

直接用audio source做的,没什么好说的

枪的射击音效
    //定义一个音频组件
    private AudioSource gunAudio;

在awake方法里赋值

    private void Awake()
    {
        gunAudio = GetComponent < AudioSource>();
    }

播放手枪开火的声音

                    //播放手枪开火的声音
                    gunAudio.Play();
撞击音效
    public AudioSource kickAudio;

    //在Awake里获得动画组件
    private void Awake()
    {
        anim = gameObject.GetComponent<Animation>();
        //设置动画初始是idle
        anim.clip = idleClip;
        kickAudio = GetComponent<AudioSource>();//赋值音效
    }

播放撞进音效

            //播放击中的音效
            kickAudio.Play();

控制背景音乐的开关

先拿到音乐的按钮

    //拿到Toggle组件
    public Toggle musicToggle;

得到背景音乐

    //得到背景音乐
    public AudioSource backGroundMusic;

在Update里面做判断,单独写个方法

    //定义一个布尔值去判断音乐开关
    private bool musicOn = true;
    //音乐开关方法
    private void MusicSwitch()
    {
        if(musicToggle.isOn == true)
        {
            musicOn = true;
            backGroundMusic.enabled = true;
        }
        else
        {
            musicOn = false;
            backGroundMusic.enabled = false;

        }
    }

存档相关概念

Unity中使用的存档方式
  • PlayerPrefs:数据持久化方案
  • 采用键值对的方式对数据进行存储
  • PlayerPrefs.SetInt(“Index”,1)
  • 可以储存Int,Float,String类型的数据
  • PlayerPrefs.SetFloat(“Height”,183.5f)
  • PlayerPrefs.SetString(“Name”,“Tom”)
  • 获取数据
  • PlayerPrefs.GetInt(“Index”)
序列化和反序列化方式
  • Serialization序列化,可以用来将对象转化为字节流

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E4U4wfuc-1681389891122)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230413141326005.png)]

  • Deserialization反序列化,可以用来将字节流转换为对象

  • 常见的数据序列化方法:二进制方法,XML方法,JSON方法

详细介绍二进制方法、XML、JSON

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X1xDGXO4-1681389891122)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230413141913809.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qB6jpnEC-1681389891122)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230413141925484.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8UFINJDJ-1681389891122)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230413141946737.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TKeHy9hf-1681389891123)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230413142300497.png)]

存储背景音乐的开关状态

设定一下需要的PlayerPrefs值

在不同状态下改变PlayerPrefs的状态

调用保存方法

    //音乐开关方法
    public void MusicSwitch()
    {
        if(musicToggle.isOn == true)
        {
            musicOn = true;
            backGroundMusic.enabled = true;
        }
        else
        {
            musicOn = false;
            backGroundMusic.enabled = false;
            //1代表开启状态
            PlayerPrefs.SetInt("MusicOn",1);
            
        }
        //调用PlayerPrefs保存方法
        PlayerPrefs.Save();
    }

在Awake方法里判断是否存在PlayerPrefs的键值对


    //给单例赋值
    private void Awake()
    {
        _instance = this;
        if (PlayerPrefs.HasKey("MusicOn"))
        {
            if (PlayerPrefs.GetInt("MusicOn") == 1)//=1代表音乐是开启的
            {
                //把触发器赋值成true
                musicToggle.isOn = true;
                backGroundMusic.enabled = true;

            }
            else
            {
                musicToggle.isOn = false;
                backGroundMusic.enabled = false;

            }
        }
        else
        {
            musicToggle.isOn = true;
            backGroundMusic.enabled = true;

        }

    }

设置Target位置和怪物类型

设置target的编号

设置一个target的值

    //给每个怪物一个编号值
    public int targetPosition;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6PjadXco-1681389891123)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230413151618595.png)]

在层级面板下就可以单独赋值了

怪物给一个类型的值,用数字代替怪物类型

    //怪物类型
    public int monsterType;//0 3
储存怪物信息的方法
    //储存怪物信息的方法
    public void ActivateMonsterByType(int type)
    {
        //先停止所有的协程
        StopAllCoroutines();
        //先通过清空判断
        if (activeMonster != null)
        {
            activeMonster.GetComponent<BoxCollider>().enabled = false;
            activeMonster.SetActive(false);
            activeMonster = null;
        }
        //激活数组
        activeMonster = monsters[type];
        activeMonster.SetActive(true);
        activeMonster.GetComponent<BoxCollider>().enabled = true;
        //开起来之后还得回到调用死亡的方法
        StartCoroutine(DeathTimer());

    }

复制Target并完善

先切断这几个怪物和预制体之间的联系

操作:选中 - GameObject - Break Prefab instance

然后把Target放到左上角第一个,按顺序和编号复制填满其他格子

复制的target出现了死亡动画播放时间过长的问题

原因:之前调用的单例模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ImuG7qPi-1681389891123)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230413153838711.png)]

但是存在多个Target的话,系统就不知道该调用哪一个了

解决思路:删掉单例模式,通过父物体来得到Update功能

        //使用InParent调用父物体的方法
        gameObject.GetComponentInParent<TargetManager>().UpdateMonsters();

通过Mesh换皮肤

创建Save保存类

创建一个Save类
public class Save//不需要继承基类
{
    //存活的怪物位置类
    public List<int> livingTargetPosition = new List<int>();
    //存活怪物类型类
    public List<int> livingTargetTypes = new List<int>();

    //得分 设计数
    public int score = 0;
    public int shootNum = 0;

}

在最前面给到可序列化特性

[System.Serializable]//可序列化
在GameManager里写一个Save方法
 //创建保存游戏方法
    private Save CreateSaveObject()
    {
        //创建一个Save量
        Save save = new Save();
        //保存怪物信息:位置和类型
        foreach(GameObject TargetGO in targetsGO)
        {
            //得到每个怪物身上的TargetManager组件
            TargetManager targetManager = TargetGO.GetComponent<TargetManager>();
            //判断激活怪物是否为空,如果不为空,就把激活怪物的坐标添加到save量里面
            if (targetManager.activeMonster != null)
            {
                save.livingTargetPosition.Add(targetManager.targetPosition);
                //获得/查看怪物类型
                int type = targetManager.activeMonster.GetComponent<MonsterManager>().monsterType;
                //把这个类型也存进save
                save.livingTargetTypes.Add(type);
            }
        }

        //保存得分
        save.shootNum = UIManager._instance.shootNum;
        save.score = UIManager._instance.score;

        //返回save
        return save;
    }

保存游戏(二进制方法)

先在GameManager里写保存和加载的方法
创建public方法去注册按键
    //保存游戏
    public void SaveGame()
    {

    }



    //加载游戏
    public void LoadGame()
    {

    }
引入读写命名空间
using System.IO;

读取游戏(二进制方法)

创建一个文字UI用来提示保存信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OoVsIAcZ-1681389891123)(C:\Users\CoreDawg\AppData\Roaming\Typora\typora-user-images\image-20230413180314849.png)]

在UIManager里面得到Text
    //拿到TEsxtUI
    public Text messageText;
写一个展示文字的方法
    //展示文字UI的方法
    public void ShowMessage(string str)
    {
        messageText.text = str;
    }
在游戏开始方法里写一个空字符串还原Message
        UIManager._instance.ShowMessage("");
继续游戏的时候也要还原
        UIManager._instance.ShowMessage("");
加载游戏的时候也要还原
        UIManager._instance.ShowMessage("");
在保存游戏的时候输出UI
 //二进制保存方法
    private void SaveByBin()
    {
        //序列化过程(将Save对象转换为字节流)
        Save save = CreateSaveObject();//创建save对象并保存当前游戏状态
        //创建一个二进制格式化程序
        BinaryFormatter bf = new BinaryFormatter();
        //创建一个文件流来保存
        FileStream fileStream = File.Create(Application.dataPath + "/StreamingAssets" + "/byBin.txt");//后面这个格式保存文件可以随便写
        //用二进制格式化程序的序列方法来序列化Save对象,参数:创建的文件流和需要序列化的对象
        bf.Serialize(fileStream,save) ;
        //关闭流
        fileStream.Close();

        if (File.Exists(Application.dataPath + "/StreamingAssets" + "/byBin.txt"))
        {
            //输出 保存成功!
            UIManager._instance.ShowMessage("保存成功!");
        }


    }

加载游戏

二进制加载方法
    //二进制加载方法
    private void LoadByBin()
    {
        //先判断存档是否存在,存在的话才进行这个操作
        if(File.Exists(Application.dataPath + "/StreamingAssets" + "/byBin.txt"))
        {
            //反序列化过程
            //创建一个二进制格式化程序
            BinaryFormatter bf = new BinaryFormatter();
            //打开一个文件流
            FileStream fileStream = File.Open(Application.dataPath + "/StreamingAssets" + "/byBin.txt", FileMode.Open);
            //调用bf二进制格式化程序反序列方法,将文件流转换成save对象
            Save save = (Save)bf.Deserialize(fileStream);
            //关闭文件流
            fileStream.Close();

            SetGame(save);
        }
        else
        {
            UIManager._instance.ShowMessage("存档文件不存在");

        }
    }
根据Save文件加载对象
思路

因为无论是用什么方法加载,这一步都是一样的,所以这个功能单独写成一个方法

思路,先拿到Save类里存储的Poistion,然后再根据Save类里的type设置怪物

根据存档设置游戏的方法
    //根据存档文件加载对象的方法
    private void SetGame(Save save)
    {
        //读档之前先清空所有怪物
        foreach(GameObject targetGo in targetsGO)
        {
            //调用TargetManager里面的重新激活方法
            targetGo.GetComponent<TargetManager>().UpdateMonsters();

        }

        //遍历数据
        for(int i = 0; i<save.livingTargetPosition.Count; i++)
        {
            //na拿到位置和类型数据
            int position = save.livingTargetPosition[i];
            int type = save.livingTargetTypes[i];

            targetsGO[position].GetComponent<TargetManager>().ActivateMonsterByType(type);

        }
        UIManager._instance.shootNum = save.shootNum;
        UIManager._instance.score = save.score;

        UnPause();
    }

保存游戏(JSON方法)

导入LitJson库

引入命名空间

using LitJson;

如果没有导入LitJson库,这里就会报错

Json保存方法
    //Json保存方法
    private void SaveByJson()
    {
        Save save = CreateSaveObject();
        //与之前不同,直接拿到文件路径path
        string filePath = Application.dataPath + "/StreamingAssets" + "/byHson.json";

        //利用JsonMapper,将save对象转换为json类型的字符串
        string saveJsonStr = JsonMapper.ToJson(save);
        //将字符串写入到文件中
        //创建一个StreamWriter
        StreamWriter sw = new StreamWriter(filePath);
        //利用write方法把json格式的字符串写进StreamWriter
        sw.Write(saveJsonStr);
        //关闭写入工具treamWriter
        sw.Close();

        //用UI显示保存成功
        UIManager._instance.ShowMessage("保存成功!");
    }

读取游戏(JSON)


    //Json加载方法
    private void LoadByJson()
    {
        string filepath = Application.dataPath + "/StreamingAssets" + "/byHson.json";
        if (File.Exists(filepath))
        {
            //创建一个读取流StreamReader
            StreamReader sr = new StreamReader(filepath);
            //将读取到的流赋值给jsonstr
            string jsonStr = sr.ReadToEnd();
            //关闭
            sr.Close();

            //将字符串jsonStr转换为Save对象
            Save save = JsonMapper.ToObject<Save>(jsonStr);
        }
        else
        {
            UIManager._instance.ShowMessage("存档文件不存在");
        }
    }

保存游戏(XML)

xml用的是Unity自带的库,但是需要引入额外的命名空间

using System.Xml;
 //XML保存方法
    private void SaveByXml()
    {
        Save save = new Save();
        //创建xml存储路径
        string filePath = Application.dataPath + "/StreamingAssets" + "/byXml.txt";
        //创建一个xml文档
        XmlDocument xmldoc = new XmlDocument();
        //创建根节点,即最上层节点
        XmlElement root = xmldoc.CreateElement("save");
        //设置根节点中的值
        root.SetAttribute("name", "savefile1");

        //Xml方法不能使用Save类,必须单独设定所有需要的数据成员
        //创建XmlElement
        XmlElement target;
        XmlElement targetPosition;
        XmlElement monsterType;

        //遍历save/root中存储的数据,将数据转换成Xml格式
        for(int i = 0; i < save.livingTargetPosition.Count; i++)
        {
            target = xmldoc.CreateElement("target");
            targetPosition = xmldoc.CreateElement("targetPosition");
            //设置节点的值
            targetPosition.InnerText = save.livingTargetPosition[i].ToString();//赋值
            monsterType = xmldoc.CreateElement("monsterType");
            monsterType.InnerText = save.livingTargetPosition[i].ToString();

            //设置节点间的层级关系 root - target - (targetPosition,monsterType)
            target.AppendChild(targetPosition);
            target.AppendChild(monsterType);
            root.AppendChild(target);


        }
        //设置分数和射击数节点并设置层级关系 xmldoc - root - (target- (targetPosition,monsterType),shootNum,score)
        XmlElement shootNum = xmldoc.CreateElement("shootNum");
        shootNum.InnerText = save.shootNum.ToString();
        XmlElement score = xmldoc.CreateElement("score");
        score.InnerText = save.score.ToString();
        root.AppendChild(shootNum);
        root.AppendChild(score);

        //把根节点传递过去
        xmldoc.AppendChild(root);
        xmldoc.Save(filePath);

        //判断文件是否存在,然后显示保存成功
        if (File.Exists(filePath))
        {
            UIManager._instance.ShowMessage("保存成功!");
        }
       }

读取游戏(XML)

private void LoadByXml()
    {
        string filePath = Application.dataPath + "/StreamingAssets" + "/byXml.txt";
        if (File.Exists(filePath))
        {
            Save save = new Save();
            //加载XML文档
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(filePath);

            //通过节点名称来获取元素,结果为XmlNodeList类型
            XmlNodeList targets = xmlDoc.GetElementsByTagName("target");
            //遍历所有的target节点,并获得子节点和子节点的InnerText
            if (targets.Count != 0)
            {
                foreach (XmlNode target in targets)
                {
                    XmlNode targetPosition = target.ChildNodes[0];
                    int targetPositionIndex = int.Parse(targetPosition.InnerText);
                    //把得到的值存储到save中
                    save.livingTargetPosition.Add(targetPositionIndex);

                    XmlNode monsterType = target.ChildNodes[1];
                    int monsterTypeIndex = int.Parse(monsterType.InnerText);
                    save.livingTargetTypes.Add(monsterTypeIndex);
                }
            }

            //得到存储的射击数和分数
            XmlNodeList shootNum = xmlDoc.GetElementsByTagName("shootNum");
            int shootNumCount = int.Parse(shootNum[0].InnerText);
            save.shootNum = shootNumCount;

            XmlNodeList score = xmlDoc.GetElementsByTagName("score");
            int scoreCount = int.Parse(score[0].InnerText);
            save.score = scoreCount;

            SetGame(save);
            UIManager._instance.ShowMessage("");

        }
        else
        {
            UIManager._instance.ShowMessage("存档文件不存在");
        }
    }
Save save = new Save();
        //加载XML文档
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.Load(filePath);

        //通过节点名称来获取元素,结果为XmlNodeList类型
        XmlNodeList targets = xmlDoc.GetElementsByTagName("target");
        //遍历所有的target节点,并获得子节点和子节点的InnerText
        if (targets.Count != 0)
        {
            foreach (XmlNode target in targets)
            {
                XmlNode targetPosition = target.ChildNodes[0];
                int targetPositionIndex = int.Parse(targetPosition.InnerText);
                //把得到的值存储到save中
                save.livingTargetPosition.Add(targetPositionIndex);

                XmlNode monsterType = target.ChildNodes[1];
                int monsterTypeIndex = int.Parse(monsterType.InnerText);
                save.livingTargetTypes.Add(monsterTypeIndex);
            }
        }

        //得到存储的射击数和分数
        XmlNodeList shootNum = xmlDoc.GetElementsByTagName("shootNum");
        int shootNumCount = int.Parse(shootNum[0].InnerText);
        save.shootNum = shootNumCount;

        XmlNodeList score = xmlDoc.GetElementsByTagName("score");
        int scoreCount = int.Parse(score[0].InnerText);
        save.score = scoreCount;

        SetGame(save);
        UIManager._instance.ShowMessage("");

    }
    else
    {
        UIManager._instance.ShowMessage("存档文件不存在");
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值