个人总结笔记,参考自B站各教程,希望对他人也有所帮助,对我自己也方便复习。
感谢唐老狮的教学
Resources文件夹加载资源(其中所有东西最终都会被打包不管有没有用到)
Scripts文件夹放置相关代码
Scenes场景资源放置一些保存好的场景
ArtRes文件夹直接将外部资源导入于此(减少游戏包大小)
作用:减少单例模式重复代码的书写
作为管理者不继承Monobehaviour
一个类只有一个实例,而且自行实例化并向整个系统提供这个实例
使用单例模式可以减少资源消耗
饿汉式:唯一实例在类加载时立即进行实例化
懒汉式:在类加载时不进行实例化,在第一次使用时进行实例化
双重检查锁解决线程问题
BV1af4y1y7sS
一个静态成员变量类型是自身
公共的静态成员方法/属性
Unity小游戏中一般不考虑双锁线程问题所以只需要简单写懒汉式即可
基础代码
public class GameManager
{
private static GameManager instance;
public static GameManager Getinstance()
{
if(instance == null)
instance = new GameManager();
return instance;
}
}
升级版2.0加入泛型
public class BaseManager where T:new()
{
private static T instance;
public static T Getinstance()
{
if(instance == null)
instance = new T();
return instance;
}
}
它的子类管理类只需要继承自他然后把自身的类型传进去即可
public class GameManager :BaseManager
BV1A4411F7fj
泛型的作用
Generic types泛型类型
开放类型和封闭类型
泛型方法
泛型方法中需要引用泛型参数
补充:ref和out基本一样,即代替了c中指针的部分作用,区别是即在使用ref,和不使用修饰符的时候,必须要传递一个有值的参数。ref和out几乎就只有一个区别,那就是out可以使用未赋值的变量。
原因是out应该是在方法内部做了分配地址的操作,然后把地址赋给外部的变量。但是ref的话是直接传递外部地址进方法。
声明泛型类型
Unity中不适用new的方法创建实例(继承了mono的脚本)
只能通过拖动到对象上或者通过加脚本api Addcomponent去加脚本
U3D内部帮我们实现他
引用:最后总结一下Awake和Start的异同点:
相同点:
1)两者都是对象初始化时调用的,都在Update之前,场景中的对象都生成后才会调用Awake,Awake调用完才会调用Start,所有Start调用完才会开始Update。
2)两者在对象生命周期内都只会被调用一次,即初始化时被调用,之后即使是在被重新激活之后也不会再次被调用。
不同点:
1)Awake函数在对象初始化之后立刻就会调用,换句话说,对象初始化之后第一调用的函数就是Awake;而Start是在对象初始化后,第一次Update之前调用的,
在 Start中进行初始化不是很安全,因为它可能被其他自定义的函数抢先。
2)Awake不管脚本是否enabled都会被调用;而Start如果对象被SetAcive(false)或者enabled= false了是不会被 调用的。
3)如果对象(GameObject)本身没激活,那么Awake,Start都不会调用。
单例模式mono基础代码
public class NewBehaviourScript : MonoBehaviour
{
private static NewBehaviourScript instance;
public static NewBehaviourScript GetInstance()
{
return instance;
}
void Awake()
{
instance = this;
}
}
采用instance方法而不用addcomponent方法是因为挂载后再instance=this的方法可以解决重复问题用新的顶掉旧的,而addcomponent多挂一遍就会多加一个不太行。
单例模式mono基类代码
public class SingletonMono : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T GetInstance()
{
return instance;
}
protected virtual void Awake()
{
instance = this as T;
}
}
采用virtual虚函数子类保证子类可以对Awake进行重写
子类再用protected override进行重写Awake
继承了Monobehaviour的单例模式对象需要我们自己保证其唯一性
升级版AutoSingletonMono继承这种单例模式的不需要直接去拖直接Getinstance就好了
public class SingletonAutoMono : MonoBehaviour where T: MonoBehaviour
{
private static T instance;
public static T GetInstance()
{
if(instance == null)
{
GameObject obj = new GameObject();
obj.name = typeof(T).ToString();
DontDestroyOnLoad(obj);//保证物体过场景不被销毁
instance = obj.AddComponent();
}
return instance;
}
}
缓存池模块本质上是一个抽屉来存储暂时没有用的东西供给以后用
原理是减少GC次数减少卡顿短期增加内存
从而实现手动GC而减少卡顿
Array
需要处理的元素数量确定并且需要使用下标时可以考虑,不过建议使用List
ArrayList
不推荐使用,建议用List
List泛型List
需要处理的元素数量不确定时 通常建议使用
LinkedList
链表适合元素数量不固定,需要经常增减节点的情况,2端都可以增减
Queue
先进先出的情况
Stack
后进先出的情况
Dictionary
需要键值对,快速操作
用dictionary键值对来对应存储相应的种类名字与list对应
用list而不用数组方便管理和动态存储
定义字典(类名-list)
public Dictionary
定义存入和取出函数
取出函数写法:
判断有无对应name下的缓存模块
如果没有直接进行实例化将name作为路径传入
如果有直接取出第0个obj并且移除位于list中的obj
并且进行object激活
存入函数写法
存入也就是销毁物体
如果缓存池中无name对应list创建name和list,如果已经有list就加上这个obj
Invoke复习
Invoke(“SendMsg”, 5); 它的意思是:5 秒之后调用 SendMsg() 方法
Invoke(); 不能接受含有 参数的方法;
Invoke() 也支持重复调用:InvokeRepeating(“SendMsg”, 2 , 3);
CancelInvoke();取消调用
注意:常用OnEnable函数(当对象激活时会进入声明周期函数)代替Start函数因为反复调用的时候Start函数只会进行第一次
public class PoolMgr : BaseManager
{
public Dictionary> poolDic = new Dictionary>();
public GameObject Getobj(string name)
{
GameObject obj = null;
if ( poolDic.ContainsKey(name)&& poolDic[name].Count >0)
{
obj =poolDic[name][0];
poolDic[name].RemoveAt(0);
}
else
{
obj= GameObject.Instantiate(Resources.Load(name));
obj.name = name;//很关键:把对象名改成池子名字一样防止因clone导致的错误
}
obj.SetActive(true);
return obj;
}
public void PushObj(string name,GameObject obj )
{
obj.SetActive(false);
if(poolDic.ContainsKey(name))
{
poolDic[name].Add(obj);
}
else
{
poolDic.Add(name,new List(){obj});
}
}
}
增加一个Pool父物体,将缓存池中的子物体存储于其中,在get的时候解除父子关系调出,在false的时候进入父子关系
在切换场景的时候原先的物体已经清除了但原先内存上的关联关系仍然没有被清除所以会产生错误,解决的方法是新增clear方法。
public void clear()
{
poolDic.Clear();
poolObj= null;
}
再给Pool物体区分的更细节(这里先省略一下,以后再补,有点难以理解而且我暂时没用到不太需要)
}
public class PoolData
{
public GameObject fatherObj;
public List poolList;
public PoolData(GameObject obj,GameObject poolObj)
{
fatherObj = new GameObject(obj.name);
fatherObj.transform.parent=poolObj.transform;
poolList = new List(){obj };
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
///
/// 抽屉数据 池子中的一列容器
///
public class PoolData
{
//抽屉中 对象挂载的父节点
public GameObject fatherObj;
//对象的容器
public List poolList;
public PoolData(GameObject obj, GameObject poolObj)
{
//给我们的抽屉 创建一个父对象 并且把他作为我们pool(衣柜)对象的子物体
fatherObj = new GameObject(obj.name);
fatherObj.transform.parent = poolObj.transform;
poolList = new List() {};
PushObj(obj);
}
///
/// 往抽屉里面 压都东西
///
///
public void PushObj(GameObject obj)
{
//失活 让其隐藏
obj.SetActive(false);
//存起来
poolList.Add(obj);
//设置父对象
obj.transform.parent = fatherObj.transform;
}
///
/// 从抽屉里面 取东西
///
///
public GameObject GetObj()
{
GameObject obj = null;
//取出第一个
obj = poolList[0];
poolList.RemoveAt(0);
//激活 让其显示
obj.SetActive(true);
//断开了父子关系
obj.transform.parent = null;
return obj;
}
}
///
/// 缓存池模块
/// 1.Dictionary List
/// 2.GameObject 和 Resources 两个公共类中的 API
///
public class PoolMgr : BaseManager
{
//缓存池容器 (衣柜)
public Dictionary poolDic = new Dictionary();
private GameObject poolObj;
///
/// 往外拿东西
///
///
///
public void GetObj(string name, UnityAction callBack)
{
//有抽屉 并且抽屉里有东西
if (poolDic.ContainsKey(name) && poolDic[name].poolList.Count > 0)
{
callBack(poolDic[name].GetObj());
}
else
{
//通过异步加载资源 创建对象给外部用
ResMgr.GetInstance().LoadAsync(name, (o) =>
{
o.name = name;
callBack(o);
});
//obj = GameObject.Instantiate(Resources.Load(name));
//把对象名字改的和池子名字一样
//obj.name = name;
}
}
///
/// 换暂时不用的东西给我
///
public void PushObj(string name, GameObject obj)
{
if (poolObj == null)
poolObj = new GameObject("Pool");
//里面有抽屉
if (poolDic.ContainsKey(name))
{
poolDic[name].PushObj(obj);
}
//里面没有抽屉
else
{
poolDic.Add(name, new PoolData(obj, poolObj));
}
}
///
/// 清空缓存池的方法
/// 主要用在 场景切换时
///
public void Clear()
{
poolDic.Clear();
poolObj = null;
}
减少代码量,减少复杂性,降低程序的耦合度
核心思路:设置事件中心将事件加进去事件发生时通知监听者
原理:基于字典和委托
KEY–事件的名字(玩家死亡,怪物死亡)
value–监听这个事件的对应的委托函数们
因为委托可以通过+=和-=所以可以有很多委托
需要事件的监听方法:两个参数,事件名和其监听者的委托函数
事件触发函数:得到哪个函数被触发了 参数是name 找到委托函数并且触发
public class EventCenter : BaseManager
{
private Dictionary eventDic =new Dictionary();
public void AddEventListener(string name,UnityAction action)
{
if(eventDic.ContainsKey(name))
{
eventDic[name]+=action;
}
else
{
eventDic.Add(name,action);
}
}
public void EventTrigger(string name)
{
if(eventDic.ContainsKey(name))
{
eventDic[name]();
}
}
}
事件中心会由开始到程序结束都存在
将UnityAction中传入Object参数增强事件系统的通用性
public class EventCenter : BaseManager
{
private Dictionary> eventDic =new Dictionary>();
public void AddEventListener(string name,UnityAction
方法是使用泛型类型,字典中存储泛型的基类接口来实现减少传入参数为object带来的装箱拆箱所造成的消耗,并且使用重载的方法处理不带有类型参数的情况
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public interface IEventInfo
{
}
public class EventInfo : IEventInfo
{
public UnityAction actions;
public EventInfo( UnityAction action)
{
actions += action;
}
}
public class EventInfo : IEventInfo
{
public UnityAction actions;
public EventInfo(UnityAction action)
{
actions += action;
}
}
///
/// 事件中心 单例模式对象
/// 1.Dictionary
/// 2.委托
/// 3.观察者设计模式
/// 4.泛型
///
public class EventCenter : BaseManager
{
//key —— 事件的名字(比如:怪物死亡,玩家死亡,通关 等等)
//value —— 对应的是 监听这个事件 对应的委托函数们
private Dictionary eventDic = new Dictionary();
///
/// 添加事件监听
///
/// 事件的名字
/// 准备用来处理事件 的委托函数
public void AddEventListener(string name, UnityAction action)
{
//有没有对应的事件监听
//有的情况
if( eventDic.ContainsKey(name) )
{
(eventDic[name] as EventInfo).actions += action;
}
//没有的情况
else
{
eventDic.Add(name, new EventInfo( action ));
}
}
///
/// 监听不需要参数传递的事件
///
///
///
public void AddEventListener(string name, UnityAction action)
{
//有没有对应的事件监听
//有的情况
if (eventDic.ContainsKey(name))
{
(eventDic[name] as EventInfo).actions += action;
}
//没有的情况
else
{
eventDic.Add(name, new EventInfo(action));
}
}
///
/// 移除对应的事件监听
///
/// 事件的名字
/// 对应之前添加的委托函数
public void RemoveEventListener(string name, UnityAction action)
{
if (eventDic.ContainsKey(name))
(eventDic[name] as EventInfo).actions -= action;
}
///
/// 移除不需要参数的事件
///
///
///
public void RemoveEventListener(string name, UnityAction action)
{
if (eventDic.ContainsKey(name))
(eventDic[name] as EventInfo).actions -= action;
}
///
/// 事件触发
///
/// 哪一个名字的事件触发了
public void EventTrigger(string name, T info)
{
//有没有对应的事件监听
//有的情况
if (eventDic.ContainsKey(name))
{
//eventDic[name]();
if((eventDic[name] as EventInfo).actions != null)
(eventDic[name] as EventInfo).actions.Invoke(info);
//eventDic[name].Invoke(info);
}
}
///
/// 事件触发(不需要参数的)
///
///
public void EventTrigger(string name)
{
//有没有对应的事件监听
//有的情况
if (eventDic.ContainsKey(name))
{
//eventDic[name]();
if ((eventDic[name] as EventInfo).actions != null)
(eventDic[name] as EventInfo).actions.Invoke();
//eventDic[name].Invoke(info);
}
}
///
/// 清空事件中心
/// 主要用在 场景切换时
///
public void Clear()
{
eventDic.Clear();
}
}
作用:让没有继承mono的类可以开启协程进行update真更新
Mono的管理者
类中的构造函数在被new的时候可以进行执行
public class MonoController : MonoBehaviour
{
private event UnityAction updateEvent;
void Start()
{
DontDestroyOnLoad(this.gameObject);
}
void Update()
{
if(updateEvent != null)
updateEvent();
}
public void AddUpdateListener(UnityAction fun)
{
updateEvent+=fun;
}
public void RemoveUpdateListener(UnityAction fun)
{
updateEvent-=fun;
}
}
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using UnityEngine;
using UnityEngine.Events;
///
/// 1.可以提供给外部添加帧更新事件的方法
/// 2.可以提供给外部添加 协程的方法
///
public class MonoMgr : BaseManager
{
private MonoController controller;
public MonoMgr()
{
//保证了MonoController对象的唯一性
GameObject obj = new GameObject("MonoController");
controller = obj.AddComponent();
}
///
/// 给外部提供的 添加帧更新事件的函数
///
///
public void AddUpdateListener(UnityAction fun)
{
controller.AddUpdateListener(fun);
}
///
/// 提供给外部 用于移除帧更新事件函数
///
///
public void RemoveUpdateListener(UnityAction fun)
{
controller.RemoveUpdateListener(fun);
}
public Coroutine StartCoroutine(IEnumerator routine)
{
return controller.StartCoroutine(routine);
}
public Coroutine StartCoroutine(string methodName, [DefaultValue("null")] object value)
{
return controller.StartCoroutine(methodName, value);
}
public Coroutine StartCoroutine(string methodName)
{
return controller.StartCoroutine(methodName);
}
}
可以提供给外部没有继承自Mono方法真更新的方法和协程的方法
new一个t再调用monoMgr.getinstance.addupdatelistener(t.事件名)就可以实现真更新
目的是当从场景A切换到场景B的时候需要do sth动态的创建一些玩家物件
同步的时候会出现卡顿的情况
u3d异步加载会和协程配合使用
需要有ienumerator
编写一个接口调用unity自带的u3dasync加载
用协程来配合异步加载
ao.progress可以得到场景加载的进度
再结合事件中心分发事件更新加载条
执行完毕之后调用传入的fun方法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
///
/// 场景切换模块
/// 知识点
/// 1.场景异步加载
/// 2.协程
/// 3.委托
///
public class ScenesMgr : BaseManager
{
///
/// 切换场景 同步
///
///
public void LoadScene(string name, UnityAction fun)
{
//场景同步加载
SceneManager.LoadScene(name);
//加载完成过后 才会去执行fun
fun();
}
///
/// 提供给外部的 异步加载的接口方法
///
///
///
public void LoadSceneAsyn(string name, UnityAction fun)
{
MonoMgr.GetInstance().StartCoroutine(ReallyLoadSceneAsyn(name, fun));
}
///
/// 协程异步加载场景
///
///
///
///
private IEnumerator ReallyLoadSceneAsyn(string name, UnityAction fun)
{
AsyncOperation ao = SceneManager.LoadSceneAsync(name);
//可以得到场景加载的一个进度
while(!ao.isDone)
{
//事件中心 向外分发 进度情况 外面想用就用
EventCenter.GetInstance().EventTrigger("进度条更新", ao.progress);
//这里面去更新进度条
yield return ao.progress;
}
//加载完成过后 才会去执行fun
fun();
}
}
BV1N4411B7i3
协程和C#的迭代器有关IEnumerator
协程的开始在Start这种一次性执行的函数之中调用不可以在Update中调用StartCoroutine
yield return null实现以下的代码都在下一帧中进行运行。
协程在update和lateupdate之间执行的
yell return 数字表示协程后面的所有代码在下一帧进行执行
同步加载资源和异步加载资源
泛型作用:加载后判断是否为Gameobject直接进行实例化否则就返回
开协程通常都是一个方法是调用真正的协程函数
里式转换原则用基类存子类可以将子类转换为基类
异步具体什么时间调用时Unity内部的协程调度器控制的,它会根据你yield return返回的内容自己去判断到底何时继续执行后面的代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
///
/// 资源加载模块
/// 1.异步加载
/// 2.委托和 lambda表达式
/// 3.协程
/// 4.泛型
///
public class ResMgr : BaseManager
{
//同步加载资源
public T Load(string name) where T:Object
{
T res = Resources.Load(name);
//如果对象是一个GameObject类型的 我把他实例化后 再返回出去 外部 直接使用即可
if (res is GameObject)
return GameObject.Instantiate(res);
else//TextAsset AudioClip
return res;
}
//异步加载资源
public void LoadAsync(string name, UnityAction callback) where T:Object
{
//开启异步加载的协程
MonoMgr.GetInstance().StartCoroutine(ReallyLoadAsync(name, callback));
}
//真正的协同程序函数 用于 开启异步加载对应的资源
private IEnumerator ReallyLoadAsync(string name, UnityAction callback) where T : Object
{
ResourceRequest r = Resources.LoadAsync(name);
yield return r;
if (r.asset is GameObject)
callback(GameObject.Instantiate(r.asset) as T);
else
callback(r.asset as T);
}
}
BV1Bk4y1B7DN
委托:存储封装几个方法
委托的构造
用统一的输出输出Mgr检测输入输出来
一旦有某个输入就用分发事件分发到事件中心告诉他触发了
使用的时候只需要在start里面将键盘输入设为true并且添加监听方法就可以了
可以综合Switch语句使用来判断具体按了哪个键位
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 1.Input类
/// 2.事件中心模块
/// 3.公共Mono模块的使用
///
public class InputMgr : BaseManager
{
private bool isStart = false;
///
/// 构造函数中 添加Updata监听
///
public InputMgr()
{
MonoMgr.GetInstance().AddUpdateListener(MyUpdate);
}
///
/// 是否开启或关闭 我的输入检测
///
public void StartOrEndCheck(bool isOpen)
{
isStart = isOpen;
}
///
/// 用来检测按键抬起按下 分发事件的
///
///
private void CheckKeyCode(KeyCode key)
{
//事件中心模块 分发按下抬起事件
if (Input.GetKeyDown(key))
EventCenter.GetInstance().EventTrigger("某键按下", key);
//事件中心模块 分发按下抬起事件
if (Input.GetKeyUp(key))
EventCenter.GetInstance().EventTrigger("某键抬起", key);
}
private void MyUpdate()
{
//没有开启输入检测 就不去检测 直接return
if (!isStart)
return;
CheckKeyCode(KeyCode.W);
CheckKeyCode(KeyCode.S);
CheckKeyCode(KeyCode.A);
CheckKeyCode(KeyCode.D);
}
}
BV1oq4y1H7wz
值类型向引用类型转化称为装箱
引用类型向值类型转化称为拆箱
区分背景音乐和音效
要有 播放音效 停止音效 播放背景音乐 停止背景音乐 四个方法
大部分的游戏并非使用的3D音乐而是2D音乐即不随着远近改变而改变
声明唯一的背景音乐 创建Gameobject直接挂上
暂停音乐
播放音乐
改变音量大小
播放音效的方法
播放音效创建Gameobject也挂上,区别是需要委托函数并且主要内容需要写在异步加载后
可以结合缓存池
需要检测自己是否播放完播放完就把自己移除掉防止内存占用过高导致崩溃
关键属性:唯一的背景音乐组件,音乐大小,音效依附对象,音效列表,音效大小
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class MusicMgr : BaseManager
{
//唯一的背景音乐组件
private AudioSource bkMusic = null;
//音乐大小
private float bkValue = 1;
//音效依附对象
private GameObject soundObj = null;
//音效列表
private List soundList = new List();
//音效大小
private float soundValue = 1;
public MusicMgr()
{
MonoMgr.GetInstance().AddUpdateListener(Update);
}
private void Update()
{
for( int i = soundList.Count - 1; i >=0; --i )
{
if(!soundList[i].isPlaying)
{
GameObject.Destroy(soundList[i]);
soundList.RemoveAt(i);
}
}
}
///
/// 播放背景音乐
///
///
public void PlayBkMusic(string name)
{
if(bkMusic == null)
{
GameObject obj = new GameObject();
obj.name = "BkMusic";
bkMusic = obj.AddComponent();
}
//异步加载背景音乐 加载完成后 播放
ResMgr.GetInstance().LoadAsync("Music/BK/" + name, (clip) =>
{
bkMusic.clip = clip;
bkMusic.loop = true;
bkMusic.volume = bkValue;
bkMusic.Play();
});
}
///
/// 暂停背景音乐
///
public void PauseBKMusic()
{
if (bkMusic == null)
return;
bkMusic.Pause();
}
///
/// 停止背景音乐
///
public void StopBKMusic()
{
if (bkMusic == null)
return;
bkMusic.Stop();
}
///
/// 改变背景音乐 音量大小
///
///
public void ChangeBKValue(float v)
{
bkValue = v;
if (bkMusic == null)
return;
bkMusic.volume = bkValue;
}
///
/// 播放音效
///
public void PlaySound(string name, bool isLoop, UnityAction callBack = null)
{
if(soundObj == null)
{
soundObj = new GameObject();
soundObj.name = "Sound";
}
//当音效资源异步加载结束后 再添加一个音效
ResMgr.GetInstance().LoadAsync("Music/Sound/" + name, (clip) =>
{
AudioSource source = soundObj.AddComponent();
source.clip = clip;
source.loop = isLoop;
source.volume = soundValue;
source.Play();
soundList.Add(source);
if(callBack != null)
callBack(source);
});
}
///
/// 改变音效声音大小
///
///
public void ChangeSoundValue( float value )
{
soundValue = value;
for (int i = 0; i < soundList.Count; ++i)
soundList[i].volume = value;
}
///
/// 停止音效
///
public void StopSound(AudioSource source)
{
if( soundList.Contains(source) )
{
soundList.Remove(source);
source.Stop();
GameObject.Destroy(source);
}
}
}
BV14v411h7UP
总结共性通用模块
目的要找到自己面板下的控件对象并且提供隐藏显示方法
找到子对象的控件用泛型存入字典
编写得到对应名字的控件方法
显示面板\隐藏面板
里式转换原则:基类装子类
显示方法:创建出来 位子设置对 存到对应父物体下
需要回调函数便于修改basepanel