@DreamingImLatios I figured now something different out: Is it intentional that the subscene does the GameObject conversion perfectly, but using Authoring, Baker and GetEntity seems to ignore Components like the MeshCollider? Is there a function to take advantage of the subscene conversion method as I feel like it does the job better?
Well, I better share my code. I am trying to convert terrain trees to entities, but the tree entities don’t have a MeshCollider at the end.
This script is attached to the terrain:
using UnityEditor;
using UnityEngine;
[RequireComponent(typeof(Terrain))]
public class TerrainAccess : MonoBehaviour
{
public static Terrain terrain;
public static TreeInstance[] originalTreeInstances;
private void OnValidate()
{
if (terrain == null)
terrain = GetComponent<Terrain>();
}
private void Awake()
{
//remember the original treeInstances
originalTreeInstances = terrain.terrainData.treeInstances;
//restore the trees when the scene changes or in editor exit play mode
SceneController.instance.onSceneChange.AddListener(RestoreTrees);
#if UNITY_EDITOR
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
#endif
}
#if UNITY_EDITOR
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
//check if exiting play mode
if (state == PlayModeStateChange.ExitingPlayMode)
RestoreTrees();
}
#endif
public static void ClearTrees()
{
//clear all tree instances
terrain.terrainData.treeInstances = new TreeInstance[0];
//refresh the terrain immediately to see the changes
terrain.terrainData.RefreshPrototypes();
terrain.Flush();
}
public static void RestoreTrees()
{
//just reassign the tree instances
terrain.terrainData.treeInstances = originalTreeInstances;
//refresh the terrain immediately to see the changes
terrain.terrainData.RefreshPrototypes();
terrain.Flush();
}
}
This script does the Authoring and Baking. It needs to be attached on a GameObject that is inside the subscene:
using System;
using System.Linq;
using Unity.Collections;
using Unity.Entities;
using UnityEngine;
[ExecuteInEditMode]
public class TerrainTreeAuthoring : MonoBehaviour
{
public GameObject[] treePrefabs;
//as these instance can be a lot, hide them in the inspector for better performance
[HideInInspector] public TreeEntityInstance[] treeEntityInstances;
#if UNITY_EDITOR
private void Update()
{
//don't execute this in play mode
if (Application.isPlaying)
return;
//get the terrain via a static member as thread Workers have no access to Terrain.activeTerrain, GameObjects and SceneManagement
Terrain terrain = TerrainAccess.terrain;
//only if there is a terrain and the tree count is different, update this
if (terrain != null && (treePrefabs == null || treePrefabs.Length != terrain.terrainData.treePrototypes.Length ||
treeEntityInstances == null || treeEntityInstances.Length != terrain.terrainData.treeInstances.Length))
{
//as cross scene reference is not supported, remember only its tree data and not the terrain itself
//moreover, the terrain tree structs are not supported, therefore get only the needed data
treePrefabs = terrain.terrainData.treePrototypes.Select(x => x.prefab).ToArray();
treeEntityInstances = new TreeEntityInstance[terrain.terrainData.treeInstances.Length];
//use span to achieve a much higher performance
Span<TreeInstance> sourceSpan = terrain.terrainData.treeInstances.AsSpan();
Span<TreeEntityInstance> targetSpan = treeEntityInstances.AsSpan();
for (int i = 0; i < sourceSpan.Length; i++)
{
//convert TreeInstance to TreeEntityInstance
targetSpan[i] = sourceSpan[i];
//convert the terrain position to world position
targetSpan[i].position = Vector3.Scale(targetSpan[i].position, terrain.terrainData.size) + terrain.GetPosition();
}
//set this object as dirty to save the changes
UnityEditor.EditorUtility.SetDirty(this);
Debug.Log("Entity trees were updated!");
}
}
#endif
}
public class TerrainTreeBaker : Baker<TerrainTreeAuthoring>
{
public override void Bake(TerrainTreeAuthoring authoring)
{
//create an entity and add a dynamic buffer to store the prefabs
Entity entity = GetEntity(TransformUsageFlags.None);
DynamicBuffer<TreePrefabBuffer> prefabBuffer = AddBuffer<TreePrefabBuffer>(entity);
//populate the buffer with terrain tree entity prefabs
foreach (GameObject prefab in authoring.treePrefabs)
{
prefabBuffer.Add(new TreePrefabBuffer
{
prefab = GetEntity(prefab, TransformUsageFlags.Renderable)
});
}
//create a BlobBuilder for the TerrainTreeBlob
using (BlobBuilder blobBuilder = new BlobBuilder(Allocator.Temp))
{
ref TerrainTreeBlob root = ref blobBuilder.ConstructRoot<TerrainTreeBlob>();
//allocate space for treeEntityInstances
BlobBuilderArray<TreeEntityInstance> treeEntityInstances = blobBuilder.Allocate(ref root.treeEntityInstances, authoring.treeEntityInstances.Length);
//populate the treeEntityInstances BlobArray
for (int i = 0; i < authoring.treeEntityInstances.Length; i++)
{
treeEntityInstances[i] = authoring.treeEntityInstances[i];
}
//add the BlobAssetReference to the entity
AddComponent(entity, new TerrainTreeData
{
blobData = blobBuilder.CreateBlobAssetReference<TerrainTreeBlob>(Allocator.Persistent)
});
}
}
}
public struct TreePrefabBuffer : IBufferElementData
{
public Entity prefab;
}
public struct TerrainTreeData : IComponentData
{
public BlobAssetReference<TerrainTreeBlob> blobData;
}
public struct TerrainTreeBlob
{
public BlobArray<TreeEntityInstance> treeEntityInstances;
}
//serializable is needed to save the struct instances properly
[Serializable]
public struct TreeEntityInstance
{
public Vector3 position;
public float rotation;
public float heightScale;
public float widthScale;
public int prototypeIndex;
public static implicit operator TreeEntityInstance(TreeInstance treeInstance)
{
return new TreeEntityInstance
{
position = treeInstance.position,
widthScale = treeInstance.widthScale,
heightScale = treeInstance.heightScale,
rotation = treeInstance.rotation,
prototypeIndex = treeInstance.prototypeIndex
};
}
}
And this System spawns the tree entities
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
public partial struct TerrainTreeSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<TerrainTreeData>();
}
public void OnUpdate(ref SystemState state)
{
//let the terrain clear its trees
TerrainAccess.ClearTrees();
//create an entity command buffer
EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.Temp);
//query the TerrainTreeData and its corresponding buffer
foreach ((RefRO<TerrainTreeData> terrainTreeData, DynamicBuffer<TreePrefabBuffer> prefabBuffer, Entity entity) in
SystemAPI.Query<RefRO<TerrainTreeData>, DynamicBuffer<TreePrefabBuffer>>().WithEntityAccess())
{
//get references to the BlobAsset data
ref BlobArray<TreeEntityInstance> treeEntityInstances = ref terrainTreeData.ValueRO.blobData.Value.treeEntityInstances;
//get all Entities that have the component with the Entity reference
for (int i = 0; i < treeEntityInstances.Length; i++)
{
//instantiate the prefab Entity
Entity instance = ecb.Instantiate(prefabBuffer[treeEntityInstances[i].prototypeIndex].prefab);
//setup its position, rotation and scale
ecb.SetComponent(instance, LocalTransform.FromPositionRotationScale(
treeEntityInstances[i].position,
quaternion.Euler(0, treeEntityInstances[i].rotation, 0),
1f
));
ecb.AddComponent(instance, new PostTransformMatrix()
{
Value = float4x4.Scale(new float3(
treeEntityInstances[i].widthScale,
treeEntityInstances[i].heightScale,
treeEntityInstances[i].widthScale))
});
}
//now remove the terrain tree data and buffer by destroying its entity as everything was instantiated
ecb.DestroyEntity(entity);
}
//execute the orders and free the ecb from memory
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
EDIT 1: I updated the TerrainTreeAuthoring code and it works much better now.
- replaced OnValidate with Update and ExecuteInEditMode
- changed the condition to update the tree entities when the terrain trees have changed
- used Span instead of working with big arrays for much better performance (30 s → 0.002 s)
- added more comments
EDIT 2: I updated the TerrainTreeSystem to support non uniform scaling.