• Unity坦克迷宫Demo总结


    1. 游戏需要有多个面板,那就将BasePanel作为面板的基类,可以去显示面板 隐藏面板,最主要就是写成单例类。
      知识点
      (1)虚方法:在父类中声明一个虚方法,在子类中可以调用该方法,还可以通过override选择性的重写,如果不是虚方法,那子类就需要重新声明方法,所以写成虚方法的目的就是增加代码的复用性。
      (2)单例类:私有的静态成员变量(声明) 和 公开的静态成员变量(获取)。继承的面板类型不确定(开始面板 设置面板 游戏面板…),所以写成泛型;这样写的好处就是:我们可以在一个面板中调用其他面板,只需要调用其他面板的instance就可以获取到这个脚本,控制其他面板的显示和隐藏
      (3)如果在两个场景中调用同一个面板,单例会不会出问题?在首个场景中,instance会指向场景中唯一的面板,当切换场景时,Awake再次执行,instance就会指向下一个场景中唯一的面板
    public class BasePanel<T> : MonoBehaviour where T : class
    {
        //BasePanel作为面板的基类
        //隐藏面板 获取面板 单例
        //继承了MonoBehaviour的类 是不可以new的 必须要通过addcomponent依附在一个对象上 不能自己new
        private static T instance;
    
        //这个写法就是get方法
        public static T Instance => instance;
    
        //instance = this 会报错 因为传进来的类型是T 不确定是结构体还是类 所以要泛型约束类型
        private void Awake()
        {
            //在Awake中初始化 是因为继承的面板在场景中只会挂载一次
            //那就可以在Awake中直接获取场景上唯一的脚本
            instance = this as T;
            //this此时还是BasePanel T是继承BasePanel 所以as可以转换成T
            //此时会报错 因为T不知道是结构还是类 只有类才能使用as 所以加一个约束
        }
    
        //写的是虚函数 为了外部访问和子类重写
        public virtual void ShowPanel()
        {
            this.gameObject.SetActive(true);
        }
        public virtual void HidePanel()
        {
            this.gameObject.SetActive(false);
        }
    }
    

    2.GameDataManager 数据管理类,也是单例类。
    (1)管理和加载本地的数据
    (2)音乐类、坦克类、排行榜类中就是一些成员变量,可以从加载和保存到本地。
    知识点
    (1)构造函数中初始化其他类
    (2)通过调用Json存储到本地(文章末尾记录Json的写法)

    public class GameDataManager 
    {
        //数据管理类 单例 初始化和保存一些数据
        private static GameDataManager instance = new GameDataManager();
        public static GameDataManager Instance => instance;
    
        //音乐类
        public MusicData musicData;
    
        //排行榜类
        public List<RankData> rankData;
    
        //坦克类
        public List<TankData> tankData;
        public GameDataManager()
        {
            musicData = JsonMgr.Instance.LoadData<MusicData>("MusicData");
            rankData = JsonMgr.Instance.LoadData<List<RankData>>("RankData");
            tankData = JsonMgr.Instance.LoadData<List<TankData>>("TankData");
        }
    
        public void AddRankData(int id,string name,int score,float time)
        {
            rankData.Add(new RankData(id,name,score,time));
            //排序
            rankData.Sort((a, b) => a.time < b.time ? -1 : 1);
            //超出8个的就清理掉
            for(int i = 0; i < rankData.Count; i++)
            {
                if(i >= 7)
                {
                    rankData.RemoveAt(i);
                }
            }
            //存储
            SaveRankData();
        }
        /// 
        /// 保存排行榜数据类
        /// 
        public void SaveRankData()
        {
            JsonMgr.Instance.SaveData(rankData, "RankData");
        }
        /// 
        /// 保存音乐数据
        /// 
        public void SaveMusicData()
        {
            JsonMgr.Instance.SaveData(musicData, "MusicData");
        }
    
        /// 
        /// 保存玩家坦克数据
        /// 
        public void SaveTankData()
        {
            JsonMgr.Instance.SaveData(tankData, "TankData");
        }
    }
    

    3.其他各种面板基本是一个套路
    (1)绑定组件
    (2)通过按钮控制面板的现实和隐藏
    (3)通过GameDataManager将数据进行保存和更新UI
    (4)同时还可以使用override重写父类中的ShowPanel和HidePanel方法,保留父类就是base.ShowPanel(),不想保留就删除,同时下面写出想要执行的方法即可,这样就可以在显示或隐藏面板时,同时做一些事。
    4.音乐和音效
    (1)首先就是创建音乐类,开关和音量
    (2)然后在场景中创建一个空物体改名为BgMusic单例类,提供两个方法给外部,目的就是在游戏运行时,可以随时得到背景音乐去修改和保存它

    //音乐类
    public class MusicData 
    {
        //音乐类 管理声音和开关
        public bool isOpenMusic = true;
        public bool isOpenSound = true;
    
        public float volumeMusic = 0.2f;
        public float volumeSound = 0.2f;
    }
    //场景中的脚本
    public class BkMusic : MonoBehaviour
    {
    	//继承了Mono就不能new 所以在Awake中赋值
        private static BkMusic instance;
        public static BkMusic Instance => instance;
        private AudioSource audio;
        void Awake()
        {
            instance = this;
            audio =  this.GetComponent<AudioSource>();
            //初始化数据
            audio.volume = GameDataManager.Instance.musicData.volumeMusic;
            audio.mute = !GameDataManager.Instance.musicData.isOpenMusic;
        }
    
        /// 
        /// 是否开启音乐
        /// 
        /// 
        public void SetOpenMusic(bool isOpen)
        {
            audio.mute = !isOpen;
        }
        /// 
        /// 通过场景改变的音量 实时更新
        /// 
        /// 
        public void ChangeValue(float volume)
        {
            audio.volume = volume;
        }
    }
    

    5.坦克基类(核心):(1)先考虑需要哪些变量,比如坦克的id 血条 生命值 最大生命值 移动速度 旋转速度 头部变量 头部旋转速度…
    (2)开火:我方坦克和敌方坦克有不同的开火逻辑,所以可以写成抽象类,方法自然就写成抽象方法,子类重写实现开火即可
    (3)受伤:虚方法,直接写逻辑,我方和敌方受伤逻辑一致,不同的内部重写
    (4)死亡:当hp小于等于0时就死亡,销毁gameobject,播放死亡特效,同时设置音效
    知识点
    抽象类1.抽象类不能被实例化
    2.同时子类必须重写该抽象方法
    3.父类中的抽象方法不能写具体的逻辑

    public abstract class TankObject : MonoBehaviour
    {
        public int id;
        public int atk;
        public int maxHp;
    
        //声明一个临时变量接受代表当前hp
        public int hp;
    
        //炮台的头部
        public Transform head;
    
        public int moveSpeed = 10;
        public int rotaSpeed = 100;
        public int headRotaSpeed = 100;
    
        public  GameObject effectObj;
        private TankData rankData;
    
    	//加载数据
        void Awake()
        {
            rankData = GameDataManager.Instance.tankData[id];
            this.atk = rankData.atk;
            this.id = rankData.id;
            this.maxHp = rankData.maxHp;
            this.hp = maxHp;
        }
        //因为我们想让坦克类和敌方坦克类都继承这个开火方法
        //那就可以写成抽象类
        //
        public abstract void Fire();
    
        /// 
        /// 传进来的就是伤害者
        /// 
        /// 
        public virtual void Wound(TankObject other)
        {
            this.hp -= other.atk;
            if(hp <= 0)
            {
                this.hp = 0;
                Dead();
            }
        }
        /// 
        /// 目标死亡
        /// 
        public virtual void Dead()
        {
            Destroy(this.gameObject);
            //死亡动画********
            //原来的死亡是播放动画 这次的死亡是创建预设体对象并且在同一位置
            GameObject eff = Instantiate(this.effectObj, this.transform.position, this.transform.rotation);
            //预设体自身携带了音效 所以把音效一起改了
            AudioSource audioSourcs = eff.GetComponent<AudioSource>();
            audioSourcs.volume = GameDataManager.Instance.musicData.volumeSound;
            audioSourcs.mute = !GameDataManager.Instance.musicData.isOpenSound;
            audioSourcs.Play();
        }
    }
    

    6.我方坦克
    (1)首先是实现坦克的走路,以及鼠标在X轴上坦克头部的移动
    (2)具体开火逻辑写在武器的脚本中;重写受伤和死亡,为了执行一些其他事情
    知识点
    (1)GameObject 实例化有一个重载Instantiate(weapon, weaponPos,false);
    weapon会将weaponPos设置为父对象,false会保留自己的缩放大小

    public class PlayerObj : TankObject
    {
        public WeaponObj weaponObj;
    
        //生成武器的点
        public Transform weaponPos;
    
        void Start()
        {
            
        }
    
        // Update is called once per frame
        void Update()
        {
            //移动 旋转 头部旋转
            this.transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime * Input.GetAxis("Vertical"));
            //左右旋转
            this.transform.Rotate(Vector3.up * rotaSpeed * Time.deltaTime * Input.GetAxis("Horizontal"));
            //鼠标左右控制炮台旋转
            this.head.Rotate(Vector3.up * headRotaSpeed * Time.deltaTime * Input.GetAxis("Mouse X"));
            //左键开火
            if(Input.GetMouseButtonDown(0))
            {
                Fire();
            }
            //hpBase.transform.position = Camera.main.WorldToScreenPoint(this.transform.position + this.transform.up * hpHeight);
        }
        public override void Fire()
        {
            if (weaponObj != null)
                weaponObj.Fire();
        }
        public override void Dead()
        {
            //base.Dead();
            //不能执行死亡 有主摄像机在身上
            Time.timeScale = 0;
            FailPanel.Instance.ShowPanel();
        }
        public override void Wound(TankObject other)
        {
            base.Wound(other);
            GamePanel.Instance.UpdateHp(this.hp,this.maxHp);
        }
        //有捡道具玩法 更新枪支
        public void UpdateWeapon(GameObject weapon)
        {
            //先把身上的删除了
            if(weaponObj != null)
            {
                //不能只移除脚本 要移除实物
                Destroy(weaponObj.gameObject);
                //置空
                weaponObj = null;
            }
            //有一个重载方法 设置父对象 false保证缩放没什么问题
            GameObject go = Instantiate(weapon, weaponPos,false);
            weaponObj = go.GetComponent<WeaponObj>();
            //给武器设置拥有者
            weaponObj.SetFather(this);
        }
    }
    

    7.武器和子弹
    (1)武器主要是创建子弹,以及设置开火口,最后设置一下父对象(武器拥有者)
    (2)开火:创建子弹在枪口,并设置子弹的父对象
    (3)子弹(核心):子弹自身带刚体组件。子弹的移动速度,碰撞特效 音效的设置。子弹发射移动,Trigger Enter通过标签来识别
    知识点
    (1)有一点bug现在还未解决:就是一个子弹会有两次触发,第二次子弹的父对象为null, if (fatherObj == null) return 暂时只能这样解决了

    //武器
    public class WeaponObj : MonoBehaviour
    {
        //子弹对象
        public GameObject bullet;
        //发射位置
        public Transform[] shootPos;
        //拥有者
        public TankObject fatherObj;
        //通过里氏替换原则 用TankObject去记录武器的拥有者
        public void SetFather(TankObject tankObject)
        {
            this.fatherObj = tankObject;
        }
        public void Fire()
        {
            for(int i = 0; i < shootPos.Length; i++)
            {
                GameObject go = Instantiate(bullet, shootPos[i].position, shootPos[i].rotation);
                BulletObj bulletObj = go.GetComponent<BulletObj>();
                bulletObj.SetFather(fatherObj);
            }
        }
    }
    //子弹
    public class BulletObj : MonoBehaviour
    {
        public float moveSpeed = 50;
    
        public TankObject fatherObj;
    
        public GameObject effectObj;
    
        void Start()
        {
        }
        public void SetFather(TankObject tankObject)
        {
            //父类装子类
            this.fatherObj = tankObject;
        }
        // Update is called once per frame
        void Update()
        { 
            this.transform.Translate(Vector3.forward * Time.deltaTime * moveSpeed);
        }
    
        private void OnTriggerEnter(Collider other)
        {
            if (fatherObj == null) return;
            if(other.CompareTag("Cube") ||
               other.CompareTag("Player") && fatherObj.CompareTag("Monster")||
               other.CompareTag("Monster") && fatherObj.CompareTag("Player"))
            {
                print(other.tag);
                //受伤逻辑
                //判断碰撞体身上是否有坦克脚本 没有的话就说明是墙壁
                TankObject obj = other.GetComponent<TankObject>();
                if(obj != null)
                {
                    obj.Wound(fatherObj);
                }
                GameObject go = Instantiate(effectObj, this.transform.position, this.transform.rotation);
                Destroy(go, 1);
                AudioSource audioSource = go.GetComponent<AudioSource>();
                audioSource.volume = GameDataManager.Instance.musicData.volumeSound;
                audioSource.mute = !GameDataManager.Instance.musicData.isOpenSound;
                Destroy(this.gameObject);
            }
        }
    }
    

    8.固定不动的敌人

    public class MonsterTower : TankObject
    {
        //间隔时间发射子弹 打到玩家扣除血量并更新 
        //开火点 间隔时间 子弹实体 开火创建子弹和音效
        public Transform[] ShootPoint;
    
        public GameObject bullet;
    
        public float nowTime;
    
        public float offsetTime = 2;
        private void Update()
        {
        	//通过一个临时变量控制子弹发射的间隔
            nowTime += Time.deltaTime;
            if(nowTime >= offsetTime)
            {
                nowTime = 0;
                Fire();
            }
        }
        //重写开火方法 遍历开火口创建对象即可
        public override void Fire()
        {
            for(int i = 0; i < ShootPoint.Length; i++)
            {
                GameObject go = Instantiate(bullet, this.ShootPoint[i].position, this.ShootPoint[i].rotation);
                BulletObj goBullet = go.GetComponent<BulletObj>();
                //设置子弹的父对象 方便之后进行属性计算
                goBullet.SetFather(this);
            }
        }
        public override void Wound(TankObject other)
        {
        	//这里不希望它会受伤和死亡 所以重写受伤方法 不写任何逻辑
        }
    }
    

    9.移动的敌人
    (1)枪口开向玩家 开火间隔的设置
    (2)没有使用寻路组件,在场景中设置Transform,随机的向某个Transform移动
    (3)

    public class MonsterObj : TankObject
    {
        //目标点 和 随机点 
        private Transform targetPos;
    
        public Transform[] randomPos;
    
        public Transform lookTrans;
    
        public GameObject bullet;
    
        public Transform[] shootPos;
    
        private float nowTime;
    
        private float offsetTime = 1;
    
    	//一模一样的开火方法
        public override void Fire()
        {
            for(int i = 0; i < shootPos.Length; i++)
            {
                Instantiate(bullet, this.shootPos[i].position, this.shootPos[i].rotation);
                bullet.GetComponent<BulletObj>().SetFather(this);
            }
        }
    
        //AI敌人 检测玩家到达一定范围 发射子弹
        //在两个点直接来回移动
    
        void Start()
        {
        	//随机几个值
            RandomPos();
        }
    
        // Update is called once per frame
        void Update()
        {
        	//看向玩家
            this.transform.LookAt(targetPos);
            //移动
            this.transform.Translate(Vector3.forward * Time.deltaTime * moveSpeed);
            //当两点距离小于某个值 就去随机其他值去移动
            if(Vector3.Distance(targetPos.position,this.transform.position) < 0.05f)
            {
                RandomPos();
            }
            if(lookTrans != null)
            {
                head.LookAt(lookTrans);
            }
            //开火间隔时间
            nowTime += Time.deltaTime;
            //和玩家相距较近就开火
            if (Vector3.Distance(this.lookTrans.position,this.transform.position) < 10f)
            {
                if (nowTime >= offsetTime)
                {
                    nowTime = 0;
                    Fire();
                }
            }
        }
    	//随机方法
        public void RandomPos()
        {
            if (randomPos.Length == 0)
                return;
            //int 左包含 右不包含
            targetPos = randomPos[Random.Range(0,randomPos.Length)];
        }
    	//敌人要会死亡 保留base 同时去加分
        public override void Dead()
        {
            base.Dead();
            //加分
            GamePanel.Instance.AddScore(10);
        }
    }
    

    10.小地图
    (1)设置一个相机在玩家的上方
    (2)创建一个RawImage在场景中
    (3)Asset中创建一个RenderTexcure
    (4)将RenderTexcure拖入摄像机中的TargetTexcure
    (5)将RenderTexcure拖入RawImage中即可
    (6)不想让小地图跟随玩家旋转就单独写个脚本

    public class CameraMove : MonoBehaviour
    {
        public Transform tankObj;
    
        public float hightY;
        // Start is called before the first frame update
        void Start()
        {
            
        }
        //摄像机移动 最好写到lateupdate中 因为和update之间有一些处理 为了避免渲染出问题
        void LateUpdate()
        {
            //不能单独改变position某一个值
            this.transform.position = new Vector3(tankObj.position.x, hightY, tankObj.position.z);
        }
    }
    

    11.Json
    Json可以实现数据持久化,将游戏数据存储到硬盘中,硬盘中数据可以读取到游戏中

    //想要序列化自定义类 需要加上这个特性
    [System.Serializable]
    public class Student2
    {
        public int level;
        public Student2(int level)
        {
            this.level = level;
        }
    }
    //最外侧类不需要加特性
    public class Student
    {
        public int id;
        public string name;
        public List<int> students;
        public Student2 student2;
        //想要序列化私有变量 需要加上这个特性
        [SerializeField]
        private int hp;
    }
    public class Json : MonoBehaviour
    {
        // Start is called before the first frame update
        void Start()
        {
            //序列化
            //1.存储字符串到指定路径
            //不能同时创建文件夹
            File.WriteAllText(Application.persistentDataPath + "/one.json" , "第二个参数是存储内容");
            print(Application.persistentDataPath);
            //2.读取指定路径的json文件
            string s =  File.ReadAllText(Application.persistentDataPath + "/one.json");
    
            //JsonUtility
            //3.序列化
            //将游戏数据存储到硬盘中
            Student st = new Student();
            st.id = 123;
            st.name = "可可";
            st.students = new List<int> { 2, 3, 4, 5 };
            st.student2 = new Student2(20);
    
            //JsonUtility可以把类对象 序列化为 json字符串
            string s2 =  JsonUtility.ToJson(st);
            //然后利用上面的方法存储到指定路径
            File.WriteAllText(Application.persistentDataPath + "/two.json", s2);
            //读取
            string s3 = File.ReadAllText(Application.persistentDataPath + "/two.json");
            print(s3);
    
            //4.JsonUtility不支持序列化字典
            //5.JsonUtility不会存储null 会存储默认值
    
            //反序列化
            //1.将json内容转换为类对象 读取到为null的数据不会报错
            JsonUtility.FromJson<Student>(s3);
            //2.无法直接读取数据集合 想要读取集合 必须在json中前面加“list:”
            //3.编码格式必须为UTF-8
    
            //ListJson 
            //1.序列化
            //可以存储字典类型 字典的键最好是字符串 因为json的特点 json中的键会加上双引号 但是读取的时候就会分不清是字符串还是int
            //引用LitJson命名空间
            //不能序列化私有变量
            //可以保存null
            JsonMapper.ToJson(st);
            //2.反序列化
            string s5 = File.ReadAllText(Application.persistentDataPath + "/one.json");
            //jsonData需要键值对访问
            JsonData data =  JsonMapper.ToObject(s5);
            //通过泛型转换即可
            Student st1 = JsonMapper.ToObject<Student>(s5);
            //3.ListJson可以直接读取数据集合
            //4.编码格式必须为UTF-8
        }
    }
    

    JsonMgr:单例 保存和存储数据

    /// 
    /// 序列化和反序列化Json时  使用的是哪种方案
    /// 
    public enum JsonType
    {
        JsonUtlity,
        LitJson,
    }
    
    /// 
    /// Json数据管理类 主要用于进行 Json的序列化存储到硬盘 和 反序列化从硬盘中读取到内存中
    /// 
    public class JsonMgr
    {
        //没有继承mono 可以直接new
        private static JsonMgr instance = new JsonMgr();
        public static JsonMgr Instance => instance;
    
        //私有的无参构造函数 防止外部实例化
        private JsonMgr() { }
    
        //序列化
        //1.任何数据都可以存储 万物之父object
        //2.存储的文件名
        //3.有两种方式 给使用者一个选择
        public void SaveData(object data, string fileName, JsonType type = JsonType.LitJson)
        {
            //游戏中存储 只能使用persistentDataPath可读写目录 streamingAssetsPath是只读
            //1.确定存储路径
            string path = Application.persistentDataPath + "/" + fileName + ".json";
            //2.对存储内容进行序列化 得到对应的Json字符串
            string jsonStr = "";
            switch (type)
            {
                case JsonType.JsonUtlity:
                    jsonStr = JsonUtility.ToJson(data);
                    break;
                case JsonType.LitJson:
                    jsonStr = JsonMapper.ToJson(data);
                    break;
            }
            //3.把序列化的Json字符串 存储到指定路径的文件中
            File.WriteAllText(path, jsonStr);
        }
    
        //反序列化
        //读取指定文件中的 Json数据进行反序列化
        //需要有返回值 泛型
        //1.存储的文件名
        //2.有两种方式 给使用者一个选择
        public T LoadData<T>(string fileName, JsonType type = JsonType.LitJson) where T : new()//传入的类型中必须有无参构造函数
        {
            //确定从哪个路径读取
            //首先先判断 默认数据目录中是否有我们想要的数据 如果有 就从中获取
            string path = Application.streamingAssetsPath + "/" + fileName + ".json";
            //先判断 是否存在这个文件
            //如果不存在默认文件 就从 可读写目录中去寻找
            if(!File.Exists(path))
                path = Application.persistentDataPath + "/" + fileName + ".json";
            //如果读写文件夹中都还没有 那就返回一个默认对象
            if (!File.Exists(path))
                return new T();
    
            //进行反序列化
            string jsonStr = File.ReadAllText(path);
            //数据对象
            T data = default(T);
            switch (type)
            {
                case JsonType.JsonUtlity:
                    data = JsonUtility.FromJson<T>(jsonStr);
                    break;
                case JsonType.LitJson:
                    data = JsonMapper.ToObject<T>(jsonStr);
                    break;
            }
            //把对象返回出去
            return data;
        }
    }
    
  • 相关阅读:
    数字孪生解决方案-最新全套文件
    linux学习笔记
    微信小程序提示确认框 wx.showModal
    nginx-php镜像安装常用软件 —— k8s从入门到高并发系列教程 (三)
    < 今日份知识点:谈谈内存泄漏 及 在 Javascript 中 针对内存泄漏的垃圾回收机制 >
    Sentinel概述
    初学者使用R语言读取excel/csv/txt的注意事项
    java拼图游戏(待优化)
    USB转IIC I2C SPI UART适配器模块可编程开发板应用工业数字接口转换
    迅为RK3399开发板Linux系统TFTP传输文件服务器测试
  • 原文地址:https://blog.csdn.net/m0_70388078/article/details/139724573