Unity Jobsystem ECS

news2024/11/24 18:41:41

简介

随着ECS的加入,Unity基本上改变了软件开发方面的大部分方法。ECS的加入预示着OOP方法的结束。随着实体组件系统ECS的到来,我们在Unity开发中曾使用的大量实践方法都必须进行改变以适应ECS,也许不少人需要些时间适应ECS的使用,但是ECS会对游戏性能的提升产生很大作用。

面向对象编程是一个很好的编程模式,OOP非常容易掌握和易于理解,尤其适合初学者。OOP的最大优点是它的可访问性,开发者可以在几乎没有任何相关知识的情况下创建类并维护编写的代码。然而OOP方法带有严重的缺点,它会给总体性能带来不好的影响,因为OOP方法很难避免出现重复的代码,造成一定程度上的性能开销,而且OOP虽然简单,但是却很依赖引用。

面向对象编程是基于特定对象的概念实现的,对象的种类由类的实例定义,它们通过互相交互来构建程序。对象可以包含属性和方法等形式的数据。

ECS实体组件系统的方法和OOP不同,它的数据和行为/方法是明确分离的,这样能大大提高内存使用效率,从而提高性能。

现在对于Unity而言,ECS还处于早期阶段,有很大的发展空间,但是开发者已经可以开始使用ECS。本文将讲解ECS的常见方法,Hybrid ECS和Pure ECS,并且介绍ECS的实现方法、语法以及如何开始使用ECS。

ECS方法

对于ECS,我们会更多谈论实体(Entity),而不是游戏对象。或许你会觉得实体和游戏对象没有太大区别,因为你可以将实体视为组件的容器(Container)。但是,如果你深入研究的话,会发现二者区别很大,实体其实只是特定组件的句柄。

OOP和ECS所提到的“组件”是否相同?不,这二者并不一样。在ECS出现前,我们通常将附加到游戏对象上的MonoBehaviour视为组件。MonoBehaviour包含数据和行为,它的数据来自所有变量,行为来自用于定义和调用游戏对象的函数。ECS是不同的,因为实体和组件都没有任何行为逻辑,它们只包含数据。

所有逻辑都保存在Managers/Systems中,它会获取一组实体,然后根据分组实体所包含的数据来执行所请求的行为。这意味着现在,不是所有实体都会处理自己的行为,而是所有实体行为都会在同一位置进行处理。

要完全理解为什么ECS方法比旧OOP方法的速度更快,你需要了解内存管理的相关知识。在OOP方法中,数据不会被组织起来,而是会分散再整个内存中,这是因为使用了自动内存管理功能。

自动内存管理功能介绍

像C#这种语言,内存的分配和释放过程是通过垃圾收集器自动完成的。该过程开始后,Mono平台会从操作系统请求特定容量的内存,并使用该部分内存来生成代码可使用的堆内存空间。该堆空间随着代码需要使用更多内存而逐渐增大。如果不再需要之前声明的内存,内存会释放回操作系统,堆的大小也会随之减小,这便是垃圾回收器的工作方式。

对于之后的内存分配,如果大小合适的话,垃圾回收器会使用之前用于保存数据并在释放后产生的剩余“间隙”。

因此,将数据从内存移动到缓存需要消耗较多性能,因为必须先找到引用才可以进行移动。

对于内存管理而言,ECS更加优化,因为ECS的数据会根据类型进行保存,而不是根据数据的分配时间。试想一下,一个商店根据商品的上架顺序放置商品,另一个商店根据商品的分类放置商品,你认为哪个商店的做法更好?

ECS性能更高效的另一个原因是,由于数据已明确地分离出来,ECS只会缓存相关数据。这是什么意思呢?

当使用OOP时,无论何时访问游戏对象,即使只需要一个特定属性,都必须缓存该对象的所有属性,该做法会对性能产生很大影响,因为缓存这些类型的话,会导致原生系统和托管系统之间的交互,而这样做就会产生垃圾,导致垃圾回收的发生。

Hybrid ECS

目前想纯粹使用ECS来编写一个完整的游戏还不太现实,因为部分Unity功能还未支持ECS,但是这个原因不会阻止我们使用ECS。

Hybrid ECS允许我们将ECS逻辑结合到现有的项目中,利用ECS的优点,而且不会影响还未支持ECS的功能的使用。

Hybrid ECS会和helper类一同工作,例如:Game Object Entity,它会将游戏对象转换为实体,并将附带的MonoBehaviour转换为组件。

我们编写的C#脚本派生自MonoBehaviour,它只包含数据但没有行为,然后我们将这些MonoBehaviour附加到带有Game Object Entity的游戏对象上。

我们的行为会放到Manager/System中,该类必须派生自ComponentSystem。我们不需要将该类附加到场景中的游戏对象,因为Unity会检测到该类并自动执行它。

在System类中,我们将通过定义它附带的组件来将Entity定义为struct。我们不会使用Update函数,而是使用OnUpdate函数来获取实体,并在实体中进行迭代并执行行为。

ECS Solar System (Hybrid)

我们已经了解到实体组件系统是什么,现在来研究一下实际案例。

如前文所所述,学习使用ECS对一些人而言很难,因为必须改变自己编写程序的方法。希望下面部分内容能够帮助你轻松理解该过程。

我们将使用Hybrid ECS编写一个小型宇宙,因为Hybrid ECS非常容易掌握,希望看完本案例后,你不会对ECS感到恐惧,而是更自信地尝试使用ECS。

项目准备

本文中使用的版本是Unity 2018.2.0.2,首先从资源包管理器获取Entities资源包。打开Window >> PackageManager,找到Entities进行安装。

安装完成后,Console中会出现报错内容,但这是正常现象。我们还要将脚本运行时版本从.Net 3.5改为.Net 4.x,方法是打开Build Settings >> Player Settings,修改脚本运行时版本。

预制件

Hybrid ECS拥有能将MonoBehaviour转换为组件的helper类,我们来仔细研究一下。

首先我们为星球、行星、椭圆形和月球创建大量预制件。

从上图中可以看到,该预制件就像普通预制件一样,没有什么特别之处。预制件上带有Transform、Mesh Filter、Mesh Renderer和Mesh Collider。

该预制件上添加了Game Object Entity组件。该组件不是自定义编写的脚本,而是前面提到的helper类,它将把普通游戏对象转换为实体。

Planet Comonent组件,代码如下:

using UnityEngine;

public class PlanetComponent : MonoBehaviour

{

public float rotationSpeed;

public float orbitDuration;

public OrbitalEllipse orbit;

}

Components Job用来保存数据,所以在该函数中没有其它内容。下面列出了所有需要的组件,这些组件就不进行太多讲解。

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class MoonComponent : MonoBehaviour {

public float movementSpeed;

public GameObject parentPlanet;

}

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class OrbitalElipseComponent : MonoBehaviour {

public float xExtent;

public float yExtent;

public GameObject parent;

}

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class StarComponent : MonoBehaviour {

[Range(0f, 100f)] public float twinkleFrequency;

}

这样组件和预制件就设置好了,是不是非常简单。

HybridECSSolarSystem

在我们通过创建组件来处理所有数据之前,如果仔细阅读前文,就知道现在我们需要处理行为。

我们将添加新C#脚本,命名为HybridECSSolarSystem,我们不会将该脚本附加到任何对象上,实际上它甚至不是MonoBehaviour,它派生自ComponentSystem。

Unity会在内部检测并执行该脚本,请记得添加using Unity.Entities声明。我们将在该类的开始定义Stars、Planets和Moons的struct,前面提到实体是组件分组的句柄,所以Stars类型的实体是附带StarComponent和MeshRenderer组件的所有实体。

或许你还注意到,我们没有Update()函数,而是使用了protected override void OnUpdate(),派生自ComponentSystem的每个类都需要拥有OnUpdate()函数,我们的实体行为将在该函数中执行。

仔细观察OnUpdate()函数中第一个foreach循环会发现,该循环获取了所有Stars类型的实体,然后循环处理每个实体。我们通过检查依赖starComponent.twinkleFrequency实体的Random.Range来获得随机性。

using UnityEngine;

using Unity.Entities;

public class HybridECSSolarSystem : ComponentSystem

{

struct Stars

{

public StarComponent starComponent;

public MeshRenderer renderer;

}

struct Planets

{

public PlanetComponent planetComponent;

public Transform transform;

}

struct Moons

{

public Transform transform;

public MoonComponent moonComponent;

}

protected override void OnUpdate()

{

foreach (var starEntity in GetEntities<Stars>())

{

int timeAsInt = (int)Time.time;

if(Random.Range(1f, 100f) < starEntity.starComponent.twinkleFrequency)

{

starEntity.renderer.enabled = timeAsInt % 2 == 0;

}

}

foreach (var planetEntity in GetEntities<Planets>())

{

planetEntity.transform.Rotate(Vector3.up * Time.deltaTime * planetEntity.planetComponent.rotationSpeed, Space.Self);

planetEntity.transform.position = planetEntity.planetComponent.orbit.Evaluate(Time.time / planetEntity.planetComponent.orbitDuration);

}

foreach (var moonEntity in GetEntities<Moons>())

{

Vector3 parentPos = moonEntity.moonComponent.parentPlanet.transform.position;

Vector3 desiredPos = (moonEntity.transform.position - parentPos).normalized * 5f + parentPos;

moonEntity.transform.position = Vector3.MoveTowards(moonEntity.transform.position, desiredPos, moonEntity.moonComponent.movementSpeed);

moonEntity.transform.RotateAround(moonEntity.moonComponent.parentPlanet.transform.position, Vector3.up, moonEntity.moonComponent.movementSpeed);

}

}

}

我们没有通过以前的方法使用Update函数,而是执行了System类中的Update,这就是在使用Hybrid ECS时的区别。

虽然我们已经完成将游戏对象转换为实体所需的操作,但我们还需要一个类来实例化银河系,现在进入下一部分。

HybridECSInstantiator

HybridECSInstantiator类的内容非常简单明了,不必进行过多赘述。我们在实例化整个太阳系的场景中,设置了一组变量用来创建游戏对象。

我们通过使用处理对象位置的onUnitSphere和UniverseRadius,将宇宙的大致结构视为球形。通过计算生成椭圆形,然后使用LineRenderer组件进行绘制,我们便得到了椭圆形轨道。

我们使用了HybridECSSolarSystem.cs类中的Ellipse.Evaluate()函数实现了行星的移动。

对于每个对象,不管是Star还是Planet,我们只是实例化这些对象,设置它们的组件并放置到合适的位置。

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class HybridECSInstatiator : MonoBehaviour

{

[Header("General Settings:")]

[SerializeField] float universeRadius;

[Header("Sun:")]

[SerializeField] GameObject sunPrefab;

[SerializeField] Vector3 sunPosition;

[Header("Moon:")]

[SerializeField] GameObject moonPrefab;

[SerializeField] float minMoonMovementSpeed;

[SerializeField] float maxMoonMovementSpeed;

[Header("Stars:")]

[SerializeField] GameObject starPrefab;

[SerializeField] float minStarsize;

[SerializeField] float maxStarsize;

[SerializeField] int starsAmount;

[SerializeField] [Range(0, 100)] float minTwinkleFrequency;

[SerializeField] [Range(0, 100)] float maxTwinkleFrequency;

[Header("Orbital Elipses:")]

[SerializeField] int elipseSegments;

[SerializeField] float elipseWidth;

[SerializeField] GameObject orbitalElipsePrefab;

[Header("Planets:")]

[SerializeField] List<Planet> planets = new List<Planet>();

static HybridECSInstatiator instance;

public static HybridECSInstatiator Instance { get { return instance; } }

GameObject sun;

void Awake()

{

instance = this;

PlaceSun();

PlaceStars();

PlacePlanets();

}

#region Sun

void PlaceSun()

{

sun = Instantiate(sunPrefab, sunPosition, Quaternion.identity);

GameObject sunParent = new GameObject();

sunParent.name = "Sun";

sun.transform.parent = sunParent.transform;

}

#endregion

#region Stars

void PlaceStars()

{

GameObject starParent = new GameObject();

starParent.name = "Stars";

for (int i = 0; i < starsAmount; i++)

{

GameObject currentStar = Instantiate(starPrefab);

currentStar.transform.parent = starParent.transform;

currentStar.GetComponent<StarComponent>().twinkleFrequency = Random.Range(minTwinkleFrequency, maxTwinkleFrequency);

float randomStarScale = Random.Range(minStarsize, maxStarsize);

currentStar.transform.localScale = new Vector3(randomStarScale, randomStarScale, randomStarScale);

currentStar.transform.position = Random.onUnitSphere * universeRadius;

currentStar.SetActive(true);

}

}

#endregion

#region OrbitalElipses

void DrawOrbitalElipse(LineRenderer line, OrbitalEllipse ellipse)

{

Vector3[] drawPoints = new Vector3[elipseSegments + 1];

for (int i = 0; i < elipseSegments; i++)

{

drawPoints[i] = ellipse.Evaluate(i / (elipseSegments - 1f));

}

drawPoints[elipseSegments] = drawPoints[0];

line.useWorldSpace = false;

line.positionCount = elipseSegments + 1;

line.startWidth = elipseWidth;

line.SetPositions(drawPoints);

}

#endregion

#region Planets

void PlacePlanets()

{

GameObject planetParent = new GameObject();

planetParent.name = "Planets";

for (int i = 0; i < planets.Count; i++)

{

GameObject currentPlanet = Instantiate(planets[i].planetPrefab);

currentPlanet.transform.parent = planetParent.transform;

currentPlanet.GetComponent<PlanetComponent>().rotationSpeed = planets[i].rotationSpeed;

currentPlanet.GetComponent<PlanetComponent>().orbitDuration = planets[i].orbitDuration;

currentPlanet.GetComponent<PlanetComponent>().orbit = planets[i].orbit;

GameObject currentElipse = Instantiate(orbitalElipsePrefab, sunPosition, Quaternion.identity);

currentElipse.transform.parent = sun.transform;

DrawOrbitalElipse(currentElipse.GetComponent<LineRenderer>(), planets[i].orbit);

if(planets[i].hasMoon)

{

GenerateMoon(currentPlanet);

}

}

}

#endregion

#region Moons

void GenerateMoon(GameObject planet)

{

GameObject moonParent = new GameObject();

moonParent.name = "Moons";

GameObject currentMoon = Instantiate(moonPrefab);

currentMoon.transform.parent = moonParent.transform;

currentMoon.GetComponent<MoonComponent>().movementSpeed = Random.Range(minMoonMovementSpeed, maxMoonMovementSpeed);

currentMoon.GetComponent<MoonComponent>().parentPlanet = planet;

}

#endregion

}

[System.Serializable]

public class OrbitalEllipse

{

public float xExtent;

public float yExtent;

public float tilt;

public Vector3 Evaluate(float _t)

{

Vector3 up = new Vector3(0, Mathf.Cos(tilt * Mathf.Deg2Rad), -Mathf.Sin(tilt * Mathf.Deg2Rad));

float angle = Mathf.Deg2Rad * 360f * _t;

float x = Mathf.Sin(angle) * xExtent;

float y = Mathf.Cos(angle) * yExtent;

return up * y + Vector3.right * x;

}

}

[System.Serializable]

public class Planet

{

public GameObject planetPrefab;

public OrbitalEllipse orbit;

public bool hasMoon;

[Header("Movement Settings:")]

public float rotationSpeed;

public float orbitDuration;

}

这样一个小型的宇宙便实现了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/373819.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学python的第二天---差分

一、改变数组元素&#xff08;差分&#xff09;方法一&#xff1a;差分数组map(int,input().split())for b in arr[:n]:print(1 if b else 0,end )方法二&#xff1a;区间合并interval.sort(keylambda x:x[0])二、差分a [0] list(map(int, input().split())) a[n 1:]三、差…

Android从屏幕刷新到View的绘制(二)之Choreographer、Vsync与屏幕刷新

0.相关分享&#xff1a; Android从屏幕刷新到View的绘制&#xff08;一&#xff09;之 Window、WindowManager和WindowManagerService之间的关系 Android从屏幕刷新到View的绘制&#xff08;二&#xff09;之Choreographer、Vsync与屏幕刷新 1. 相关类 Choreographer 编舞者…

MySQL创建表

在创建表时需要提前了解mysql里面的数据类型 常见的数据类型 创建表方式1&#xff1a; 格式&#xff1a; CREATE TABLE [IF NOT EXISTS] 表名( 字段1, 数据类型 [约束条件] [默认值], 字段2, 数据类型 [约束条件] [默认值], 字段3, 数据类型 [约束条件] [默认值], …… [表约束…

英语基础语法学习(B站英语电力公司)

1. 句子结构 五大基本句型&#xff1a; 主谓主谓宾主谓宾宾主谓宾宾补主系表 谓语&#xff1a; 一般来说&#xff0c;谓语是指主语发出的动作。&#xff08;动词&#xff09;但是很多句子是没有动作的&#xff0c;但是还是必须要有谓语。&#xff08;此时需要be动词&#x…

echo命令

这是一条内置命令。 输出指定的字符串 一、语法 echo [选项] [参数] 二、选项 -e&#xff1a;激活转义字符。 使用-e选项时&#xff0c;若字符串中出现以下字符&#xff0c;则特别加以处理&#xff0c;而不会将它当成一般文字输出&#xff1a; \a 发出警告声&#xff1b; \b 删…

k8s-yaml文件

文章目录一、K8S支持的文件格式1、yaml和json的主要区别2、YAML语言格式二、YAML1、查看 API 资源版本标签2、编写资源配置清单2.1 编写 nginx-test.yaml 资源配置清单2.2 创建资源对象2.3 查看创建的pod资源3、创建service服务对外提供访问并测试3.1 编写nginx-svc-test.yaml文…

pytorch入门2--数据预处理、线性代数的矩阵实现、求导

数据预处理是指将原始数据读取进来使得能用机器学习的方法进行处理。 首先介绍csv文件&#xff1a; CSV 代表逗号分隔值&#xff08;comma-separated values&#xff09;&#xff0c;CSV 文件就是使用逗号分隔数据的文本文件。 一个 CSV 文件包含一行或多行数据&#xff0c;每一…

尚硅谷nginx基础

nginx1. nginx安装1.1版本区别1.2安装步骤1.3 启动nginx1.4关于防火墙1.5 安装成系统服务1.6 配置nginx环境变量2. nginx基本使用2.1 基本运行原理2.2 nginx配置文件2.2.1 最小配置2.2.1.1 基本配置说明2.3 虚拟主机2.3.1域名、dns、ip地址的关系2.3.2IP地址和DNS地址的区别2.3…

Vue2 组件基础使用、父子组件之间的传值

一、什么是组件如画红框的这些区域都是由vue里的各种组件组成、提高复用信通常一个应用会以一棵嵌套的组件树的形式来组织&#xff1a;例如&#xff0c;你可能会有页头、侧边栏、内容区等组件&#xff0c;每个组件又包含了其它的像导航链接、博文之类的组件。为了能在模板中使用…

Mybatis中添加、查询、修改、删除

在Mybatis中添加数据的操作 编写相对应的SQL语句&#xff0c;并完成相关数据的对应关系 编写测试用例 需要提交事务 sqlSession commit() 这里需要注意的是mybatis是默认的是手动提交事务&#xff0c;如果不写的话会进行回滚&#xff0c;添加操作就不会被执行 或者在 如果…

15- TensorFlow基础 (TensorFlow系列) (深度学习)

知识要点 TensorFlow是深度学习领域使用最为广泛的一个Google的开源软件库 .TensorFlow中定义的数据叫做Tensor(张量), Tensor又分为常量和变量. 常量一旦定义值不能改变. 定义常量: t tf.constant([[1., 2., 3.], [4., 5., 6.]])定义变量: v tf.Variable([[1., 2., 3.], [4…

黑盒测试用例设计方法-边界值分析法

一、边界值定义 边界值分析法就是对输入或输出的边界值进行测试的一种黑盒测试方法。通常边界值分析法是作为对等价类划分法的补充&#xff0c;这种情况下&#xff0c;其测试用例来自等价类的边界。 长期的测试工作经验告诉我们&#xff0c;大量的错误是发生在输入或输出范围…

Vision Transformer(ViT)

1. 概述 Transformer[1]是Google在2017年提出的一种Seq2Seq结构的语言模型&#xff0c;在Transformer中首次使用Self-Atttention机制完全代替了基于RNN的模型结构&#xff0c;使得模型可以并行化训练&#xff0c;同时解决了在基于RNN模型中出现了长距离依赖问题&#xff0c;因…

TDG code

部分 数据集 参数设置 def setup_args(args None):args.algorithm_name TDG# args.algorithm_name HDGargs.user_num 1000000args.attribute_num 6args.domain_size 64args.epsilon 0.2args.dimension_query_volume 0.5args.query_num 20args.query_dimension 3运行…

leetcode 41~50 学习经历

leetcode 41~50 学习经历41. 缺失的第一个正数42. 接雨水43. 字符串相乘44. 通配符匹配45. 跳跃游戏 II46. 全排列47. 全排列 II48. 旋转图像49. 字母异位词分组50. Pow(x, n)小结41. 缺失的第一个正数 给你一个未排序的整数数组 nums &#xff0c;请你找出其中没有出现的最小的…

C语言数据结构(二)—— 受限线性表 【栈(Stack)、队列(Queue)】

在数据结构逻辑层次上细分&#xff0c;线性表可分为一般线性表和受限线性表。一般线性表也就是我们通常所说的“线性表”&#xff0c;可以自由的删除或添加结点。受限线性表主要包括栈和队列&#xff0c;受限表示对结点的操作受限制。一般线性表详解&#xff0c;请参考文章&…

数据结构基础之栈和队列

目录​​​​​​​ 前言 1、栈 2、队列 2.1、实现队列 2.2、循环队列 前言 上一篇中我们介绍了数据结构基础中的《动态数组》&#xff0c;本篇我们继续来学习两种基本的数据结构——栈和队列。 1、栈 特点&#xff1a;栈也是一种线性结构&#xff0c;相比数组&#xff…

(汇总记录)电机控制算法

1.S曲线应用电机加减速 电机控制 | S曲线加减速 - Tuple - 博客园 (cnblogs.com) 如要将S型曲线应用到电机的加减速控制上&#xff0c;需要将方程在X、Y坐标系进行平移&#xff0c;同时对曲线进行拉升变化&#xff1a;即 Y A B / ( 1 exp( -ax b ) ) &#xff0c;则根据该…

Pandas怎么添加数据列删除列

Pandas怎么添加数据列 1、直接赋值 # 1、直接赋值df.loc[:, "最高气温"] df["最高气温"].str.replace("℃", "").astype("int32")df.loc[:, "最低气温"] df["最低气温"].str.replace("℃"…

Java异常架构与异常关键字

Java异常简介 Java异常是Java提供的一种识别及响应错误的一致性机制。 Java异常机制可以使程序中异常处理代码和正常业务代码分离&#xff0c;保证程序代码更加优雅&#xff0c;并提高程序健壮性。在有效使用异常的情况下&#xff0c;异常能清晰的回答what, where, why这3个问…