C#框架
1、单例
where T : class , new();
代表着是对通用参数T的约束,它必须是class类型(引用类型),且它必须具备公共参数的默认构造函数。
①继承于Mono的实例单例
(挂载在脚本上)【程序开启时自动创建】
进阶:当忘记挂载在脚本上时自动创建挂载,并且切换场景时不销毁。
②普通C#类的实例单例
(泛型单例,作为单例基类使用)【使用时自动创建】
不挂载到场景当中,不需要继承MonoBehaviour,减少内存消耗
优点:代码相对简单,只需要继承Singleton<T>类就可以实现单例类。
缺点:因为泛型约束中填写了new(),因此子类无法私有化构造函数,子类依然可以通过new来实例化对象。
③管理器基类单例
需要手动进行初始化单例,并不是在创建时就生成。
④逻辑管理器基类(单例)
继承此基类,不仅可以实现自动单例,同时也必须实现事件的监听以及取消方法,通过OnEnable以及OnDisable方法来
实现挂载对象的监听事件以及取消事件的执行。
2、对象池
①游戏物体和非游戏物体
Unity开发中,我们有两种类型的实例,一个是GameObject,一种是普通的C#类,对象池中也要兼容这两种不同的类型。
-
对象池添加特性,可根据此特性来配置该对象池的相关个性化参数。
-
利用list性能会消耗大一点,所以尽量使用Queue。
②对象池管理器具备通用功能
-
从对象池中获取游戏对象或者C#普通对象
-
先从对象池中取,若无,则生成一个游戏对象/C#普通对象。
-
游戏对象的获取通过预制体、路径获取,C#普通对象通过泛型名称获取
-
将游戏对象或者C#普通对象加入对象池当中
-
将游戏对象或者C#普通对象从对象池当中批量/全部删除
③游戏对象池有根节点,挂载在场景对象当中
C#普通对象无根节点,放在队列当中,需要进行封箱、拆箱操作。
④游戏对象生成时放置于根节点的对应游戏对象名称当中
对象全部隐藏状态,取出用时将其放置其他位置当中。
父物体为Null后GameRoot场景属于DontDestroy场景中,而不是默认场景,所以需要回归默认场景。
3、状态机
( 非动画状态机,代码逻辑层面的有限状态机实现)
①状态进入、更新采用拓展的方式切换
![](https://i-blog.csdnimg.cn/blog_migrate/05caab3761176c19e609a1bc410cf30f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b7cd8ac566ff3733bd6e78ea26147535.png)
②状态机实现方式
![](https://i-blog.csdnimg.cn/blog_migrate/5f6bc65bcd6bb38b4c558d2f882d5d30.png)
-
状态接口 - 定义状态的行为和状态切换
-
具体状态 - 实现状态的行为和状态切换
-
系统 - 负责状态的切换和状态的实际调用以及状态的管理。(包括定义宿主)
4、场景根节点
①根节点使用挂载脚本的单例模式,管理器使用自定义的单例模式(需主动开启单例),错开单例生成顺序。
②初始化生成所有管理器单例
③[InitializeOnLoadMethod]特性:在Editor模式下刷新时自动运行一次,一般用于当需要确保某一个核心对象处于正确位置时使用
-
设置GameRoot为游戏根节点,手动设为单例模式
-
清空事件
-
初始化全部管理器,手动设为单例模式
-
将带有Pool特性的类型加入缓存池字典
-
将UI元素类型加入缓存
[InitializeOnLoad]特性:在Editor文件夹下,添加了InitializeOnLoad特性后,需要添加static关键字,其构造方法会自动执行,测试结果是,每次修改这个类的内容,就会重新执行一遍构造方法。可以在构造方法中执行一些操作,来控制Editor模式下的代码执行。
[RuntimeInitializeOnLoad],Editor运行时,可以增加参数, 执行顺序BeforeRuntime、Awake、OnRuntime、AfterRuntime、Start、Update。
5、文件存储及写入
参考博客: C#中文件读写类(File、FileStream和StreamReader等类)-简记_不全的博客-CSDN博客
-
对于不是一次性写入操作,可使用File.AppendAllText方法,追加写入
-
对于一次性读取行操作,可使用File.ReadAllLines方法,返回string[]
-
ConcurrentQueue表示线程安全的队列集合,而Queue不保证线程安全,添加的同时进行读取或者其他操作就会出现问题。
-
①ConcurrentQueue的常用属性
Count 获取队列内元素数量
-
②ConcurrentQueue的常用方法
public void EnQueue (Object obj) 向 Queue 的末尾添加一个对象。
public bool TryDequeue () 移除在 Queue 的开头的对象。
public bool IsEmpty () 判断队列是否为空 (1. 头节点(段)不为空返回false; 2. 头节点为空而且下一个节点也为空返回true; 3. 头节点为空而且下一个节点不为空返回false,这种情况说明队列正在扩容,所以要自选等待扩容完毕时再次进行判断)
-
Path.Combine方法----将多个字符串组合成一个路径
String.Format方法------将对象的值转换为基于指定格式的字符串,并将其插入到另一个字符串。
-
创建线程执行读写日志操作(线程开启后,可根据需求何时进行取消)
Task.Factory.StartNew(Action,CancellationToken)-----为指定的异步执行的动作委派和取消标记创建并启动任务
当收到线程取消的指令后,带有异步执行委托的线程关闭。
-
StringBuilder的优点:弥补了string在赋值时开辟新空间不足之处
String 对象是不可改变的。每次使用 System.String 类中的方法之一时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间。
在需要对字符串执行重复修改的情况下,与创建新的 String 对象相关的系统开销可能会非常昂贵。
如果要修改字符串而不创建新的对象,则可以使用 System.Text.StringBuilder 类。
例如,当在一个循环中将许多字符串连接在一起时,使用 StringBuilder 类可以提升性能。
-
在进行数据转换前选择转换方法要谨慎,如果是数字类型可以考虑直接用(int)强制转换,如果是整型字符串类型的,考虑用int.Parse()进行转换,如果不是这两种类型,再考虑用Convert.ToInt32()进行转换。
-
Application.persistentDataPath(数据持久化路径 ,热更的重要路径,该文件夹可读可写,在移动端唯一一个可读写操作的文件夹。) 电脑存储路径:C:\Users\Admin\AppData\LocalLow\(unity的CompanyName)\(Unity的ProductName)
-
Oculus设备存储路径:/storage/emulated/0/Android/data/(unity的PackageName)/files/
-
电脑存储路径:C:\Users\Admin\AppData\LocalLow\(unity的CompanyName)\(Unity的ProductName)
6、系统
-
配置系统---需要用到奥丁插件
-
配置系统( 开发时各种配置的缓存、游戏中诸如角色配置、装备配置等)
①此方式可以实现几个功能:
1、可以通过面板去设置此对象
2、外界可以获取此对象,但不能在代码层面去修改它
如果实现方法是:public GamSetting gameSetting;
则可通过面板去设置此对象,但外界可以通过代码去修改,重要文件不建议这样实现。
②当多个脚本需要继承同一个类的时候,而且这些脚本属于同一系列,使用同一个基类的方式来统一管理,方便区分。
③配置分为非框架层面的全局配置(包含所有的配置)以及框架层面的游戏配置(如对象池配置、UI层级管理)
④通过继承同一个基类来实现统一通用配置,所有继承于ConfigBase的子类配置都可以存储进去,后续取出时进行类型转换。
⑤Unity编译前运行(Unity编译之前可以执行我们指定的函数)
-
有些配置需要在编译前自动计算,就可以通过这种方式来实现,但会影响编译效率,微乎其微可以忽略。
-
可通过两种方式实现编译前执行函数:
-
通过Odin插件的特性按钮的方式实现
-
通过GameRoot中的[InitializeOnLoadMethod]特性实现
⑥特性遍历搜寻:
-
获取所有程序集,遍历所有程序集
-
获取程序集下的所有类型,遍历所有类型
-
获取Pool特性,若该Pool特性存在,则缓存该类型
-
⑦通过特性设置UI窗口元数据,并将其存储至缓存字典当中
-
获取所有程序集,遍历所有程序集
-
获取程序集下的所有类型,遍历所有类型
-
获取UIElementAttribute特性,若该特性不为空,则获取特性中的元数据,并将其中的数据存储至对应此类型的字典当中
-
-
事件系统
自定义单个事件工具(EventManager)①通过内部接口来实现内部类通过类型转换来转变成对应的事件类②事件框架逻辑:(利用字典存储不同的事件类型)【同时也是拓展类及函数】- 添加事件监听(添加Action)
- 触发事件
- 取消事件监听(移除Action)
- 移除事件
一类事件工具
(鼠标、碰撞、触发等事件)【常用类型,根据类型来处理】(EventListener)( 统一 触发/碰撞/鼠标点击、进入、拖拽等等一系列游戏事件)①事件种类包括:
某个事件中一个时间的数据包装类
一类事件的数据包装类型
②通过继承IEqualityComparer<T>接口来实现复杂类型对象的比较,实现去重的作用。
.NET处理时先比较两个对象GetHashCode方法返回的哈希值是否相等,如果不相等则直接返回,不再执行Equals方法,如果哈希值相等则执行Equals方法继续比较!
为什么要用两个方法去对比呢?原因是GetHashCode方法比Equals方法效率更高,所以先执行GetHashCode方法。
③继承鼠标、碰撞、触发事件接口并实现
③模块细化事件工具系统(通过拓展方式实现),简化一类事件工具操作(EventListenerExtend)
存档管理系统( 本地序列化存档,支持多存档机制、自动更新存档时间、存档切换)
①存档的数据类需要允许被序列化,加上特性[Serializable]
②List集合的排序
方法一:增加比较器
方法二:使用List的拓展方法OrderByDescending与OrderBy
③BinaryFormatter:以二进制格式序列化和反序列化对象或连接对象的整个图形。
MemoryStream位于System.IO命名空间,为系统内存提供流式的读写操作。常作为其他流数据交换时的中间对象操作。
保存文件:
加载文件:
④存档管理框架:
存档管理数据(当前ID、存档列表(ID、上次保存时间))
存档对象的缓存字典<存档ID、<文件名称、实际的对象>>
保存、读取存档文件(扩展方法)
【文件保存先保存到文件夹当中、更新存档时间同步更新保存管理数据、同步保存进入缓存当中】
【文件读取先从缓存中读取、若缓存中没有则去文件夹中读取,读取完毕后重新设置缓存】
存档文件夹架构
一级:saveData、setting
二级:saveData/SaveManagerData、ID、setting/具体内容的名称
三级:saveDate/ID(具体)/具体内容的名称、setting/具体内容的名称/具体内容
四级:saveDate/ID(具体)/具体内容的名称/具体内容
⑤const的值是在编译期间确定的,因此只能在声明时通过常量表达式指定其值。
而static readonly,在程式中只读, 不过它是在运行时计算出其值的,所以还能通过静态构造函数来对它赋值,
本地化系统( 支持多语言文字、图片切换)
①通过内部接口实现不同类型之间的切换,不同语言之间的切换
②通过事件触发方式来切换语言类型,在此之前需要监听具体的语言事件
③本地化数据设置存储方式
场景管理系统
( 场景切换、异步加载管理等)
①两种方式进行场景管理:同步加载场景与异步加载场景
1、同步加载场景:利用Unity内置的场景管理器进行同步加载场景
2、异步加载场景:利用协程实现场景切换,使用Unity内置的场景管理器进行异步加载场景
同步与异步的区别:同步与异步的区别(一看则懂)_同步和异步的区别-CSDN博客
7、Unity拓展
①拓展方法的三个要求:
声明扩展方法的类必须声明为 static;
扩展方法本身必须声明为static;
扩展方法第一个参数类型前一定要包含关键字 this。
注意:需要特别说明的是,扩展方法可以拥有多个参数,但第一个参数的位置始终是属于为扩展对象的,不能改变。也就是说只有第一个参数可以并且必须用this关键字修饰,其他的参数视为方法的普通参数。
②拓展方法一般定义为通用方法,方便开发过程中直接使用。
资源管理:对象池的push操作
通用拓展:数组对比、获取特性
本地化拓展:从本地化系统中获取修改的内容(文本、图片、音效、视频)
Mono:添加、移除Update、LateUpdate、FixedUpdate监听、开启、关闭协程
static关键字的作用:
static可修饰类、字段、属性、方法。
static在使用的时候分三种方法。
1.修饰类的时候:
将类修饰为静态的,意味着这个类不能实例化对象,在调用类里面的任何内容的时候直接用类名加点的形式调用其中的方法或者字段(类名.方法/类名.字段)
2.修饰方法或者属性的时候:
将方法修饰为静态方法,表示此方法为所在的类所有,而不是这个类实例化的对象所拥有,这个方法在调用的时候需要类名加点(类名.方法)来调用。
3.修饰变量:
表示在每次调用该变量的方法或者类的时候,变量的值为最后一次赋值时的值,而不是再次初始化它的值。
8、开发插件
Odin插件
①可以通过特性的方式来修改Inspector的界面展现形式,与ScriptableObject结合起来使用效果会更好。
②由于Unity本身的Editor编程中,存在其自身的局限性,比如:无法序列化Dictionary,对多态、空值、循环嵌套等序列化显示,Inspector或EditorWindow多线程下复杂属性显示处理等等;
③通过使用Odin中给予的Attributes快速影响所需要编辑的字段,帮助开发者快速开发可视化界面及工具。
④使用Odin插件需要继承SerializedScriptableObject,引用using Sirenix.OdinInspector;
官网:Odin Inspector and Serializer | Improve your workflow in Unity
9、服务
资源服务
①检查注册的类型是否需要缓存(是否注册进对象池中)
②从资源文件夹中加载Unity资源
③获取实例-普通Class、组件,如果类型需要缓存,会从对象池中获取,否则重新生成
④资源异步加载
1、异步加载游戏对象,优先从对象池中获取,若无则重新异步生成
2、异步加载Unity资源,使用ResourceRequest
⑤预制体实例化(通过路径,具体对象)
预制体获取后并不是显示在场景当中(它是个资源),需要进行实例化。
音效服务
①具体功能包括:全局音量控制、背景音量控制、特效音量控制、循环、暂停、静音
②背景音乐播放需要默认挂载在场景当中(不销毁)。
特效音乐需另外生成场景挂载,并加进列表当中缓存。
③特效音乐播放完毕后异步回收,特效时间短数量多适合异步回收,回收进对象池当中。
更新特效音乐播放器,如果已经被回收,则清空移除列表。
管理器总服务---注意!!!防止重复加入Update,最好加入Debug!
①增添Update(更新)、FixedUpdate、LateUpdate事件,增添事件通过一个脚本将Update的更新功能以委托事件的方式统一管理起来。10、【UI(界面)】
( UI层级管理(谁压住谁,上层出现时下层不能交互)、Tips、Loading等 )①通过UIElementAttribute方式定义UI界面的相关信息,通过字典进行存储,循环遍历特性的方式来获取特性里的UI内容数据
UI特性:
- 是否缓存
- UI资源地址
- 层级
UI元素:
- 是否缓存
- UI的预制体
- UI层级
- 具体实例对象
②遮罩层级管理,使得生效对象始终在最上层。
③窗口的显示,分两个重要步骤,一是窗口类型的选择,二是遮罩的处理(对应的窗口对象有且只有一个)
字典中查找是否注册有对应的UI窗口类型
判断是否有对应实例化的窗口对象,若有则显示、设置根节点、放置最后且更新字体,若无则创建
设置遮罩,保证生效的只有最后的对象。
④由于C#本身有GC机制,当对象的引用为0的时候就会被垃圾回收,对应的引用则会被置为null, 但Unity里边,调Destroy删除一个Object,只是释放了Unity的资源(既外在显示的Unity对象),而在C#层面,这个Object对应的引用都还在,那么它便不会被当成垃圾回收掉,所以C#层的资源并没有释放,但是拿它的引用跟null做对比确实相等的。
因此,Unity的资源在被destroy之后,需考虑引用是否需要置Null,若下次依旧需要变更判断则置Null。
⑤UITips动画可自由进行录制以及添加动画事件
提示信息采用队列形式进行数据存储
⑥字典里的遍历
⑦本地坐标转世界坐标
本地坐标转换为世界坐标API:tramsform.TransformPoint();
世界坐标转换为本地坐标方法:将游戏物体设置目标物体为父对象,然后获取其本地坐标,之后解绑