1.前言

Megacity Demo发布于2019年春,本博文撰写于2024年,ECS也早已Release并发布了1.2.3版本。不过好在核心变化不大,多数接口也只是换了调用名称,

该Demo相较于之前的Book of the Dead(2018年发布),体量要小一些,主要演示DOTS相关内容。近期刚好空闲,并且工程文件与2019.2版本躺硬盘已久,故把坑填上。

该Demo已上传百度网盘:

链接:https://pan.baidu.com/s/1X1gh6hQSRuB0KenlRZsOiw
提取码:iios

打开请使用Unity2019.1.0b7,其中Unity Package部分包会从Unity服务器下载,版本过老,

不保证是否能正确拉取,可以自行修复。

2.Hybrid ECS 部分

先讲一讲用到Hybrid ECS的几个功能。

2.1 HLOD

打开主场景MegaCity.unity后,在任意Section SubScene内,可以看见一些模型都套用有HLOD组件,

HOLD指的是场景内的细碎物件在到达最后一级LOD时,将这些物件的最后一级LOD合并进一个Mesh进行显示,例如远处的三四个房屋,电线杆

等等。合批后将替换为合并Mesh的单个模型,而模型合并操作可以离线进行,提前生成好

HOLD的缺点是内存中需要多放置HLOD模型,并且存在负优化的情况,具体看项目而定。

在MegaCity Demo中可通过脚本CombineMeshFromLOD.cs进行HLOD模型的离线创建。

而HLOD脚本则是Hybrid ECS内封装了部分功能,通过ECS计算HLOD的显示替换等一些逻辑处理,使用时需要确保LOD Group组件的LOD数量

与HLOD中的LodParentTransforms一致即可,例如下图中有2个Low LOD的GameObject,实际上是2个级别的HLOD:

(理论上是单个HLOD Mesh替换,但实际Unity支持多级别HLOD)

2.2 SubScene

SubScene是Unity通过DOTS实现的子场景嵌套功能,其核心博主认为是Unity开放的流式场景加载接口:

m_Streams[i].Operation = new AsyncLoadSceneOperation(entitiesBinaryPath, sceneData.FileSize, sceneData.SharedComponentCount, resourcesPath, entityManager);
m_Streams[i].SceneEntity = entity;

同时SubScene也附带了将场景内容转换为适合流式加载的二进制格式

3.ECS的一些常见概念

在开始看MegaCity之前,我觉得得写一些ECS的前置概念。

3.1 筛选机制

常规编写一个Manager类会通过注册(Register)/反注册(Unregister)的机制管理该类的对象,

而ECS中这样的逻辑变为了筛选机制,以MegaCity的BoxTriggerSystem为例,这是一个类似处理OnTriggerEnter事件触发的碰撞管理系统,

碰撞盒的注册通过HybridECS的Mono转换组件进行:

ECS的System中,筛选代码如下:

m_BBGroup = GetComponentGroup(
new EntityArchetypeQuery
{
All = new ComponentType[] { typeof(BoundingBox) },
None = new ComponentType[] { typeof(TriggerCondition) },
Any = Array.Empty<ComponentType>(),
});

其中含有BoundingBox的ComponentData将会被筛选到对应System中进行处理。

而传统Manager的Unregister操作在ECS中则是将这个ComponentData移除,这样下一帧筛选时就不会筛选到了。

3.2 Jobs中CommandBuffer处理

还是以MegaCity Demo的BoxTriggerSystem为例,struct Job用于处理多线程的各项任务,并可以通过Burst对底层代码进行加速,

而在Job中不能进行如ComponentData移除这样的删改操作,我们可以通过CommandBuffer来加入到操作队列,在Job结束之后进行处理,

这和渲染管线处理上的CommandBuffer有点像:

public struct TriggerJob : IJobChunk
{
public EntityCommandBuffer.Concurrent m_EntityCommandBuffer;
  //...
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
//...
// add trigger component
m_EntityCommandBuffer.AddComponent(chunkIndex, newBoundingBox, new TriggerCondition());
}
}

3.3 标记逻辑处理

那么像BoxTriggerSystem这样的碰撞管理器,如何对已经产生碰撞的对象进行标记?

其实也是通过筛选处理的,在产生碰撞后为对应Entity实体增加一个ComponentData,TriggerCondition:

m_EntityCommandBuffer.AddComponent(chunkIndex, newBoundingBox, new TriggerCondition());

筛选时跳过含有TriggerCondition的实体即可:

m_BBGroup = GetComponentGroup(
new EntityArchetypeQuery
{
All = new ComponentType[] { typeof(BoundingBox) },
None = new ComponentType[] { typeof(TriggerCondition) },
Any = Array.Empty<ComponentType>(),
});

而在另一个音乐处理的System中,又会拿到标记了TriggerCondition和MusicTrigger的实体:

m_TriggerData = GetComponentGroup(
new EntityArchetypeQuery
{
All = new ComponentType[] { typeof(TriggerCondition), typeof(MusicTrigger) },
None = Array.Empty<ComponentType>(),
Any = Array.Empty<ComponentType>()
});

所以ECS的思路就是通过标记来代替传统OnEnable/OnDisable消息事件的触发。

4.MegaCity Demo本体

4.1 场景结构

先来看下静态置于MegaCity场景中的内容结构。

  1. Audio存放了音频配置,MegaCity运用了Unity开放出来的ECS音频模块DSPGraph,不过当时(指MegaCity Demo发布时)实现比较简陋,大概是满足了基本使用需求的情况。
  2. Pathing存放了飞船的路径信息,也是该Demo想展示的一个点。
  3. 玩家飞船相关的逻辑不写了,这部分没有用到DOTS

4.2 LightPoolSystem

LightPoolSystem主要是用ECS的形式,遍历当前飞船和相机视锥范围内的灯光,进行逻辑筛选并进行对象池复用。

因为借助了HDRP渲染管线,场景内的灯光将和体积雾效果产生交互,达到较好的显示呈现。

其中LightRef脚本用于将场景中的灯光转换进ECS:

来到LightPoolSystem的OnUpdate中,对其中逻辑进行快速讲解:

protected override JobHandle OnUpdate(JobHandle handle)
{
if (Camera.main == null || !AdditiveScene.isLoaded)
return handle; #region Setup new lights #region Find closest lights #region Assign instances #region Update light intensity return handle;
}

1).第一步Setup new lights,拿到没有标记LightPoolCreatedTag组件数据的SharedLight,筛选结构如下:

m_NewSharedLights = GetComponentGroup
(
ComponentType.ReadOnly<SharedLight>(),
ComponentType.Exclude<LightPoolCreatedTag>()
);

SharedLight就是场景中HybridECS的转换对象,对应的MonoBehavior转换脚本是LightRef

假设场景内当前加载了50盏灯光,那么这一步也会创建50个实体,但对应的对象池则是用到了哪种灯光模板在惰性创建。

这一步最后再标记上LightPoolCreatedTag,防止下一次Update时进入这部分逻辑。

2).第二步Find closest lights,对已经映射上的场景灯光进行视锥和距离筛选,存入另一份NativeArray - ClosestLights。

3).第三步Assign instances分配实例,对已经筛选出来的实体分配具体灯光,并存入另一份NativeArray - AssignedLights,方便后续操作。

4).第四步Update light intensity更新灯光强度,直接操作AssignedLights更新灯光亮度,对于Active标记为False的灯光,将不断变暗直到

亮度数值为0并进行回收。

4.3 StreamingLogic

流式加载场景的封装逻辑,因为Unity SubScene并没有完全封装对应的加载卸载逻辑处理,

只提供了接口,我们还需要额外编写一层逻辑。

玩家组件上挂有配置脚本StreamingLogicConfigComponent处理流式加载的参数:

然后System中进行少量逻辑处理,最后用挂载ComponentData的方式通知Unity ECS的流失加载系统进行加载:

struct BuildCommandBufferJob : IJob
{
public EntityCommandBuffer CommandBuffer;
public NativeArray<Entity> AddRequestArray;
public NativeArray<Entity> RemoveRequestArray; public void Execute()
{
foreach (var entity in AddRequestArray)
{
CommandBuffer.AddComponent(entity, default(RequestSceneLoaded));
}
foreach (var entity in RemoveRequestArray)
{
CommandBuffer.RemoveComponent<RequestSceneLoaded>(entity);
}
}
}

4.4 Megacity Audio System

或许这个系统才是重点,但发现主要仍是Unity的封装。

首先在Package Manager中可以看见该系统的相关代码,同时也可以发现AudioMixer中空空如也,这也MegaCity Demo的不同之处,

其内部所有的音频都是基于这套系统开发的。

在项目宏定义处加上ENABLE_DSPGRAPH_INTERCEPTOR开启调试器:

开启后可以在Window/DSP Graph处打开调试器窗口,看见所有音频的Graph结构以及最终是如何汇总输出的:

Megacity demo中飞机之间快速擦过(FlyBySystem)以及交通中的各类音频都是调用了这个System

其中ECSoundEmitterComponent可挂载,类似于AudioSource:

游戏内的音频会先挂载到PlaybackSystem,好比先把Audio放置于Graph内,再将音频暂时关闭,需要时打开:

var playbackSystem = World.Active.GetOrCreateManager<SamplePlaybackSystem>();
playbackSystem.AddClip(clip);

而真正去用,则是其他地方另行处理,可以看见读取缓存的AudioClip通过GetInstanceID:

var sample = EntityManager.CreateEntity();
AddClip(clip);
EntityManager.AddComponentData(sample, new AdditiveState());
EntityManager.AddComponentData(sample, new SamplePlayback { Volume = 1, Loop = 1, Pitch = 1 });
EntityManager.AddComponentData(sample, new SharedAudioClip { ClipInstanceID = clip.GetInstanceID() });
m_SampleEntities.Add(sample);

最后看音效实现,好像没有对应接口,也是通过类似挂载AudioClip的方式,定时播放和移除挂载。

其思路和Wwise/FMod也不相似,没有事件逻辑,只是性能系统设计。

4.4.2 ChunkEntityEnumerable

通过工具类ChunkEntityEnumerable,简化了在Job中遍历Chunk时的翻页处理:

public bool MoveNext()
{
if (++elementIndex >= currChunkLength)
{
if (++chunkIndex >= chunks.Length)
{
return false;
} elementIndex = 0;
currChunk = chunks[chunkIndex].GetNativeArray(entityType);
currChunkLength = currChunk.Length;
}
return true;
}

4.5 Traffic 交通逻辑处理

这可能是MegaCity最能学到东西的一个功能,MegaCity Demo中玩家路径用的是Cinemachine Path,NPC飞船用的路径是

4.5.1 道路处理

自己写的Path.cs:

若需要编辑Path,需要勾选Show All Handles,Show Coloured Roads则是查看路网的开关。

Is On Ramp用于标记主干道(匝道),Percetage Chance For On Ramp用于标记从分支进入主干道的概率。

勾选Show Coloured Roads:

4.5.2 NPC飞船寻路处理

NPC飞船通过Path拿到道路信息,并且通过CatmullRom插值进行路径计算,非常巧妙的一点是它利用了

CatmulRom的导数得到曲线变化率,并用此作为系数实现飞船平滑过渡:

public void Execute(ref VehiclePathing p, ref VehicleTargetPosition pos, [ReadOnly] ref VehiclePhysicsState physicsState)
{
var rs = RoadSections[p.RoadIndex]; float3 c0 = CatmullRom.GetPosition(rs.p0, rs.p1, rs.p2, rs.p3, p.curvePos);
float3 c1 = CatmullRom.GetTangent(rs.p0, rs.p1, rs.p2, rs.p3, p.curvePos);
float3 c2 = CatmullRom.GetConcavity(rs.p0, rs.p1, rs.p2, rs.p3, p.curvePos); float curveSpeed = length(c1); pos.IdealPosition = c0;
pos.IdealSpeed = p.speed; if (lengthsq(physicsState.Position - c0) < kMaxTetherSquared)
{
p.curvePos += Constants.VehicleSpeedFudge / rs.arcLength * p.speed / curveSpeed * DeltaTimeSeconds;
}
}

但为什么还要除以arcLength不太清楚。

5.杂项

5.1 ComponentDataFromEntity<T>通过实体快速映射组件

以Demo中的代码为例:

foreach (var newFlyby in New)//New = Entities
{
var positional = PositionalFromEntity[newFlyby];

可以通过这个类直接得到组件,目前在新版本ECS中该类改名为了:

ComponentLookup<T>

5.2 DelayLineDopplerHack

这个脚本放在了Script文件夹外,并没有在项目里实装,它用了比较HACK的方法直接处理音频,并且

尝试实现哈斯HAAS效应:

var haasDelay = (int)((s.m_Attenuation[0] - s.m_Attenuation[1]) * m_Haas * (c * 2 - 1));
var delaySamples = Mathf.Clamp (delaySamplesBase + haasDelay, 0, maxLength);

哈斯(Haas)通过实验表明:两个同声源的声波若到达听音者的时间差Δt在5~35ms以内,人无法区分两个声源,给人以方位听感的只是前导声(超前的声源),滞后声好似并不存在;若延迟时间Δt在35~50ms时,人耳开始感知滞后声源的存在,但听感做辨别的方位仍是前导声源;若时间差Δt>50ms时,人耳便能分辨出前导声与滞后声源的方位,即通常能听到清晰的回声。哈斯对双声源的不同延时给人耳听感反映的这一描述,称为哈斯效应。这种效应有助于建立立体声的听音环境


Unity2022新版MegacityDemo下载:https://unity.com/de/demos/megacity-competitive-action-sample

Unity多人联机版本Megacity: https://unity.com/cn/demos/megacity-competitive-action-sample

Unity2019旧版本Megacity下载:https://discussions.unity.com/t/megacity-feedback-discussion/736246/81?page=5

Megacity Unity Demo工程学习的更多相关文章

  1. unity导出工程导入到iOS原生工程中详细步骤

    一直想抽空整理一下unity原生工程导入iOS原生工程中的详细步骤.做iOS+vuforia+unity开发这么长时间了.从最初的小小白到现在的小白.中间趟过了好多的坑.也有一些的小小收货.做一个喜欢 ...

  2. demo工程的清单文件及activity中api代码简单示例

    第一步注册一个账户,并创建一个应用.获取app ID与 app Key. 第二步下载sdk 第三步新建工程,修改清单文件,导入相关的sdk文件及调用相应的api搞定. 3.1 修改清单文件,主要是加入 ...

  3. 【Unity Shaders】学习笔记——SurfaceShader(十一)光照模型

    [Unity Shaders]学习笔记——SurfaceShader(十一)光照模型 转载请注明出处:http://www.cnblogs.com/-867259206/p/5664792.html ...

  4. 【Unity Shaders】学习笔记——SurfaceShader(十)镜面反射

    [Unity Shaders]学习笔记——SurfaceShader(十)镜面反射 如果你想从零开始学习Unity Shader,那么你可以看看本系列的文章入门,你只需要稍微有点编程的概念就可以. 水 ...

  5. 【Unity Shaders】学习笔记——SurfaceShader(九)Cubemap

    [Unity Shaders]学习笔记——SurfaceShader(九)Cubemap 如果你想从零开始学习Unity Shader,那么你可以看看本系列的文章入门,你只需要稍微有点编程的概念就可以 ...

  6. 【Unity Shaders】学习笔记——SurfaceShader(八)生成立方图

    [Unity Shaders]学习笔记——SurfaceShader(八)生成立方图 转载请注明出处:http://www.cnblogs.com/-867259206/p/5630261.html ...

  7. 【Unity Shaders】学习笔记——SurfaceShader(七)法线贴图

    [Unity Shaders]学习笔记——SurfaceShader(七)法线贴图 转载请注明出处:http://www.cnblogs.com/-867259206/p/5627565.html 写 ...

  8. 【Unity Shaders】学习笔记——SurfaceShader(六)混合纹理

    [Unity Shaders]学习笔记——SurfaceShader(六)混合纹理 转载请注明出处:http://www.cnblogs.com/-867259206/p/5619810.html 写 ...

  9. 【Unity Shaders】学习笔记——SurfaceShader(五)让纹理动起来

    [Unity Shaders]学习笔记——SurfaceShader(五)让纹理动起来 转载请注明出处:http://www.cnblogs.com/-867259206/p/5611222.html ...

  10. 【Unity Shaders】学习笔记——SurfaceShader(四)用纹理改善漫反射

    [Unity Shaders]学习笔记——SurfaceShader(四)用纹理改善漫反射 转载请注明出处:http://www.cnblogs.com/-867259206/p/5603368.ht ...

随机推荐

  1. NOIP模拟91(多校24)

    T1 破门而入 解题思路 签到题(然而我数组开小直接变成暴力分...) 发现其实就是第一类斯特林数,然后 \(n^2\) 推就好了. 感觉可以用 NTT 优化成 \(nlogn\) ,但是好像并没有什 ...

  2. docker——health(容器的健康检查)

    容器的健康检查机制 了解在dockerfile中容器的健康检查 # 在dockerfile中使用healthcheck指令,声明健康检测配置,用于判断容器主进程的服务状态是否正常,反映容器的实际健康状 ...

  3. [SWPUCTF 2021 新生赛]gift_F12

    首先我们打开环境会发现花里胡哨的,而题目中有提示:F12,所以我们直接F12查看源码 然后ctrl+f信息检索flag.直接找到flag提交 但要注意提交格式为NSSCTF{}

  4. 一文搞懂 ARM 64 系列: 一文搞懂 ARM 64 系列: 函数调用传参与返回值

    函数调用涉及到传参与返回值,下面就来看下ARM 64中,参数与返回值的传递机制. 1 整数型参数传递 这里的整数型并不单指int类型,或者NSInteger类型,而是指任何能够使用整数表示的数据类型, ...

  5. ssh练习

    根据要求完成部署 根据如下要求,完成部署过程 1.恢复7.8.9.31.41所有机器的快照 7 8 9 web服务 nginx ​ 172.16.1.xx ​ ​ nfs-31 提供共享文件存储 ​ ...

  6. 一种复习flex布局的方法

    方法论 flex布局有多个属性,时常会忘记.我们复习的话,单纯看一些博客文章,不能直观的理解,也比较枯燥. 因此如果有一种用写代码闯关的方式来复习(学习)flex布局,那也许会更有意思. FLEXBO ...

  7. JavaScript 中判断 {}是空对象

    Javascript 中判断空对象 简介:在 JavaScript 判断字符串是否是一个空字符串 可以 !"" 返回 true 来判断, 要是判断 {} 是否是空对象,也用 !{} ...

  8. 彻底解决C盘不够用的问题(Windows 10)- 常规方法——清垃圾、转虚拟内存、挪大文件

    1.清垃圾 2.转虚拟内存 3.挪大文件

  9. Flash驱动控制--芯片擦除(SPI协议)

    摘要: 本篇博客具体包括SPI协议的基本原理.模式选择以及时序逻辑要求,采用FPGA(EPCE4),通过SPI通信协议,对flash(W25Q16BV)存储的固化程序进行芯片擦除操作. 关键词:SPI ...

  10. 03-vi和vim编辑器的使用

    背景 vim是一个类似于vi的著名的功能强大.高度可定制的文本编辑器. vim在vi的基础上改进和增加了很多特性. 如今vi已经是最受IT届欢迎的编辑器之一. 不止在Linux中,主流IDE都支持vi ...