一、C#实用基础
1、C#自定义特性
① 一个新的自定义特性应派生自 System.Attribute 类。
②where是限定此类继承于哪个脚本或者接口
特性的详细介绍:
1、特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。是通过放置在它所应用的元素面前的方括号([])来描述的。特性用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。2、.Net框架提供了三种预定义特性:①AttributeUsage预定义特性 AttributeUsage 描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。[AttributeUsage(validon,AllowMultiple=allowmultiple,Inherited=inherited)]其中:* 参数 validon 规定特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是 AttributeTargets.All。* 参数 allowmultiple(可选的)为该特性的 AllowMultiple 属性(property)提供一个布尔值。如果为 true,则该特性是多用的。默认值是 false(单用的)。* 参数 inherited(可选的)为该特性的 Inherited 属性(property)提供一个布尔值。如果为 true,则该特性可被派生类继承。默认值是 false(不被继承)。例如:// 一个自定义特性 BugFix 被赋给类及其成员[AttributeUsage(AttributeTargets.Class |AttributeTargets.Constructor |AttributeTargets.Field |AttributeTargets.Method |AttributeTargets.Property,AllowMultiple = true)]public class DeBugInfo : System.Attribute②Conditional这个预定义特性标记了一个条件方法,其执行依赖于指定的预处理标识符。它会引起方法调用的条件编译,取决于指定的值,比如 Debug 或 Trace。例如,当调试代码时显示变量的值。规定该特性的语法如下:[Conditional(conditionalSymbol)]例如:[Conditional("DEBUG")]③Obsolete这个预定义特性标记了不应被使用的程序实体。它可以让您通知编译器丢弃某个特定的目标元素。例如,当一个新方法被用在一个类中,但是您仍然想要保持类中的旧方法,您可以通过显示一个应该使用新方法,而不是旧方法的消息,来把它标记为 obsolete(过时的)。规定该特性的语法如下:[Obsolete(message)][Obsolete(message,iserror)]其中:* 参数 message,是一个字符串,描述项目为什么过时以及该替代使用什么。* 参数 iserror,是一个布尔值。如果该值为 true,编译器应把该项目的使用当作一个错误。默认值是 false(编译器生成一个警告)。
③运用特性+反射
using System;
using System.Reflection;//System.Reflection 类的 MemberInfo用于发现与类相关的特性(attribute)。
namespace BugFixApplication{
// 一个自定义特性 BugFix 被赋给类及其成员
[AttributeUsage
#region//定义了特性能被放在那些前面
(AttributeTargets.Class |//规定了特性能被放在class的前面
AttributeTargets.Constructor |//规定了特性能被放在构造函数的前面
AttributeTargets.Field |//规定了特性能被放在域的前面
AttributeTargets.Method |//规定了特性能被放在方法的前面
AttributeTargets.Property,//规定了特性能被放在属性的前面
#endregion
AllowMultiple = true)]//这个属性标记了我们的定制特性能否被重复放置在同一个程序实体前多次。
public class DeBugInfo : System.Attribute//继承了预定义特性后的自定义特性
{
private int bugNo;
private string developer;
private string lastReview;
public string message;
public DeBugInfo(int bg,string dev,string d)//构造函数,接收三个参数并赋给对应实例
{
this.bugNo = bg;
this.developer = dev;
this.lastReview = d;
}
#region//定义对应的调用,返回对应值value
public int BugNo
{
get
{
return bugNo;
}
}
public string Developer
{
get
{
return developer;
}
}
public string LastReview
{
get
{
return lastReview;
}
}
//前面有public string message;
public string Message//定义了可以通过Message = "",来对message进行赋值。
//因为不在构造函数中,所以是可选的
{
get
{return message;}
set
{message = value;}
}
/*
* 这部分可以简写如下
* public string Message{get;set;}
*/
}
#endregion
[DeBugInfo(45, "Zara Ali", "12/8/2012",
Message = "Return type mismatch")]
[DeBugInfo(49, "Nuha Ali", "10/10/2012",
Message = "Unused variable")]//前面定义时的AllowMultiple=ture允许了多次使用在同一地方
class Rectangle
{
protected double length;
protected double width;//定义两个受保护的(封装)的成员变量
public Rectangle(double l,double w)//构造函数,对两个成员变量进行初始化,公开的
{
length = l;
width = w;
}
[DeBugInfo(55, "Zara Ali", "19/10/2012",
Message = "Return type mismatch")]
public double GetArea()
{
return length * width;
}
[DeBugInfo(56, "Zara Ali", "19/10/2012")]//因为message是可选项,所以可以不给出
//不给出即为null,为空白
public void Display()
{
Console.WriteLine("Length: {0}", length);
Console.WriteLine("Width:{0}", width);
Console.WriteLine("Area:{0}", GetArea());//常规打印
}
}
class ExecuteRectangle
{
static void Main(string[] args)//程序入口
{
Rectangle r = new Rectangle(4.5, 7.5);//实例化
r.Display();//执行打印长、宽、面积
Type type = typeof(Rectangle);//让type对应这个Rectangle类
// 遍历 Rectangle 类的特性
foreach (Object attributes in type.GetCustomAttributes(false))//遍历Rectangle的所有特性
{
DeBugInfo dbi = (DeBugInfo)attributes;//强制转换(拆箱)
//通过 object 声明对象,是用了装箱和取消装箱的概念.
//也就是说 object 可以看成是所有类型的父类。
//因此 object 声明的对象可以转换成任意类型的值。
//通过拆装箱代替强制转换
if(null != dbi)//dbi非空
{
Console.WriteLine("Bug on: {0}", dbi.BugNo);
Console.WriteLine("Developer: {0}", dbi.Developer);
Console.WriteLine("Last REviewed: {0}", dbi.LastReview);
Console.WriteLine("Remarks: {0}", dbi.Message);
}
}
// 遍历方法特性
foreach (MethodInfo m in type.GetMethods())//遍历Rectangle类下的所有方法
{
foreach (Attribute a in m.GetCustomAttributes(true))//遍历每个方法的特性
{
DeBugInfo dbi = a as DeBugInfo;(继承于同一个基类的就可以用as类型转换)
if (null !=dbi)//同理打印
{
Console.WriteLine("BugFixApplication no: {0},for Method: {1}", dbi.BugNo, m.Name);
Console.WriteLine("Developer:{0}", dbi.Developer);
Console.WriteLine("Last Reviewed: {0}", dbi.LastReview);
Console.WriteLine("Remarks: {0}", dbi.Message);
}
}
}
Console.ReadKey();
}
}}
④装箱与拆箱的定义
1、值类型:
值类型是在栈中分配内存,在声明时初始化才能使用,不能为null。 值类型超出作用范围系统自动释放内存 主要由两类组成:结构、枚举(enum),结构分为以下几类①整形(Sbyte、Byte、Char、Short、Ushort、Int、Uint、Long、Ulong)②浮点型(Float、Double)③decimal④bool⑤用户自定义的结构(struct)2、引用类型
引用类型在堆中分配内存,初始化时默认为null。 引用类型是通过垃圾回收机制进行回收。 包括类、接口、委托、 数组以及内置引用类型object与string3、装箱与拆箱的区别:装箱:将值类型转换成引用类型的值拆箱:将引用类型转换成值类型int n = 10;object o = n;//装箱int nn = (int)o;//拆箱4、装箱与拆箱有什么用一种最普通的场景是,调用一个含类型为Object的参数的方法,该Object可支持任意为型,以便通用。当你需要将一个值类型传入时,需要装箱。另一种用法是,一个非泛型的容器,同样是为了保证通用,而将元素类型定义为Object。于是,要将值类型数据加入容器时,需要装箱。
2、ScriptableObject基本使用
生成特性:[CreateAssetMenu(fileName = "GameObject",menuName = "GameConfig/设置"]
用处:用来在编辑器模式下保存和存储数据到本地Asset下的,数据保存以后是可以共享的,就像纹理、shader等资源一样,是可以共享于当前整个工程和其他工程的;这个ScriptableObject在真机上不可以修改,就像我们不可以在游戏运行时修改一个shader资源的代码、不可以修改一个纹理资源的像素内容一样,而在UnityEditor里可以修改ScriptableObject是因为Unity的编辑器对它格式的支持,就像使用vs code修改shader和使用ps修改一张纹理一样。
3、Odin插件
①可以通过特性的方式来修改Inspector的界面展现形式,与ScriptableObject结合起来使用效果会更好。
②由于Unity本身的Editor编程中,存在其自身的局限性,比如:无法序列化Dictionary,对多态、空值、循环嵌套等序列化显示,Inspector或EditorWindow多线程下复杂属性显示处理等等;
③通过使用Odin中给予的Attributes快速影响所需要编辑的字段,帮助开发者快速开发可视化界面及工具。
④使用Odin插件需要继承SerializedScriptableObject,引用
using Sirenix.OdinInspector;
已下载文件存储地址:D:\研究生学习工作类\项目及时间规划\开发框架\资源
4、Abstract
①在类声明中使用 abstract 修饰符
以指示某个类只能是其他类的基类。
②标记为抽象或包含在抽象类中的成员必须通过从抽象类派生的类来实现。
5、Unity菜单创建
通过MenuItem特性创建,提供快捷工具及说明性文档。
二、框架结构
-
Base(通用基本)
-
Extension(拓展)
-
Sington(单例)
-
Pool(对象池)
-
System(系统)
-
Config(配置)
-
Event(事件)
-
Save(存档)
-
Localzation(本地化)
-
Scene(场景)
-
Server(服务)
-
Res(资源)
-
Audio(音效)
-
Mono(管理器挂载总服务)
-
Component(组件)
-
EventListener(事件监听)
-
StateMachine(状态机)
-
UI(界面 )
[Base(通用基本)]
1、Extension(拓展)
①拓展方法的三个要求:
-
声明扩展方法的类必须声明为 static;
-
扩展方法本身必须声明为static;
-
扩展方法第一个参数类型前一定要包含关键字 this。
注意:需要特别说明的是,扩展方法可以拥有多个参数,但第一个参数的位置始终是属于为扩展对象的,不能改变。也就是说只有第一个参数可以并且必须用this关键字修饰,其他的参数视为方法的普通参数。
②拓展方法一般定义为通用方法,方便开发过程中直接使用。
-
资源管理:对象池的push操作
-
通用拓展:数组对比、获取特性
-
本地化拓展:从本地化系统中获取修改的内容(文本、图片、音效、视频)
-
Mono:添加、移除Update、LateUpdate、FixedUpdate监听、开启、关闭协程
static关键字的作用:static可修饰类、字段、属性、方法。static在使用的时候分三种方法。1.修饰类的时候:将类修饰为静态的,意味着这个类不能实例化对象,在调用类里面的任何内容的时候直接用类名加点的形式调用其中的方法或者字段(类名.方法/类名.字段)2.修饰方法或者属性的时候:将方法修饰为静态方法,表示此方法为所在的类所有,而不是这个类实例化的对象所拥有,这个方法在调用的时候需要类名加点(类名.方法)来调用。3.修饰变量:表示在每次调用该变量的方法或者类的时候,变量的值为最后一次赋值时的值,而不是再次初始化它的值。
2、单例模式
where T : class , new();
代表着是对通用参数T的约束,它必须是class类型(引用类型),且它必须具备公共参数的默认构造函数。
①继承于Mono的实例单例(挂载在脚本上)【程序开启时自动创建】
进阶:当忘记挂载在脚本上时自动创建挂载,并且切换场景时不销毁。
②普通C#类的实例单例(泛型单例,作为单例基类使用)【使用时自动创建】
不挂载到场景当中,不需要继承MonoBehaviour,减少内存消耗
优点:代码相对简单,只需要继承Singleton<T>类就可以实现单例类。
缺点:因为泛型约束中填写了new(),因此子类无法私有化构造函数,子类依然可以通过new来实例化对象。
③管理器基类单例
需要手动进行初始化单例,并不是在创建时就生成。
④逻辑管理器基类(单例)
继承此基类,不仅可以实现自动单例,同时也必须实现事件的监听以及取消方法,通过OnEnable以及OnDisable方法来实现挂载对象的监听事件以及取消事件的执行。
3、对象池(避免频繁实例化和销毁)
①游戏物体和非游戏物体:
Unity开发中,我们有两种类型的实例,一个是GameObject,一种是普通的C#类,对象池中也要兼容这两种不同的类型。
-
对象池添加特性,可根据此特性来配置该对象池的相关个性化参数。
-
利用list性能会消耗大一点,所以尽量使用Queue。
②对象池管理器具备通用功能:
-
从对象池中获取游戏对象或者C#普通对象
-
先从对象池中取,若无,则生成一个游戏对象/C#普通对象。
-
游戏对象的获取通过预制体、路径获取,C#普通对象通过泛型名称获取
-
将游戏对象或者C#普通对象加入对象池当中
-
将游戏对象或者C#普通对象从对象池当中批量/全部删除
③游戏对象池有根节点,挂载在场景对象当中
C#普通对象无根节点,放在队列当中,需要进行封箱、拆箱操作。
④游戏对象生成时放置于根节点的对应游戏对象名称当中,对象全部隐藏状态,取出用时将其放置其他位置当中。
父物体为Null后GameRoot场景属于DontDestroy场景中,而不是默认场景,所以需要回归默认场景。
4、游戏根节点
①根节点使用挂载脚本的单例模式,管理器使用自定义的单例模式(需主动开启单例),错开单例生成顺序。
②初始化生成所有管理器单例
③
[InitializeOnLoadMethod]特性:在Editor模式下刷新时自动运行一次,一般用于当需要确保某一个核心对象处于正确位置时使用
-
设置GameRoot为游戏根节点,手动设为单例模式
-
清空事件
-
初始化全部管理器,手动设为单例模式
-
将带有Pool特性的类型加入缓存池字典
-
将UI元素类型加入缓存
[InitializeOnLoad]特性:在Editor文件夹下,添加了InitializeOnLoad特性后,需要添加static关键字,其构造方法会自动执行,测试结果是,每次修改这个类的内容,就会重新执行一遍构造方法。可以在构造方法中执行一些操作,来控制Editor模式下的代码执行。[RuntimeInitializeOnLoad],Editor运行时,可以增加参数, 执行顺序BeforeRuntime、Awake、OnRuntime、AfterRuntime、Start、Update。
【System(系统)】
1、配置系统( 开发时各种配置的缓存、游戏中诸如角色配置、装备配置等)
①
此方式可以实现几个功能:
1、可以通过面板去设置此对象
2、外界可以获取此对象,但不能在代码层面去修改它
如果实现方法是:public GamSetting gameSetting;
则可通过面板去设置此对象,但外界可以通过代码去修改,重要文件不建议这样实现。
②当多个脚本需要继承同一个类的时候,而且这些脚本属于同一系列,使用同一个基类的方式来统一管理,方便区分。
③配置分为非框架层面的全局配置(包含所有的配置)以及框架层面的游戏配置(如对象池配置、UI层级管理)
④通过继承同一个基类来实现统一通用配置,所有继承于ConfigBase的子类配置都可以存储进去,后续取出时进行类型转换。
⑤Unity编译前运行(Unity编译之前可以执行我们指定的函数)
-
有些配置需要在编译前自动计算,就可以通过这种方式来实现,但会影响编译效率,微乎其微可以忽略。
-
可通过两种方式实现编译前执行函数:
-
通过Odin插件的特性按钮的方式实现
-
通过GameRoot中的 [InitializeOnLoadMethod]特性实现
⑥特性遍历搜寻:
-
获取所有程序集,遍历所有程序集
-
获取程序集下的所有类型,遍历所有类型
-
获取Pool特性,若该Pool特性存在,则缓存该类型
⑦通过特性设置UI窗口元数据,并将其存储至缓存字典当中
-
获取所有程序集,遍历所有程序集
-
获取程序集下的所有类型,遍历所有类型
-
获取UIElementAttribute特性,若该特性不为空,则获取特性中的元数据,并将其中的数据存储至对应此类型的字典当中
2、事件工具管理系统(解耦 )
自定义单个事件工具(EventManager)
①通过内部接口来实现内部类
通过类型转换来转变成对应的事件类
②事件框架逻辑:(利用字典存储不同的事件类型)【同时也是拓展类及函数】
-
添加事件监听(添加Action)
-
触发事件
-
取消事件监听(移除Action)
-
移除事件
一类事件工具(鼠标、碰撞、触发等事件)【常用类型,根据类型来处理】(EventListener)
(
统一 触发/碰撞/鼠标点击、进入、拖拽等等一系列游戏事件)
①事件种类包括:
-
某个事件中一个时间的数据包装类
-
一类事件的数据包装类型
②通过继承IEqualityComparer<T>接口来实现复杂类型对象的比较,实现去重的作用。
.NET处理时先比较两个对象GetHashCode方法返回的哈希值是否相等,如果不相等则直接返回,不再执行Equals方法,如果哈希值相等则执行Equals方法继续比较!
为什么要用两个方法去对比呢?原因是GetHashCode方法比Equals方法效率更高,所以先执行GetHashCode方法。
③继承鼠标、碰撞、触发事件接口并实现
③模块细化事件工具系统(通过拓展方式实现),简化一类事件工具操作(EventListenerExtend)【高级操作,需慢慢啃】
3、存档管理系统
(
本地序列化存档,支持多存档机制、自动更新存档时间、存档切换
)
①存档的数据类需要允许被序列化,加上特性[Serializable]
②List集合的排序
方法一:增加比较器
方法二:使用List的拓展方法OrderByDescending与OrderBy
③BinaryFormatter:以二进制格式序列化和反序列化对象或连接对象的整个图形。
MemoryStream位于System.IO命名空间,为系统内存提供流式的读写操作。常作为其他流数据交换时的中间对象操作。
保存文件:
加载文件:
④存档管理框架:
-
存档管理数据(当前ID、存档列表(ID、上次保存时间))
-
存档对象的缓存字典<存档ID、<文件名称、实际的对象>>
-
保存、读取存档文件(扩展方法)
【文件保存先保存到文件夹当中、更新存档时间同步更新保存管理数据、同步保存进入缓存当中】
【文件读取先从缓存中读取、若缓存中没有则去文件夹中读取,读取完毕后重新设置缓存】
-
存档文件夹架构
一级:saveData、setting二级:saveData/SaveManagerData、ID、setting/具体内容的名称三级:saveDate/ID(具体)/具体内容的名称、setting/具体内容的名称/具体内容四级:saveDate/ID(具体)/具体内容的名称/具体内容
4、本地化系统
(
支持多语言文字、图片切换
)
①通过内部接口实现不同类型之间的切换,不同语言之间的切换
②通过事件触发方式来切换语言类型,在此之前需要监听具体的语言事件
③本地化数据设置存储方式
5、场景管理系统
(
场景切换、异步加载管理等
)
①两种方式进行场景管理:同步加载场景与异步加载场景
1、同步加载场景:利用Unity内置的场景管理器进行同步加载场景
2、异步加载场景:利用协程实现场景切换,使用Unity内置的场景管理器进行异步加载场景
同步与异步的区别:
同步与异步的区别(一看则懂)_同步和异步的区别-CSDN博客