今天学习游戏的存档和读档,首先需要复制例子中的游戏,因为想自己写,然后大半的时间都用来复制游戏了。
一.物体始终朝向鼠标位置
首先想到的是使用lookat来做,觉得非常简单
transform.LookAt(Input.mousePosition);
但是实际用下来有两个主要问题,一是这个mouseposition在整个电脑屏幕上的位置控制了Game窗口中的物体朝向;二是游戏物体围绕z轴旋转,也就是在二维平面上旋转,并不会朝向前面或后面。搜索查阅后发现使用射线检测的方法能实现。
/// <summary> /// 射线检测,使枪始终朝向鼠标位置 /// </summary> private void RotateGun() { //从摄像机发射一条朝向鼠标位置的射线 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; //进行射线检测 if (Physics.Raycast(ray, out hit)) { //记录物体位置到检测到的位置的方向 Vector3 gunToMouse = hit.point - transform.position; //计算朝向鼠标位置的旋转四元数 Quaternion gunRotation = Quaternion.LookRotation(gunToMouse); //设置物体的旋转方向 transform.rotation = gunRotation; } }
我的理解和推测:本来鼠标位置的坐标是整个电脑屏幕中的像素坐标,在lookat方法中,因为物体前方始终朝着鼠标位置,鼠标在屏幕上的坐标是像素坐标,因此没有z值,所以导致了物体绕着z轴旋转;使用射线检测时Camera.main.ScreenPointToRay方法能够将鼠标位置转化为世界坐标,所以同步了鼠标和物体的坐标系,因此接下来可以进行射线检测并计算方向,最后计算和设置旋转四元数。最终实现了指哪打哪的效果。Unity不开源真************。
二.游戏暂停
声明一个bool值记录游戏是否是暂停状态
public bool isPaused = true;
暂停游戏和继续游戏的方法:
/// <summary> /// 暂停游戏 /// </summary> public void Pause() { //记录游戏是暂停状态 isPaused = true; //设置游戏菜单激活 manu.SetActive(true); //设置停止游戏内时间 Time.timeScale = 0; //设置鼠标可见 Cursor.visible = true; } /// <summary> /// 继续游戏 /// </summary> public void UnPause() { //记录游戏没有暂停 isPaused = false; //设置游戏菜单非激活状态 manu.SetActive(false); //设置游戏时间正常进行 Time.timeScale = 1; //设置游戏中鼠标不可见 Cursor.visible = false; }
在update中检验是否按下暂停键,按下后当前状态为暂停则继续游戏,当前状态为正在游戏则暂停游戏
void Update() { if(Input.GetKeyDown(KeyCode.Escape)) { if (isPaused) UnPause(); else Pause(); } }
在游戏中发现,将Time.timeScale调整为0能够使游戏暂停,但是在游戏物体枪的脚本的update函数中发射子弹地功能等仍然可用,只是子弹不会发射出去,枪口也会跟着鼠标移动,取消暂停后刚才没有发射出去地子弹又全部发射出去了,查阅资料,说的是因为其运行和电脑性能等有关,Time.timeScale为0后update仍然会运行,所以导致了这个情况出现,因此在update中判断游戏是否暂停,将update地所有代码都写在这个if地代码块中解决了这个问题。
三.存档和读档
存档和读档使用键值对记录当前游戏的各种参数状态,使用序列化和反序列化存储这些键值对数据,可以使用二进制、xml或json进行序列化和反序列化。
1.写一个保存类,属性对应了所有要保存的信息,提供将这些信息写成二进制文件或xml文件或json文件的方法
2.保存游戏:在Manager中提供一个供保存游戏按钮被按下时调用的方法,这个方法得到一个保存类的实例,并将游戏状态存储到这个实例的属性中
3.保存游戏:调用保存类的方法将保存类写成二进制或xml或json格式的文件
4.读取游戏:在Manager中提供一个供读取游戏按钮按下时调用的方法,这个方法调用保存类中的读取方法读取数据,得到一个保存类的实例
5.读取游戏:通过保存类实例中的属性值还原游戏
/// <summary> /// 存储游戏 /// </summary> public void Save() { //实例化SaveAndLoad类 SaveAndLoad save = new SaveAndLoad(); //遍历所有monster,如果没有在死亡状态,就把它保存下来 if(MonsterBorn._instance.indexList.Count > 0) { //查找当前游戏中的所有monster GameObject[] monsters = GameObject.FindGameObjectsWithTag("Monster"); foreach(GameObject monster in monsters) { int monsterType = monster.GetComponent<Monsters>().monsterType; //根据下标值获取对应的idle动画名称,判断当前是否在播放此动画,即怪物是否存活 save.monsters.Add(monsterType ,monster.GetComponent<Monsters>().isDead); } //保存所有monster的下标 save.indexList = MonsterBorn._instance.indexList; } //存储音乐播放状态 save.otherProperties[0] = toggle.isOn ? 0 : 1; //存储分数和子弹数 save.otherProperties[1] = scoreNum; save.otherProperties[2] = shootTimeNum; //将保存了游戏状态的对象保存为文档 //SaveAndLoad.SaveByBinary(save); SaveAndLoad.SaveByJson(save); }
/// <summary> /// 读取游戏的方法 /// </summary> public void Load() { //还原对象 //SaveAndLoad load = SaveAndLoad.LoadByBinary(); SaveAndLoad load = SaveAndLoad.LoadByJson(); //清空当前游戏场景中的怪物 GameObject[] monsters = GameObject.FindGameObjectsWithTag("Monster"); if(monsters.Length > 0) { foreach(GameObject monster in monsters) { Destroy(monster); } } //根据对象中的值还原场景 MonsterBorn._instance.indexList = load.indexList; if(load.monsters.Count > 0) { int count = 0; foreach(KeyValuePair<int,bool> pair in load.monsters) { Vector3 position = MonsterBorn._instance.bornPoints[MonsterBorn._instance.indexList[count]].position; GameObject monster = Instantiate(MonsterBorn._instance.monsterPrefabs[pair.Key], position, Quaternion.Euler(new Vector3(0, 180, 0))); Monsters m = monster.GetComponent<Monsters>(); m.isDead = pair.Value; if (pair.Value) { m.Invoke("DestroySelf", 1); m.anim.Play(Enum.GetName(typeof(AnimationNames),pair.Key)); } count++; } } scoreNum = load.otherProperties[1]; shootTimeNum = load.otherProperties[2]; if(load.otherProperties[0] == 0) { audioSource.Play(); toggle.isOn = true; } if(load.otherProperties[0] == 1) { audioSource.Stop(); toggle.isOn = false; } }
using System.Collections; using System.Collections.Generic; using UnityEngine; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using LitJson; //声明可序列化 [System.Serializable] public class SaveAndLoad { //用于保存所有的怪物种类及其位置坐标的键值对 public Dictionary<int, bool> monsters = new Dictionary<int, bool>(); //0背景音乐是否开启(0,开启;1,关闭);1分数;2子弹数 public int[] otherProperties = new int[3]; //用于存储怪物位置下标的list public List<int> indexList = new List<int>(); /// <summary> ///将对象保存为二进制文件的方法 /// </summary> /// <param name="save"></param>待保存的对象 public static void SaveByBinary(SaveAndLoad save) { //创建二进制格式化程序 BinaryFormatter formatter = new BinaryFormatter(); //创建流 FileStream stream = File.Create(Application.dataPath + "/binaryData.txt"); //写入数据 formatter.Serialize(stream, save); //关闭流 stream.Close(); } /// <summary> /// 读取二进制文件的方法 /// </summary> public static SaveAndLoad LoadByBinary() { //反序列化过程 //创建二进制格式化程序 BinaryFormatter formatter = new BinaryFormatter(); //创建流 FileStream stream = File.Open(Application.dataPath + "/binaryData.txt",FileMode.Open); //反序列化二进制文件为对象 SaveAndLoad load = (SaveAndLoad)formatter.Deserialize(stream); //关闭流 stream.Close(); //返回结果 return load; } /// <summary> /// 将对象保存为JSON的方法 /// </summary> /// <param name="save"></param>要保存的对象 public static void SaveByJson(SaveAndLoad save) { //创建Json字符串 string jsonStr = JsonMapper.ToJson(save); //创建文件流 StreamWriter writer = new StreamWriter(Application.dataPath + "/jsonData.json"); //写入数据 writer.Write(jsonStr); //关闭文件流 writer.Close(); } /// <summary> /// 读取json文件的方法 /// </summary> public static SaveAndLoad LoadByJson() { //创建流 StreamReader reader = new StreamReader(Application.dataPath + "/jsonData.json"); //读取流 string str = reader.ReadToEnd(); //json转化为对象 SaveAndLoad load = JsonMapper.ToObject<SaveAndLoad>(str); //关闭流 reader.Close(); //返回 return load; } }
这里没有使用xml保存和读取文件,个人觉得有些麻烦和过时。在读取的过程中也暂时没有考虑读取失败或文件不存在等情况出现的提醒,存储过程也没有任何提醒,有时间可以进行拓展。这里只实现了单存档,因为文件路径写死了,可以灵活地根据保存时间等创建文件路径并保存文件路径在游戏中展示地方法实现多个存档。