存档案例
搭建游戏场景
- 背景
- 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("存档文件不存在");
}
}