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;
}
}