前言
当游戏开发使用传统的OPP编程,面对大量的OBJ时FPS会显著降低,而使用Dots技术可以极大优化性能
3种编程范式:
面向过程(Procedural Programming):程序按照指定的顺序逐步执行,每一步都依赖于上一步的结果
面向对象(Object-Oriented Programming,OPP):将程序分解为多个对象,每个对象负责特定的功能
面向数据(Data-Oriented Programming,DOP):数据被组织成组件,并存储在ECS框架中,以便高效地访问和处理
计算机内存基础
内存用来存储数据,CPU可以从内存读写数据,但是寻址速度较慢,同时,CPU处理数据的速度非常快,这就造成了CPU 大部分时间都在等待内存的数据读取操作完成,导致 CPU 的性能无法充分发挥,因此创建了高速缓存,可以快速寻址
CPU自身有三级高速缓存L1,L2,L3,CPU可以从缓存快速读取数据,其中CPU访问(L1)缓存最快,容量最小,第三级(L3)缓存最慢,容量最大。
CPU会先从一,二,三级缓存中取得数据,如果数据不在三级缓存,就需要寻址内存中的数据
Dots(Data Oriented Tech Stack) 面向数据的技术堆栈
Dots合集包含4个部分(各自都是独立的系统可以单独使用):
- Entity Component System(ECS) :实体组件系统架构(编写方便高效寻址的代码)
- Burst Compiler :Burst编译器 (编译生成高性能代码)
- Job System :任务系统(编写多线程代码)
- masmatic:数学库
ECS
ECS即实体(Entity),组件(Component),系统(System):
其实就是将对象拆分为3部分:
- Entities:一个Id索引(标识符)
- Components:结构体,数据集合,
- Systems:改变数据的静态方法,一个实体往往会被很多系统影响,例如一个方块边跑边转可能需要MoveSystem和RotateSystem
为什么ECS会加快性能?
将相同组件整齐排列在内存中,遍历相同组件时并不需要一个个从内存中读取,可以指定一个长度一次性全读进来放入缓存中,
优化了内存布局,加快寻址速度,提高CPU高速缓存利用率
理解ECS
示例:
OPP:
假设我们有3个脚本,
PlayerMovementAndLook.cs 负责角色
- 相机,速度,生命值、动画,刚体的属性
- 移动,死亡,减血的函数
EnemyBehaviour.cs负责敌人
- 速度、生命值、刚体的属性
- 移动,死亡,减血的函数
ProjectileBehaviour.cs负责导弹
- 速度、生命值,刚体的属性
- 移动的函数
3个脚本中属性以及函数、都有部分重复了,显然寻址会慢
DOP_ECS:
创建3个Entity,添加需要的组件
创建component(速度,生命值等),不用重复写
创建system(移动,减血等),不用重复写
ECS_(Chunk)块
内存块(Chunk):底层把这些拥有相同组件的实体放在一块内存里
ECS_(Archetype)原型
块的大小是有上限的,当某个块里的实体数量达到某个程度时,就不能再增加了,那么就创建新的内存块
存放相同组件的实体的所有块,都属于同一个类型,也就是原型
Burst Complier
Burst是使用LLVM从IL/.NET字节码转换为高度优化的本机代码的编译器。
JobSystem
多线程并行处理代码,unity提供的多线程使得开发者可以不用写加解锁的代码,并且保证数据不会被竞争改写
masmatic
一个C#数学库提供矢量类型和数学函数。
DOTSpath配置
unity版本
要使用 Entities 包,必须安装 Unity 版本 2022.3.0f1 及更高版本
uintyHub无法登录?
在官网下载hub到原有路径,即可更新hub版本,登录即可
渲染管线:
内置渲染管线(Built-in Render Pipeline)不是可编程渲染管线 (CPU(data)->GPU演示的过程)
URP (Universal Render Pipeline)通用渲染管线,可编程
(High Definition Render Pipeline)(HDRP)高清渲染管线。可编程
配置path
- 通过window->packageManager->'+'加号->install package by name->install搜
工具包:包括ECS,Burst,Job System,masmatic- com.unity.entities.graphics渲染entity
- com.unity.physics:ECS物理
- 注意所有包的版本要一致,如果无法安装指定版本通过:
- 工程文件Packages/manifest.json中手动修正版本(修改为1.0.16不知道高版本为什么有错误),重启引擎
unity设置
Edit > Project Settings > Editor菜单,然后启用 Enter Play Mode Options 设置,但禁用 Reload Domain 和 Reload Scene
ECS实例
容纳10万人运动物体的场景,每帧都会更新所有物体的位置
实体
新建空物体,新脚本,
using Unity.Entities;
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
entityManager.CreateEntity();
Window > Entities > Hierarchy
运行即可看见entity
组件
新的脚本继承IComponentData
修改CreateEntity,添加(typeof(……Component),typeof()……)零个或多个组件
运行点击调试面板的entity ,即可看到挂载的组件
设置组件数据
entityManager.SetComponentData(实体,new 组件{数据 = ……;};
运行可看到数据设置的值
系统
继承SystemBase
在OnUpdate中:Entities.ForEach((……Component ……){修改数据}
运行可看到数据更改
实体原型
可以简单的添加第二个组件,但是更好的管理方法是创建原型
EntityArchetype entityArchetype = entityManager.CreateArchetype(……组件);
创建大量实体
创建NativeArray<Entity>,为每个元素填充实体原型,通过for更新数组中所有实体数据
c#Attribute特性
反射:
如果让你在不修改源代码的情况下,通过一个函数(参数为类类型),运行时获取所有参数为空的公有成员方法,这是不可能的
但是如果可以修改类呢?
- 可以让每一个类继承Object基类,每个类都手写一个FuncData的表(所有MetaData元数据(FuncData,PropData)数据表)
- 每个类都重写基类的public FuncData[] getFuncData();方法
这样就可以运行时获取
Attribute:
比如我们通过反射可以快速查找一个类是否仅包含数据
那如果我想要对特定类不同控制?
Attribute是反射的扩充,目的是在不破坏原有代码的 情况下,在代码的元数据上附加一些信息
[……]语法:
实际是调用一个继承Attribute类的特性(自定义/内置)的构造
Attribute:
在运行时动态地获取和操作类和对象的信息(添加元数据),提供了一种声明性方式[……]来指定代码的行为或其他相关信息,可以被应用于程序集、类、方法、属性、事件等
运行时:编译期间无法确定的,接收非特定对象,结果会根据具体对象而不同
创建一个custom自定义特性Attribute:
继承自 System.Attribute
类,
其中AttributeUsage
内置特性,用于指定一个自定义特性可以应用于哪些代码元素上可以控制特性的有效性目标(如类、方法、属性等)以及是否允许特性被多次应用于同一个目标。
AttributeTargets
:指定特性可以应用于哪些目标(如类、方法、属性等)。Inherited
:指定特性是否可以被派生类继承。AllowMultiple
:指定是否允许在同一目标上多次应用该特性。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public class MyCustomAttribute : Attribute
{
public string Description { get; }
public MyCustomAttribute(string description)
{
Description = description;
}
}
使用:通常通过方括号 []
语法使用特性:
[MyCustomAttribute("This is a class description")]
public class MyClass
{
[MyCustomAttribute("This is a method description")]
public void MyMethod()
{
// Method implementation
}
}
在运行时,可以使用反射(attribute基类)来访问特性并读取其值:
通过Attribute.GetCustomAttribute获取到了,指定类关联的特性类,
var type = typeof(MyClass);
// 获取类上的MyCustomAttribute
var classAttribute = (MyCustomAttribute)Attribute.GetCustomAttribute(type, typeof(MyCustomAttribute));
Console.WriteLine(classAttribute.Description);
// 获取方法上的MyCustomAttribute
var method = type.GetMethod("MyMethod");
var methodAttribute = (MyCustomAttribute)Attribute.GetCustomAttribute(method, typeof(MyCustomAttribute));
Console.WriteLine(methodAttribute.Description);
常见的 C# 内置特性:
Obsolete
:标记一个程序元素为过时。Serializable
:指示一个类可以被序列化。DllImport
:用于声明非托管代码的外部方法。- Conditional:允许根据编译符号的存在与否来控制方法的调用。
C#常见内置特性功能:
- 查询类的信息:获取类的名称、基类、实现的接口、包含的字段、属性、方法等。
- 创建对象:即使不知道类的具体类型,也可以通过反射在运行时创建该类的对象实例。
- 调用方法:根据方法名和参数类型,在运行时调用对象的方法。
- 访问字段和属性:读取或修改对象的字段和属性值,即使这些字段和属性在编译时是未知的。
Object Pooling对象池
用于减少对象Instantiate()创建和destroy()销毁的性能开销,减少创建销毁api的调用
维护一个list<>
创建-》如果pool中没有obj,那么就instantiate,如果有就从池内取出对象
销毁—》使用完成放回池内
组合设计模式
- MVC(Model-View-Controller)
- MVP(Model-View-Presenter)
- MVVM(Model-View-ViewModel)
实现低耦合