HelloCube
ForEach
ForEach是一個合體的Cubes共同旋轉的簡單場景。
RotatingCube掛載了RotationSpeed Convert to Entity,將該GameObject轉換爲Entity,該物體的GameEngine Component是Transform,作爲一個旋轉單位,保存爲實體貌似也是正確的,ECS中C作爲數據集合,作爲rotation component我認爲也是可以的(更正:RotationSpeedAuthoring沒有處理旋轉,只是單純記了個數據,因此它不是Component)。
public class RotationSpeedSystem_ForEach : ComponentSystem
對應ECS中的Systsem,也是Component,Unity ECS竟然可以一個腳本同時作爲System和Component,此處處理的RotationCube的旋轉,是個Component,同時這個場景中只有單個旋轉需要處理,同時作爲System專門處理物體旋轉也行。
RotationSpeedAuthoring_ForEach繼承了MonoBehaviour,因此需要在場景內的GameObject上掛載,接着它又實現了IConvertGameObjectToEntity接口,裏面只有一個接口方法void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem);
這個接口是轉化爲實體,將GameObject的Behaviour轉化爲實體?不應該是Component麼,Convert方法裏new RotationSpeed_ForEach,這應該是個真正的實體了。
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var data = new RotationSpeed_ForEach { RadiansPerSecond = math.radians(DegreesPerSecond) };
dstManager.AddComponentData(entity, data);
}
EntityManager在Unity.Entities命名空間下,裏面有各種AddComponent/AddComponentData方法,Convert實例化了RotationSpeed_ForEach,關於RotationSpeed_ForEach它屬於Component,實現了IComponentData接口,這個接口非常簡單==過於簡單,啥都沒有隻是說了這個接口的含義,嗯,長成這樣
namespace Unity.Entities
{
public interface IComponentData
{
}
}
這樣看來Unity的ECS框架和普通ECS很不一樣,從Component可以直接創建Entity,我認爲繼承自MonoBehavious的類是組件,這個組件實現了ConvertGameObjectToEntity並在Convert方法中將自身作爲數據加入EntityManager。
public class RotationSpeedSystem_ForEach : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach((ref RotationSpeed_ForEach rotationSpeed, ref Rotation rotation) =>
{
var deltaTime = Time.deltaTime;
rotation.Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * deltaTime));
});
}
繼承自ComponentSystem集中處理OnUpdate的每幀刷新,Unity說現在這種做法不是最優解,但足以將ComponentSystem Update (logic) and ComponentData (data)解耦合,MonoBehavious已經不再處理OnUpdate的刷新了。
ForEachWithEntityChanges
Spawner有個厲害的操作,將Hierarchy的預設轉換爲了Entity,而且EntityManager提供接口entityManager.Instantiate(prefab),直接創建prefab實例,細節不說,直接看代碼。
Entity prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(Prefab, World.Active);
var entityManager = World.Active.EntityManager;
SpawnFromMonoBehaviour
這個示例我覺得能體現一些ECS的優勢,將數據和邏輯行爲解耦,Spawner_FromMonoBehaviour只需要管理行爲,而旋轉數據由自身RotationSpeedAuthoring_IJobForEach,添加進EntityManager.AddComponentData(entity, data);統一處理。
Advanced/Boids
這個示例是大量計算海洋魚羣路徑,模擬2個魚羣被鯊魚追逐的軌跡運動。
基本思想是生成兩個巨量魚羣,每幀刷新每條魚的移動方位,先計算當前位置到targets[0]點的距離,變例全部的目標點取得最短距離。
NearestPosition方法並沒有返回值,這是Convert to Entity的好處,數值由Entity統一管理,提交的一方只要確保數據正確。
void NearestPosition(NativeArray<float3> targets, float3 position, out int nearestPositionIndex, out float nearestDistance )
{
nearestPositionIndex = 0;
nearestDistance = math.lengthsq(position-targets[0]);
for (int i = 1; i < targets.Length; i++)
{
var targetPosition = targets[i];
var distance = math.lengthsq(position-targetPosition);
var nearest = distance < nearestDistance;
nearestDistance = math.select(nearestDistance, distance, nearest);
nearestPositionIndex = math.select(nearestPositionIndex, i, nearest);
}
nearestDistance = math.sqrt(nearestDistance);
}
// Resolves the distance of the nearest obstacle and target and stores the cell index.
public void ExecuteFirst(int index)
{
var position = cellSeparation[index] / cellCount[index];
int obstaclePositionIndex;
float obstacleDistance;
NearestPosition(obstaclePositions, position, out obstaclePositionIndex, out obstacleDistance);
cellObstaclePositionIndex[index] = obstaclePositionIndex;
cellObstacleDistance[index] = obstacleDistance;
int targetPositionIndex;
float targetDistance;
NearestPosition(targetPositions, position, out targetPositionIndex, out targetDistance);
cellTargetPositionIndex[index] = targetPositionIndex;
cellIndices[index] = index;
}
Shark上掛載了GameObjectEntity
GameObjectEntity感覺非常方便,在該物體滿足enabled && gameObject.activeInHierarchy時,會自動AddToEntityManager(m_EntityManager, gameObject);加入到EntityManager的組件管理中,CreateEntity(entityManager, archetype, components, types);生成一個實體返回。
public static void CopyAllComponentsToEntity(GameObject gameObject, EntityManager entityManager, Entity entity)
{
foreach (var proxy in gameObject.GetComponents<ComponentDataProxyBase>())
{
// TODO: handle shared components and tag components
var type = proxy.GetComponentType();
entityManager.AddComponent(entity, type);
proxy.UpdateComponentData(entityManager, entity);
}
}
它可以將這個GameObject上全部的Component轉成ComponentData,而且在OnEnable/OnDisable都有處理。
來看下SpawnRandomInSphere,雖然類的命名是在球體半徑中隨機生成魚,但這個類裏面乾的事情全是提交數據,它是和Entity打交道,沒有處理生成魚的行爲。
它繼承自MonoBehaviour,也就是說這個類會直接掛在場景內的GameObject上,後面兩個接口分別是聲明需要創建的prefab和將這個GameObject的數據轉換爲Entity。
public class SpawnRandomInSphere : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
public GameObject Prefab;
public float Radius;
public int Count;
// Lets you convert the editor data representation to the entity optimal runtime representation
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var spawnerData = new Samples.Boids.SpawnRandomInSphere // 自定義的數據格式,是個繼承ISharedComponentData的結構體
{
// The referenced prefab will be converted due to DeclareReferencedPrefabs.
// So here we simply map the game object to an entity reference to that prefab.
Prefab = conversionSystem.GetPrimaryEntity(Prefab),// 將GameObject Prefab轉變爲一個實體返回
Radius = Radius,
Count = Count
};
dstManager.AddSharedComponentData(entity, spawnerData);
}
// Referenced prefabs have to be declared so that the conversion system knows about them ahead of time
public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
{
referencedPrefabs.Add(Prefab);
}
}
SpawnRandomInSphereSystem並不是World,而是ComponentSystem它處理所有提交的ComponentData.
var spawnPositions = new NativeArray<float3>(toSpawnCount, Allocator.TempJob);
GeneratePoints.RandomPointsInUnitSphere(spawnPositions);
NativeArray<T>只能容納值對象。
在創建的時候除了指定length外,還需要指定allocator模式:Temp(臨時),TempJob(Job內臨時),Persistent(持久)。
這是Unity官方提供的容器類,它所指定的allocator模式可能是類似Temp對應棧內存分配,Persistent對應堆內存分配的方式。
它只是簡單的封裝一下數組,本質和普通的struct數組似乎沒什麼區別,都能內存連續使cpu更容易命中緩存。
var entities = new NativeArray<Entity>(toSpawnCount, Allocator.Temp);
for (int i = 0; i < toSpawnCount; ++i)
{
entities[i] = PostUpdateCommands.Instantiate(spawner.Prefab);
}
生成隨機點是由Unity.Mathematics提供的接口,沒有看到生成隨機點後的返回,這個方法是void.Instantiate是EntityCommandBuffer的創建函數,Unity註釋說This code is placeholder until we add the ability to bulk-instantiate many entities from an ECB,這句生成只是個佔位符,用於理解邏輯,真正的生成在其他地方。
for (int i = 0; i < toSpawnCount; i++)
{
PostUpdateCommands.SetComponent(entities[i], new LocalToWorld
{
Value = float4x4.TRS(
localToWorld.Position + (spawnPositions[i] * spawner.Radius),
quaternion.LookRotationSafe(spawnPositions[i], math.up()),
new float3(1.0f, 1.0f, 1.0f))
});
}
隨後計算每個實體看下目標點的下一個位置,魚的行進路線是看向目標點的,這個計算是當前點的世界座標+生成點(遊戲初始化時)*生成魚羣半徑+轉向目標的轉動偏移。
最後計算完了這些數據,SpawnRandomInSphereSystem將這個節點的數據刪除。
Boid : MonoBehaviour
本實例中有3個Boid變體,Boid類只是ComponentData,並轉換爲Entity.
BoidSystem : JobComponentSystem
可以直接看BoidSystem,發現行爲代碼在MergeCells中,有3個方法ExecuteFirst、ExecuteNext、NearestPosition。
MergeCells使用的[BurstCompile]編譯器,也許這是我找不到ExecuteFirst、ExecuteNext調用地方的原因,在ExecuteFirst中分別計算了到障礙物和到目標點的最近點,計算最近點是當前點到所有障礙、目標的窮舉運算。
看了一篇文章https://gameinstitute.qq.com/community/detail/126083,上面說Unity的ECS和JobSystem很相似,C# JobSystem 只支持structs和NativeContainers,並不支持託管數據類型。所以,在C# JobSystem中,只有IComponentData數據可以被安全的訪問。