-
2019-07-15 08:03:45
首先,作业调度Quartz的定义是:
Quartz.NET是一个开源的作业调度框架,是OpenSymphony 的 Quartz API的.NET移植,它用C#写成,可用于winform和asp.net应用中。它提供了巨大的灵活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。它有很多特征,如:数据库支持,集群,插件,支持cron-like表达式等等 。
简单来说,Quartz就是一个进阶版的计时器,它可以帮助我们定时执行一些功能,或者在指定时间执行一些功能。
下面介绍Quartz的最基本使用:
第一步、引入Quartz包
在当前.net 项目,点击引用->右击->点击‘管理NuGet程序包’->搜索Quartz->选择对应版本安装
第二步、实现IJob接口和Execute方法
Quartz作业调度就是按照规定的时间契约来执行Execute方法
例如:
class dd : IJob
{
public void Execute(IJobExecutionContext context)
{
Console.Write("11");
}
}
第三步、获取调度器
第四步、创建作业
第五步、创建触发器
第六步、把作业,触发器加入调度器
第七步、启动调度器
为了方便阅读,把上面代码写到一起,如下:
//从工厂中获取一个调度器实例化
IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();
//创建一个作业 -WithIdentity的参数(作业名称,做业组)
IJobDetail job1 = JobBuilder.Create<dd1>().WithIdentity("name", "group").Build();
// Trigger 作为作业的定时管理工具,一个 Trigger 只能对应一个作业实例,而一个作业实例可对应多个Trigger ;
ITrigger trigger1 = TriggerBuilder.Create()
.WithIdentity("mytrigger", "group1")
.StartNow()
.WithCronSchedule("0/5 * * ? * *").Build();
//把作业,触发器加入调度器。
scheduler.ScheduleJob(job1, trigger1);
//开启调度器
scheduler.Start();
//WithCronSchedule("") 拥有强大的Cron时间表达式,正常情况下WithSimpleSchedule(x) 已经满足大部分对日期设置的要求了。
//WithCronSchedule的参数是 Cron时间表达式
// Cron时间表达式 由7段构成:秒 分 时 日 月 星期 年(可选)
//"-" :表示范围 MON-WED表示星期一到星期三
//"," :表示列举 MON, WEB表示星期一和星期三
//"*" :表是“每”,每月,每天,每周,每年等
//"/" :表示增量:0 / 15(处于分钟段里面) 每15分钟,在0分以后开始,3 / 20 每20分钟,从3分钟以后开始
//"?" :只能出现在日,星期段里面,表示不指定具体的值
//"L" :只能出现在日,星期段里面,是Last的缩写,一个月的最后一天,一个星期的最后一天(星期六)
//"W" :表示工作日,距离给定值最近的工作日
//"#" :表示一个月的第几个星期几,例如:"6#3"表示每个月的第三个星期五(1 = SUN...6 = FRI,7 = SAT)
执行代码结果如下:
更多相关内容 -
C#作业笔记
2018-01-16 18:20:16大学C#作业答案笔记,包括if语句的使用,基本计算,计算三角形面积等等。 -
重点C#作业
2017-02-08 23:32:10重点C#作业 -
ImageProcessingGrayscale:测试C#作业
2021-04-11 22:18:55图像处理灰度测试C#作业编写一个C#程序作为控制台应用程序,该程序将两个ELI格式的图像文件(大小相同)的名称用作命令行参数,处理这些图像,并将结果保存到ELI格式的输出图像文件中。 作为图像处理,必须将第一... -
BurstWig:C#作业系统和Burst编译器的“假发”效果
2021-02-06 07:12:27现在,Unity具有C#作业系统和Burst编译器,它们可以非常高效地处理顶点动画。 最新版本的VFX Graph支持粒子条带,方便绘制发束。 因此,现在效果分为两个部分: 动态(CPU):使用C#作业系统运行头发仿真并将... -
C#班级作业提交系统源码.zip
2020-12-14 10:52:15一、源码描述 1、班级学生每次交作业都是QQ或者微信发给学委、班长、老师等,非常麻烦。校园都有校园网,把此项目部署到本地电脑IIS然后... 1、开发工具:VS2012,开发语言:C# 2、数据库:无数据库,框架版本:V4.5 -
C#作业:学生信息管理系统
2021-03-16 00:17:56北京航空学院大学生C#作业:学生信息管理系统的源代码,带数据库文件,格式是access,本程序不能在XP下运行,需要在64位电脑上运行,代码完整,可重新编译。程序带登录和对学生信息的添加、修改删除等操作,是一个供... -
Excel:C#作业
2021-04-01 00:13:53Excel:C#作业 -
Voxelman:Unity ECS + C#作业系统示例
2021-02-06 10:55:00它还利用C#作业系统,突发编译器和异步射线广播来达到多核处理器的最大效率。 系统要求 Unity 2018.2或更高版本 如何玩这个项目 子模块 该存储库使用来管理UPM(Unity软件包管理器)软件包。 如果您了解Git以及... -
北大青鸟c#作业
2014-05-28 17:05:44这是一个关于c#的练习!如有需要,就看看吧 -
VCubWatcher:C#作业
2021-03-18 20:43:51VCubWatcher:C#作业 -
C#作业
2015-04-11 09:14:01在老师布置作业的时候就想到要用数组的一些知识: (1) Array(包含AraayList,Hashtable等一些特殊的数组)提供了Sort方法来进行排序,但它常与Reverse方法(反转数组中元素的顺序)一起配合使用。 Sort方法,...从n个数中随机选取m(m<=n)个不重复的数并且对选取的m个数进行排序
在老师布置作业的时候就想到要用数组的一些知识:
(1) Array(包含AraayList,Hashtable等一些特殊的数组)提供了Sort方法来进行排序,但它常与Reverse方法(反转数组中元素的顺序)一起配合使用。
Sort方法,接受一个数组,将其实现升序,格式为:Array.Sort(数组)
Reverse方法,接受一个数组,将其反转数组中元素的顺序,格式为:Array.Reverse(数组)
(2)ArrayList(动态数组)提供了两种方法用于向ArrayList添加元素,即Add和AddRange。
Add方法将单个元素添加到列表的尾部,其格式为:ArrayList 对象.Add(要添加的值)
AddRange方法获取一个实现ICollection接口的集合实例,并将这个集合实例按顺序添加到列表的尾部,其格式为:ArrayList 对象.AddRange(要添加的数组)
(3)Array提供的数组元素的遍历,最好使用foreach语句
(4)还需要用到一个新的知识点就是C#Random()随机函数,Random.Next() 返回非负随机数。
随机数的使用很普遍,可用它随机显示图片,用它防止无聊的人在论坛灌水还可以用来加密信息等等。本文讨论如何在一段数字区间内随机生成若干个互不相同的随机数,比如在从1到20间随机生成6个互不相同的整数,并通过此文介绍Visual c#中随机数的用法。.net.Frameword中提供了一个专门产生随机数的类System.Random,此类默认情况下已被导入,编程过程中可以直接使用。我们知道,计算机并不能产生完全随机的数字,它生成的数字被称为伪随机数,它是以相同的概率从一组有限的数字中选取的,所选的数字并不具有完全的随机性,但就实用而言,其随机程度已经足够了。
第一次尝试代码
using System; using System.Collections;//使用ArrayList类需要引入的命名空间 using System.Linq; using System.Text; using System.Threading.Tasks; namespace Text { class Program { static void Main(string[] args) { ArrayList al = new ArrayList();//创建一个ArrayList类的对象al Random rm = new Random();//创建一个Random类的对象rm for (int i = 0; i < 10; i++) { int sjs = rm.Next(1,50);//调用Random类的随机选取非负数方法 al.Add(sjs);//调用ArrayList类的元素末尾添加方法 } al.Sort();//数组元素的升序排序 foreach (int temp in al) { Console.WriteLine(temp); } Console.ReadLine(); } } }
输出的结果为:
从上面输出的结果来看输出的10个数还是有重复的,虽然有时候输出的是不重复的,但是如果一定要输出不重复的10个随机数就要考虑怎么去除重复的数并进行再次的选取。
第二次代码尝试
using System; using System.Collections;//使用ArrayList类需要引入的命名空间 using System.Linq; using System.Text; using System.Threading.Tasks; namespace Text { class Program { static void Main(string[] args) { ArrayList al = new ArrayList(); Random rm = new Random(); int i = 0; while (i < 10) { int sjs = rm.Next(1, 50); bool IsExist = false;//定义一个布尔型变量并且赋值为false foreach (int item in al)//判断选取的随机数是否相同 { if (sjs == item) { IsExist = true;//如果相同就终止添加到选取数组的末尾 break; } else { IsExist = false;//如果不相同就添加到选取数组中的末尾 } } if (IsExist) { continue; } else { al.Add(sjs); i++; } } al.Sort(); foreach (int temp in al) { Console.WriteLine(temp); } Console.ReadLine(); } } }
输出的结果为:
再一次代码输出运行了几次终于是没有重复的了,记得老师还讲了两种方法,有时间了再进行补充。
-
C#作业参考答案.docx
2021-10-10 20:13:17C#作业参考答案.docx -
C#作业(小程序代码)
2018-12-18 20:32:02c#小作业,基础作业,可以直接运行,特备简单,喜欢的朋友可以看看,其实没啥用 -
c#作业 任务12.sln
2019-12-03 18:03:01C# 课程作业 无技术含量 仅是作业 -
C#Winform大作业合集
2021-02-23 20:05:43C#Winform大作业合集:住宿管理系统,游戏商人系统,音乐系统,学生管理系统需求,学生管理系统,学生个人信息管理,学生出入信息管理系统,学生成绩管理系统,宿舍入住系统,图书管理系统1,图书管理系统,图书馆... -
FrontEnd-buildschool:C#作业及其他档案
2021-03-21 23:38:17建校 C#作业及其他档案 -
Unity* 实体组件系统 (ECS)、C# 作业系统和突发编译器入门
2018-08-15 19:44:48使用 C# 作业系统显示作业移动实施的示例代码 我们的新 MovementJob 脚本是一个实现 IJob 接口变体之一的结构。这种自包含结构定义了任务或“作业”,以及完成该任务所需的数据。我们将使用 作业系统 安排这种...作者:Cristiano Ferreira (@cristianohh) 和 Mike Geig (@mikegeig)
低、中、高。GPU 设置的标准费用,但为何不使用 CPU 设置呢?如今,最终用户设备的 CPU 潜能可能会大不相同。通常情况下,开发人员将定义 CPU 最小规格,并使用该性能目标来实施仿真和游戏系统。这将使现代主流 CPU 中内置的许多潜在可用内核和功能处于闲置状态。Unity* 中的全新 C# 作业系统和实体组件系统不仅可以让您轻松利用以前未使用的 CPU 资源,还可以帮助您更高效地运行所有游戏代码。然后,您可以使用这些额外的 CPU 资源来添加更多场景动态和沉浸感。在本文中,您将了解如何快速开始学习这些新功能。
Unity 用于解决游戏引擎计算中两个重要的性能问题。它解决的第一个问题是数据布局效率低下。Unity 的实体组件系统 (ECS) 可改进数据存储的管理,在这些结构上实现高性能运算。第二个问题是缺少高性能作业语言和可以在有序数据上运行的 SIMD 矢量化。Unity 的全新 C# 作业系统、实体组件系统和突发编译器技术不具备这些缺点。
Unity 实体组件系统和 C# 作业系统是两个不同的系统,但它们密不可分。若要了解这两个系统,我们来看看用于在场景中创建对象的当前 Unity 工作流程,然后加以区分。
在当前的 Unity 工作流程中,您:
- 创建 GameObject。
- 向游戏对象添加组件,为您的对象提供所需属性:
- 渲染
- 冲突
- 刚体物理
- 创建 MonoBehaviour 脚本并将其添加到对象中,以便在运行时控制和更改这些组件的状态。
我们将其称之为 Classic Unity 工作流程。这种做法有一些固有的缺点和性能考虑因素。首先,数据和处理是紧密耦合的。这意味着代码重用的发生频率较低,因为处理与一组特定数据相关联。除此之外,典型系统非常依赖于引用类型。
在下面所示的 Classic GameObject 和 Components 示例中,GameObject 依赖于 Transform、Renderer、Rigidbody 和 Collider 引用。在这些性能关键型脚本中引用的对象分散在堆内存中。因此,数据不会转换成可由更快的 SIMD 矢量单元进行操作的形式。
图 1.典型 gameobject 和组件列表。通过缓存预取提高速度
从系统内存访问数据比从附近缓存中提取数据要慢得多。这就是预取发挥作用的地方。高速缓存预取是指计算机硬件预测接下来要访问哪些数据,然后抢先将其从速度较慢的最初内存提取到更快的内存中,以便在需要时进行加热和准备。使用此功能,硬件可以实现预测计算性能的提升。如果要迭代阵列,硬件预取单元可以学习将大量数据从系统内存拉入缓存中。当处理器在阵列的下一部分上运行时,必要的数据就位于缓存中并准备就绪。对于紧密打包的连续数据,就像在阵列中一样,硬件预取器很容易预测并获得正确的对象。当许多不同的游戏对象稀疏地分配在堆内存中时,预取器无法完成它的任务,迫使它获取无用的数据。
图 2.游戏对象、其行为及其组件之间分散的内存引用。上图显示了这种数据存储方法的随机偶发性质。通过上面显示的场景,每个单引用(箭头) - 即使作为成员变量缓存 - 都可能从系统内存中全部拉出。Classic Unity GameObject 场景可以让您的游戏在非常短的时间内完成原型构建并运行,但它对于性能关键型模拟和游戏来说不太理想。为了深化这个问题,每个引用类型都包含可能不需要访问的许多额外数据。这些未使用的成员也占用了处理器缓存中的宝贵空间。如果只需要选择现有组件的少量成员变量,则可以将其余部分视为浪费空间,如下面的“浪费空间”图所示:
图 3.粗体项目表示实际用于移动操作的成员;其余就是浪费空间若要移动您的 GameObject,脚本需要从 Transform 组件访问位置和旋转数据成员。当您的硬件从内存中获取数据时,缓存行中会填充许多可能无用的数据。如果您只是为所有应该移动的GameObjects 设置一个只有位置和旋转成员的阵列,那不是很好吗?这将使您能够在很短的时间内执行通用操作。
进入实体组件系统
Unity 的新实体组件系统可帮助消除低效的对象引用。我们来考虑一下只包含它所需数据的实体,而不考虑带自己组件集合的GameObjects 。
在下面带有作业图的实体组件系统中,请注意 Bullet 实体没有附加Transform 或 Rigidbody 组件。Bullet 实体只是显式运行您的更新例程所需的原始数据。借助这个新系统,您可以将处理与各个对象类型完全分离。
图 4.具有作业图的实体组件系统当然,从中获益的不仅仅是运动系统。许多游戏中的另一个常见组件是在各种敌人和盟友中建立的更复杂的卫生系统。对于不同的对象类型,这些系统通常几乎没有差异,所以它们是利用新系统的另一个绝佳备选。实体是一个句柄,用于索引表示它的不同数据类型的集合(ComponentDataGroups 的原型)。系统可以在没有程序员帮助的情况下,使用所需数据对所有组件进行过滤和操作;稍后我们将介绍更多信息。数据全部以紧凑的连续阵列高效组织,并在后台过滤,而无需将系统与实体类型明确结合。这个系统具有很大优势。它不仅可以提高缓存效率,缩短访问时间;它还支持在需要使用这种数据对齐方式的现代 CPU 中采用先进技术(自动矢量化/SIMD)这可为您提供游戏所需的默认性能。您可以提高每帧效率,或在更短时间内完成相同任务。您还将从即将发布的突发编译器特性中免费获得巨大的性能提升。
图 5.请注意缓存行存储中的碎片和典型系统生成的空间浪费。数据比较见下图。
图 6.将与单个移动操作相关的内存空间与实现相同目标的两个操作进行对比。突发编译器
突发编译器是实体组件系统更高效地组织数据所产生的后台性能增益。从本质上讲,突发编译器将根据玩家设备上的处理器功能优化代码操作。例如,您可以通过填充未使用的寄存器来执行 16、32 或 64,而不是一次只进行 1 次浮点运算。新的编译器技术用于 Unity 的新数学命名空间和 C# 作业系统中的代码(如下所述),基于系统知道数据已经通过实体组件系统正确设置的事实。英特尔 CPU 的当前版本支持英特尔® SIMD 流指令扩展 4(英特尔® SSE4)、英特尔® 高级矢量扩展指令集 2(英特尔® AVX2)以及用于浮点和整数的英特尔® 高级矢量扩展指令集 512(英特尔® AVX-512)。该系统还支持在每种方法中使用不同的精确度,以过渡方式应用。例如,如果您在低精度的顶级方法内使用余弦函数,则整个方法也将使用余弦的低精度版本。该系统还根据当前运行游戏的处理器的功能支持,通过动态选择适当的优化功能为 AOT(前期)编译做准备。这种编译方法的另一个优势是确保游戏的未来适用性。如果一款全新的处理器产品线上市,其中包含一些令人惊叹的新功能,Unity 可以在后台为您完成所有费力工作。只需对编译器进行升级,以获取优势。编译器是基于软件包的,无需 Unity 编辑器更新即可升级。由于突发软件包将以自己的节奏进行更新,因此您将能够利用最新的硬件架构改进和功能,而无需等待代码进入下一个编辑器版本。
C# 作业系统
大多数使用多线程代码和通用任务系统的人都知道编写线程安全代码很难。争用情况可能会发生,但非常罕见。如果编程员没有想到这个问题,可能会导致潜在的严重错误。除此之外,上下文切换的成本很高,因此学习如何平衡工作负载以尽可能高效地跨核心运行是很困难的。最后,编写 SIMD 优化代码或 SIMD 内联函数是一种深奥的技能,有时最好留给编译器。新的 Unity C# 作业系统为您解决所有这些难题,以便您可以在现代 CPU 中放心地使用所有可用的内核和 SIMD 矢量化。
图 7.C# 作业系统图。我们来看一下简单的子弹运动系统。大多数游戏程序员都为某种类型的 GameObject 编写了一个管理器,如 Bullet Manager 中所示。通常,这些管理器会汇集一个 GameObjects 列表,并每帧更新场景中所有活动项目符号的位置。这对 C# 作业系统很有用。由于运动可以单独处理,因此非常适合并行化。借助 C# 作业系统,您可以轻松地将此功能拉出,并在不同核心上并行运行不同的数据块。作为开发人员,您不必担心管理这项工作分配;您只需要完全专注于游戏特定代码。您将明白如何轻松地做到这一点。
结合这两个新系统
实体组件系统和 C# 作业系统的结合可以为您提供比其各部分之和更强大的力量。由于实体组件系统以高效、紧凑的方式设置数据,因此作业系统可以拆分数据阵列,以便可以高效地并行操作。此外,您还可以从 缓存局部性和一致性中获得一些主要的性能优势。精简的按需分配和数据排列增加了作业所需的数据在需要之前存储在共享内存中的可能性。布局和作业系统的组合将产生可预测的访问模式,帮助硬件在后台做出明智的决策,从而为您提供出色的性能。
“好的!”您说:“这太棒了,但我如何使用这个新系统?”
为了帮助您熟悉,我们来对比一个非常简单的游戏的代码,它使用以下编程系统:
- 典型系统
- 使用作业的典型系统
- 使用作业的实体组件系统
以下是游戏的运行方式:
- 玩家敲击空格键并在该帧中产生一定数量的船只。
- 生成的每个船只都设置为屏幕边界内的随机 X 坐标。
- 生成的每个船只都有一个移动功能,可将其发送到屏幕底部。
- 一旦超过底部界限,生成的每个船只将重置其位置。
测试配置:
- 在本文中,我们将介绍 Unity 分析器,这是一个非常强大的工具,用于隔离瓶颈和查看工作分配。参见 Unity 文档,了解更多信息!
- 屏幕截图和数据采用英特尔® 酷睿™ i7-8700K 处理器和 NVIDIA GeForce* GTX 1080 显卡。
1.典型系统
典型系统检查每个帧的空格键输入并触发 AddShips() 方法。这种方法在屏幕的左侧和右侧之间找到随机 X/Z 位置,将船的旋转角度设置为指向下方,并在该位置生成船只预制件。
void Update() { if (Input.GetKeyDown("space")) AddShips(enemyShipIncremement); } void AddShips(int amount) { for (int i = 0; i < amount; i++) { float xVal = Random.Range(leftBound, rightBound); float zVal = Random.Range(0f, 10f); Vector3 pos = new Vector3(xVal, 0f, zVal + topBound); Quaternion rot = Quaternion.Euler(0f, 180f, 0f); var obj = Instantiate(enemyShipPrefab, pos, rot) as GameObject; } }
代码示例显示如何使用典型系统添加船只
图 8.典型船只预制件(资料来源:Unity.com 资产商店战舰包)。船只对象生成,其每个组件都在堆内存中创建。附加的移动脚本每帧访问变换组件并更新位置,确保保持在屏幕的底部和顶部边界之间。超级简单!
using UnityEngine; namespace Shooter.Classic { public class Movement : MonoBehaviour { void Update() { Vector3 pos = transform.position; pos += transform.forward * GameManager.GM.enemySpeed * Time.deltaTime; if (pos.z < GameManager.GM.bottomBound) pos.z = GameManager.GM.topBound; transform.position = pos; } } }
代码示例显示移动行为
下图显示了分析器一次在屏幕上跟踪 16,500 个对象。不错,但我们可以做得更好!继续阅读。
图 9.在一些初始化之后,分析器正在 30 FPS 下跟踪屏幕上的 16,500 个对象。
图 10.典型性能可视化。查看 BehaviorUpdate() 方法,您可以看到完成所有发货的行为更新需要 8.67 毫秒。另请注意,这一切都在主线程上进行。
在 C# 作业系统中,该工作将分配到所有可用内核。
2.使用作业的典型系统
using Unity.Jobs; using UnityEngine; using UnityEngine.Jobs; namespace Shooter.JobSystem { [ComputeJobOptimization] public struct MovementJob : IJobParallelForTransform { public float moveSpeed; public float topBound; public float bottomBound; public float deltaTime; public void Execute(int index, TransformAccess transform) { Vector3 pos = transform.position; pos += moveSpeed * deltaTime * (transform.rotation * new Vector3(0f, 0f, 1f)); if (pos.z < bottomBound) pos.z = topBound; transform.position = pos; } } }
使用 C# 作业系统显示作业移动实施的示例代码
我们的新 MovementJob 脚本是一个实现 IJob 接口变体之一的结构。这种自包含结构定义了任务或“作业”,以及完成该任务所需的数据。我们将使用 作业系统安排这种结构。对于每个船只的移动和边界检查计算,您知道您需要 calculations, you know you need the 移动速度、上限、下限和 增量时间 值。该作业没有增量时间的概念,因此必须明确提供数据。新位置的计算逻辑本身与典型系统相同,但是将数据分配回原始变换必须通过 TransformAccess 参数进行更新,因为引用类型(如 Transform)在此处无效。创建工作的基本要求包括实施 IJob 接口变量之一,如上例中的 IJobParallelForTransform 并实施针对您任务的 Execute 方法。创建后,可以将此作业结构传递到 Job Scheduler 中。在此处,系统将完成所有执行和相应处理。
为了了解关于这一任务结构的更多信息,我们来分析一下它使用的界面:IJob | ParallelFor | Transform。IJob 是所有 IJob 变体继承的基本接口。Parallel For Loop 是一种并行模式,它基本上采用典型的单线程进行循环,并根据在不同内核中操作的索引范围将工作主体拆分为块。最后,Transform 关键字表示要实施的 Execute 函数将包含 TransformAccess 参数,用于将移动数据提供给外部 Transform引用。若要将所有这些概念化,考虑在常规 for 循环中迭代的 800 个元素的数组。如果您有一个 8 核系统并且每个内核可以自动完成 100 个实体的工作,将会如何?啊哈!这正是系统要做的。
图 11.使用任务可大幅加速迭代任务。界面名称末尾的 Transform 关键词为我们的 Execute 方法提供了 TransformAccess 参数。现在,只需知道针对每个 Execute 调用,每个船只的个别转换数据都会被传入。现在我们来看看游戏管理器中的 AddShips() 和 Update() 方法,了解每帧如何设置这些数据。
using UnityEngine; using UnityEngine.Jobs; namespace Shooter.JobSystem { public class GameManager : MonoBehaviour { // ... // GameManager classic members // ... TransformAccessArray transforms; MovementJob moveJob; JobHandle moveHandle; // ... // GameManager code // ... } }
代码示例显示了设置和跟踪作业所需的变量
您会立即注意到,您需要跟踪一些新变量:
- TransformAccessArray 是数据容器,它将保存对每个船只 Transform (job-ready TransformAccess) 的修改参考。普通的 Transform 数据类型不是线程安全的,因此这是一个方便的助手类型,用于为GameObjects设置移动相关数据。
- MovementJob 是我们刚刚创建的作业结构的一个实例。我们将使用它在作业系统中配置工作。
- JobHandle 是您的任务的唯一标识符,用于为各种操作(例如验证完成)引用您的任务。当您安排工作时,您将收到任务的句柄。
void Update() { moveHandle.Complete(); if (Input.GetKeyDown("space")) AddShips(enemyShipIncremement); moveJob = new MovementJob() { moveSpeed = enemySpeed, topBound = topBound, bottomBound = bottomBound, deltaTime = Time.deltaTime }; moveHandle = moveJob.Schedule(transforms); JobHandle.ScheduleBatchedJobs(); } void AddShips(int amount) { moveHandle.Complete(); transforms.capacity = transforms.length + amount; for (int i = 0; i < amount; i++) { float xVal = Random.Range(leftBound, rightBound); float zVal = Random.Range(0f, 10f); Vector3 pos = new Vector3(xVal, 0f, zVal + topBound); Quaternion rot = Quaternion.Euler(0f, 180f, 0f); var obj = Instantiate(enemyShipPrefab, pos, rot) as GameObject; transforms.Add(obj.transform); } }
代码示例显示 C# 作业系统 + Classic Update() and AddShips() 实施
现在,您需要跟踪我们的任务,并确保它完成并重新安排每帧的新数据。上面的moveHandle.Complete() 行可确保主线程在计划任务完成之前不会继续执行。使用此作业句柄,可以准备并再次分派作业。返回 moveHandle.Complete() 后,您可以使用当前帧的新数据更新我们的 MovementJob,然后安排作业再次运行。虽然这是一个阻止操作,但它会阻止安排作业,同时仍执行旧任务。此外,它还会阻止我们在船只集合仍在迭代时添加新船只。在一个有很多任务的系统中,出于该原因,我们可能不想使用 Complete() 方法。
当您在 Update() 结束时安排 MovementJob 时,您还会向其传递需要从船只更新的所有变换的列表,通过 TransformAccessArray 访问。当所有作业都完成设置和计划后,您可以使用 JobHandle.ScheduleBatchedJobs() 方法调度所有作业。
AddShips() 方法类似于之前的实施,但有一些小的例外。如果从其他地方调用该方法,它会仔细检查作业是否已完成。这应该不会发生,但小心不出大错!此外,它还保存了对 TransformAccessArray 成员中新生成的变换的引用。让我们看看工作分布和性能如何。
图 12.通过使用 C# 作业系统,我们可以在相同的帧时间(约 33 毫秒)内将典型系统中的屏幕对象数量增加近一倍。
图 13.C# 作业系统 + 典型分析器视图。现在您可以看到,Movement 和 UpdateBoundingVolumes 作业每帧大约需要 4 毫秒。这有大幅改进!另请注意,屏幕上的船只数量几乎是典型系统的两倍!
但是,我们仍然可以做得更好。目前的方法仍然存在一些限制:
- GameObject 实例化是一个冗长的过程,涉及系统调用内存分配。
- Transforms 仍然分配在堆中的随机位置。
- Transforms 仍包含未使用的数据,污染缓存行并降低内存访问效率。
3.使用作业的实体组件系统
这个问题有一些复杂,但是一旦明白了,您就会永远掌握这个知识。我们先来看看我们的新敌舰预制件如何解决这个问题:
图 14.C# 作业系统 + 实体组件系统船只预制件你可能会注意到一些新的东西。首先,除了 Transform 组件(未使用)之外,没有附加的内置 Unity 组件。这一预制件现在代表我们将用于生成实体的模板,而不是带组件的 GameObject 。预制件的概念并不像您习惯的那样完全适用于新系统。您可以将其视为存储实体数据的便捷容器。这一切都可以完全在脚本中完成。您现在还有一个附加到预制件的 GameObjectEntity.cs 脚本。这一必需组件表示此 GameObject 将被视为实体并使用新的实体组件系统。您可以看到,对象现在也包含一个 RotationComponent、一个PositionComponent 和一个 MoveSpeedComponent。标准组件(如位置和旋转)是内置的,不需要显式创建,但 MoveSpeed 需要。除此之外,我们有一个MeshInstanceRendererComponent,它向公共成员公开了一个支持 GPU 实例化的材质参考,这是新实体组件系统所必需的。让我们看看这些如何与新系统相结合。
using System; using Unity.Entities; namespace Shooter.ECS { [Serializable] public struct MoveSpeed : IComponentData { public float Value; } public class MoveSpeedComponent : ComponentDataWrapper<MoveSpeed> { } }
代码示例显示如何为实体组件系统设置 MoveSpeed 数据 (IComponentData)
当您打开其中一个数据脚本时,您会看到每个结构都继承自 IComponentData。这将数据标记为实体组件系统要使用和跟踪的类型,并允许在后台以智能方式分配和打包数据,同时您可以完全专注于您的游戏代码。ComponentDataWrapper 类允许您将这些数据公开到其附加的预制件的检视窗。您可以看到与此预制件关联的数据仅表示基本移动(位置和旋转)和移动速度所需的 Transform 组件的一部分。这是一个线索,您将不会在这一新工作流程中使用 Transform 组件。
让我们看看 GameplayManager 脚本的新版本:
using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; namespace Shooter.ECS { public class GameManager : MonoBehaviour { EntityManager manager; void Start() { manager = World.Active.GetOrCreateManager<EntityManager>(); AddShips(enemyShipCount); } void Update() { if (Input.GetKeyDown("space")) AddShips(enemyShipIncremement); } void AddShips(int amount) { NativeArray<Entity> entities = new NativeArray<Entity>(amount, Allocator.Temp); manager.Instantiate(enemyShipPrefab, entities); for (int i = 0; i < amount; i++) { float xVal = Random.Range(leftBound, rightBound); float zVal = Random.Range(0f, 10f); manager.SetComponentData(entities[i], new Position { Value = new float3(xVal, 0f, topBound + zVal) }); manager.SetComponentData(entities[i], new Rotation { Value = new quaternion(0, 1, 0, 0) }); manager.SetComponentData(entities[i], new MoveSpeed { Value = enemySpeed }); } entities.Dispose(); } } }
代码示例显示 C# 作业系统 + 实体组件系统 Update() 和 AddShips() 实施
我们进行了一些更改,以使实体组件系统能够使用脚本。请注意,您现在有一个 EntityManager 变量。您可以将此视为创建、更新或销毁实体的渠道。您还会注意到,用船只数量构建的NativeArray<Entity> 类型将生成。管理器的实例化方法采用 GameObject 参数和指定实例化实体数量的 NativeArray<Entity> 设置。传入的 GameObject 必须包含前面提到的 GameObjectEntity 脚本以及所需的任何组件数据。EntityManager 会根据 预制件 上的数据组件创建实体,而从未实际创建或使用任何 GameObjects。
创建实体后,遍历所有实体并设置每个新实例的起始数据。此示例会设置起始位置、旋转和移动速度。完成后,必须释放安全且强大的新数据容器,以防止内存泄漏。移动系统现在可以提供演示。
using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; namespace Shooter.ECS { public class MovementSystem : JobComponentSystem { [ComputeJobOptimization] struct MovementJob : IJobProcessComponentData<Position, Rotation, MoveSpeed> { public float topBound; public float bottomBound; public float deltaTime; public void Execute(ref Position position, [ReadOnly] ref Rotation rotation, [ReadOnly] ref MoveSpeed speed) { float3 value = position.Value; value += deltaTime * speed.Value * math.forward(rotation.Value); if (value.z < bottomBound) value.z = topBound; position.Value = value; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { MovementJob moveJob = new MovementJob { topBound = GameManager.GM.topBound, bottomBound = GameManager.GM.bottomBound, deltaTime = Time.deltaTime }; JobHandle moveHandle = moveJob.Schedule(this, 64, inputDeps); return moveHandle; } } }
代码示例展示 C# 作业系统 + 实体组件移动系统实施
这是演示最基本的部分。设置实体后,您可以将所有相关的移动工作隔离到新的 MovementSystem。我们从示例代码的顶部到底部来介绍每个新概念。
MovementSystem 类继承自 JobComponentSystem。这个基类为您提供了实施所需的回调函数,如 OnUpdate(),以确保与系统相关的所有代码保持独立。您可以在这个简洁的软件包中执行系统特定更新,而不是拥有 uber-GameplayManager.cs。JobComponentSystem 的理念是将包含的所有数据和生命周期管理存储在一个地方。
<ECS/ECS_MovementJobStruct.cs>
MovementJob 结构封装了作业所需的所有信息,包括通过 Execute 函数中的参数输入的每个实例数据以及通过 OnUpdate() 更新的成员变量的每个作业数据。请注意,除 position 参数之外,所有每个实例数据都标有 [ReadOnly] 属性。这是因为在这个例子中我们只更新每帧的位置。每个船只实体的 旋转 和 移动速度在其生命周期内都是固定的。实际的 Execute 函数包含对所有必需数据进行操作的代码。
您可能想知道如何将所有位置、旋转和移动速度数据输入到 Execute 函数调用中。这些操作会在后台自动进行。实体组件系统非常智能,能够针对包含 IComponentData 类型(指定为 IJobProcessComponentData 的模板参数)的所有实体自动过滤和注入数据。
using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; namespace Shooter.ECS { public class MovementSystem : JobComponentSystem { // ... // Movement Job // ... protected override JobHandle OnUpdate(JobHandle inputDeps) { MovementJob moveJob = new MovementJob { topBound = GameManager.GM.topBound, bottomBound = GameManager.GM.bottomBound, deltaTime = Time.deltaTime }; JobHandle moveHandle = moveJob.Schedule(this, 64, inputDeps); return moveHandle; } } }
代码示例显示 C# 作业系统 OnUpdate() 方法实施
下面的 OnUpdate() 方法 MovementJob 也是新方法。这是 JobComponentSystem 提供的一个虚拟功能,因此您可以在同一个脚本中更轻松地组织每帧设置和调度。这里所做的一切都是:
- 设置 MovementJob 数据,使用新注入的 ComponentDataArrays (每个实体实例数据)
- 设置每帧数据(时间和边界)
- 调度任务
瞧!我们的作业已经设置并且完全独立。在您首次实例化包含这一特定数据组件组的实体之前,不会调用OnUpdate() 函数。如果您决定添加一些具有相同移动行为的小行星,那么您只需要 GameObject 添加这三个相同的组件脚本(包含您实例化的代表性 GameObject 上的数据类型)即可。这里要知道的重要一点是,MovementSystem 并不关心它正在运行的实体是什么。它只关心实体是否包含它关注的数据类型。还有一些机制可以帮助控制生命周期。
图 15.以约 33 毫秒的相同帧时间运行,我们现在可以使用实体组件系统在屏幕上一次拥有 91,000 个对象。
图 16.由于不依赖于典型系统,实体组件系统可以使用可用的 CPU 时间来跟踪和更新更多对象。正如您在上面的分析器窗口中看到的那样,您现在已经丢失了转换更新方法,该方法在 C# 作业系统和上面显示的典型组合部分的主线程上花费了相当多的时间。这是因为我们完全绕过了之前的 TransformArrayAccess 管道,并直接更新了 MovementJob 中的位置和旋转信息,然后显式构建了我们自己的渲染矩阵。这意味着无需写回传统的 Transform 组件。我们忘记了突发编译器的一个小细节。
突发编译器
现在,我们将采用完全相同的场景,除了将 [ComputeJobOptimization] 属性保留在作业结构之上以允许突发编译器接收作业之外,对代码完全没有任何作用,我们将获得所有这些优势。只需确保在下面显示的“作业”下拉窗口中选择“使用突发作业”设置。
图 17.下拉菜单允许使用突发作业。
图 18.通过允许突发作业优化 [ComputeJobOptimization] 属性的作业,我们可以将屏幕上同时显示的对象数量从 91,000 个增加到具有更高潜力的 150,000 个。
图 19.在这个简单的示例中,所有 MovementJob 和 UpdateRotTransTransform 任务的总完成时间从 25 毫秒缩短为仅 2 毫秒。我们现在可以看到,瓶颈已经从 CPU 转移到 GPU,因为在 GPU 上渲染所有这些小型船只的成本现在超过了在 CPU 端跟踪、更新和渲染命令生成/分派的成本。从截图中可以看出,我们在屏幕上以相同的帧速率获得了 59,000 多个实体。全部是免费的。这是因为,突发编译器能够对 Execute() 函数中的代码执行一些神秘魔法,从而充分利用紧凑的新数据布局和现代 CPU 在后台提供的最新架构增强功能。如上所述,这种神秘魔法实际上采用自动矢量化的形式,提供优化调度,可以更好地利用指令级并行性来减少管道中的数据依赖性和停顿。
结论
花几天时间研究所有这些新概念,它们将为后续项目带来好处。通过这些新系统获得的出色收益和节能效果是实实在在的真金白银。
表 1.优化带来了大幅改进,如屏幕上支持的对象数量和更新成本。
典型 C# 作业系统 + 典型 C# 作业系统 + 实体组件系统(关闭突发) C# 作业系统 + 实体组件系统(开启突发) 总帧时间 ~ 33 毫秒 / 帧 ~ 33 毫秒 / 帧 ~ 33 毫秒 / 帧 ~ 33 毫秒 / 帧 屏幕上的对象数量 16,500 28,000 91,000 150,000+ 移动作业时间成本 ~ 2.5 毫秒 / 帧 ~ 4 毫秒 / 帧 ~ 4 毫秒 / 帧 ~ < 0.5 毫秒 / 帧 绘制所有船只的 CPU 渲染时间成本 10 毫秒 / 帧 18.76 毫秒 / 帧 计算渲染矩阵的 18.92 任务 + 3 毫秒渲染命令 = 21.92 毫秒 / 帧 计算渲染矩阵的约 4.5 毫秒作业 + 4.27 毫秒渲染命令 = 8.77 毫秒 / 帧 时间 GPU 绑定 ~ 0 毫秒 / 帧 ~ 0 毫秒 / 帧 ~ 0 毫秒 / 帧 ~ 15.3 毫秒 / 帧 如果您的目标是移动平台并希望大幅降低玩家保留的电池消耗系数,只需获取收益并保存即可。如果您正在为 PC 大师赛提供高端桌面体验,请利用这些优势通过先进的模拟或破坏技术做一些特别的事情,使您的环境更具动态性、可交互性和沉浸感。站在巨人的肩膀上,利用这种革命性的新技术,实现以前认为不可能实时完成的事情,然后将其放在资产商店中,以便我使用。
感谢阅读。请继续关注来自 Unity 的更多样本 - 拭目以待!
-
BlipsAndChitz:C#作业-Rick and Morty主题自动售货机
2021-03-30 01:26:53BlipsAndChitz C#作业-Rick and Morty主题的自动售货机。 -
VertexAnimationJob:具有C#作业系统和新的Mesh API的顶点动画
2021-02-06 06:28:03VertexAnimationJob是一个Unity项目,其中包含使用Unity 2019.3 / 2020.1。中添加的新Mesh API的示例。... 它通过作业系统将顶点更新作业分配到多个工作线程。 借助新的API,可以用最少的内存副本来实现它。 -
c#作业内容
2013-12-17 09:04:32c#作业内容 -
华农C#作业随机数
2015-08-17 22:12:13华农 C# 作业 随机数 刘汉兴!师弟师妹快下载 -
C#作业之修改Demo为自己的
2018-10-07 14:54:17第一步、修改解决方案名 更改左边的,文件名称自动命名。 修改ConsoleApp和WpfApp的名字,并双击Properties修改程序集和默认命名空间。 二、修改文件内的命名空间以及依赖关系 任意界面按:“Ctrl”+“H” ... -
吉林大学C#程序设计编程作业.zip
2021-10-15 10:20:36吉林大学C#程序设计编程作业 -
Unity_ 实体组件系统 (ECS)、C# 作业系统和突发编译器.pdf
2020-01-08 12:57:00Unity_ 实体组件系统 (ECS)、C# 作业系统和突发编译器.pdf -
C#大作业学生管理系统.zip
2021-06-03 13:22:47大二菜鸟做的很普通的学生管理系统 -
C#作业在线考试系统代码及数据库
2015-07-15 12:16:39C#在VS2013上编写在线考试系统代码及数据库 -
C#数据库大作业(学生信息管理系统).zip
2021-12-23 19:34:38免费的,开源的,C#数据库大作业(学生信息管理系统),可能还有未知bug还请各位指正,谢谢,如果可以关注一波就更好了 -
c#结课作业.doc
2021-02-04 15:27:31C# -
C# 简单的作业调度
2018-03-08 16:56:32,C# 简单的作业调度,亦可参考博客:http://www.cnblogs.com/chenwolong/p/Job.html,C# 简单的作业调度,亦可参考博客:http://www.cnblogs.com/chenwolong/p/Job.html,C# 简单的作业调度,亦可参考博客:...