Skip to content

Commit 7cfd7f2

Browse files
Eiderentebjan
authored andcommitted
fix: Instantiate() behavior for Prefab and Entity references (#2914)
(cherry picked from commit ee81d20)
1 parent 1650b5f commit 7cfd7f2

File tree

4 files changed

+85
-36
lines changed

4 files changed

+85
-36
lines changed

sources/engine/Stride.Engine.Tests/TestEntity.cs

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Linq;
77
using Xunit;
88
using Stride.Core;
9+
using Stride.Core.Annotations;
910
using Stride.Engine.Design;
1011
using Stride.Rendering;
1112

@@ -129,8 +130,6 @@ public void TestMultipleComponents()
129130
[Fact]
130131
public void TestEntityAndPrefabClone()
131132
{
132-
Prefab prefab = null;
133-
134133
var entity = new Entity("Parent");
135134
var childEntity = new Entity("Child");
136135
entity.AddChild(childEntity);
@@ -141,17 +140,25 @@ public void TestEntityAndPrefabClone()
141140

142141
var newEntity = entity.Clone();
143142

144-
// NOTE: THE CODE AFTER THIS IS EXECUTED TWO TIMES
145-
// 1st time: newEntity = entity.Clone();
146-
// 2nd time: newEntity = prefab.Instantiate()[0];
147-
check_new_Entity:
143+
CheckEntity(newEntity);
144+
145+
// Check prefab cloning
146+
var prefab = new Prefab();
147+
prefab.Entities.Add(entity);
148+
var newEntities = prefab.Instantiate();
149+
Assert.Single(newEntities);
150+
151+
CheckEntity(newEntities[0]);
152+
return;
153+
154+
void CheckEntity([NotNull] Entity e)
148155
{
149-
Assert.Single(newEntity.Transform.Children);
150-
var newChildEntity = newEntity.Transform.Children[0].Entity;
156+
Assert.Single(e.Transform.Children);
157+
var newChildEntity = e.Transform.Children[0].Entity;
151158
Assert.Equal("Child", newChildEntity.Name);
152159

153-
Assert.NotNull(newEntity.Get<CustomEntityComponent>());
154-
var newCustom = newEntity.Get<CustomEntityComponent>();
160+
Assert.NotNull(e.Get<CustomEntityComponent>());
161+
var newCustom = e.Get<CustomEntityComponent>();
155162

156163
// Make sure that the old component and the new component are different
157164
Assert.NotEqual(custom, newCustom);
@@ -162,19 +169,37 @@ public void TestEntityAndPrefabClone()
162169
// Verify that objects references outside the Entity/Component hierarchy are not cloned (shared)
163170
Assert.Equal(custom.CustomObject, newCustom.CustomObject);
164171
}
172+
}
165173

166-
// Woot, ugly test using a goto, avoid factorizing code in a delegate method, ugly but effective, goto FTW
167-
if (prefab == null)
174+
[Fact]
175+
public void TestCloningBehavior()
176+
{
177+
var externalEntity = new Entity();
178+
var sourceEntity = new Entity();
179+
var sourceComponent = new EntityComponentWithPrefab
168180
{
169-
// Check prefab cloning
170-
prefab = new Prefab();
171-
prefab.Entities.Add(entity);
172-
var newEntities = prefab.Instantiate();
173-
Assert.Single(newEntities);
174-
175-
newEntity = newEntities[0];
176-
goto check_new_Entity;
177-
}
181+
Prefab = new Prefab(),
182+
ExternalEntityRef = externalEntity,
183+
// Not yet supported, see commented out ExternalComponentRef declaration further and PR #2914
184+
//ExternalComponentRef = externalEntity.Transform
185+
};
186+
sourceComponent.Prefab.Entities.Add(sourceEntity);
187+
sourceEntity.Add(sourceComponent);
188+
189+
var clonedComponent = sourceComponent.Prefab.Instantiate()[0].Get<EntityComponentWithPrefab>();
190+
191+
// Validate that cloning did clone the entity
192+
Assert.NotEqual(clonedComponent.Entity, sourceComponent.Entity);
193+
194+
// References to prefabs should not be deep cloned as they are content types
195+
Assert.Equal(clonedComponent.Prefab, sourceComponent.Prefab);
196+
197+
// References to entities outside this one's hierarchy should not clone the entity referenced, it should point to the same reference
198+
Assert.Equal(clonedComponent.ExternalEntityRef, sourceComponent.ExternalEntityRef);
199+
200+
// References to entity component outside this one's hierarchy should not clone the component referenced, it should point to the same reference
201+
// Not yet supported, see commented out ExternalComponentRef declaration further and PR #2914
202+
/*Assert.Equal(clonedComponent.ExternalComponentRef, sourceComponent.ExternalComponentRef);*/
178203
}
179204

180205
private class DelegateEntityComponentNotify : EntityManager
@@ -270,4 +295,20 @@ public abstract class CustomEntityComponentBase : EntityComponent
270295
{
271296
public Action<CustomEntityComponentEventArgs> Changed;
272297
}
298+
299+
[DataContract]
300+
public class EntityComponentWithPrefab : EntityComponent
301+
{
302+
public required Prefab Prefab { get; set; }
303+
public required Entity ExternalEntityRef { get; set; }
304+
305+
/* TODO:
306+
* References to entity component outside of a prefab's hierarchy should not clone the component referenced, it should point to the same reference.
307+
* More work is required on that front, particularly with EntityComponent which does not have a specific serializer,
308+
* we would need one that derive from DataContentSerializerWithReuse to properly filter external references.
309+
* The serializer for TransformComponent derives from a simple DataSerializer for example.
310+
* See also PR #2914
311+
*/
312+
/*public required TransformComponent ExternalComponentRef { get; set; }*/
313+
}
273314
}

sources/engine/Stride.Engine/Engine/Design/CloneSerializer.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public override void PreSerialize(ref T obj, ArchiveMode mode, SerializationStre
3737
}
3838
else
3939
{
40+
var dataSerializer = cloneContext.EntitySerializerSelector.GetSerializer<T>();
41+
if (dataSerializer != this)
42+
dataSerializer.PreSerialize(ref obj, mode, stream);
4043
cloneContext.SerializedObjects.Add(obj);
4144
}
4245
}
@@ -58,7 +61,9 @@ public override void PreSerialize(ref T obj, ArchiveMode mode, SerializationStre
5861
}
5962
else
6063
{
61-
base.PreSerialize(ref obj, mode, stream);
64+
var dataSerializer = cloneContext.EntitySerializerSelector.GetSerializer<T>();
65+
if (dataSerializer != this)
66+
dataSerializer.PreSerialize(ref obj, mode, stream);
6267
cloneContext.SerializedObjects.Add(obj);
6368
}
6469
}
@@ -75,7 +80,8 @@ public override void Serialize(ref T obj, ArchiveMode mode, SerializationStream
7580

7681
// Serialize object
7782
//stream.Context.Set(EntitySerializer.InsideEntityComponentProperty, false);
78-
dataSerializer.Serialize(ref obj, mode, stream);
83+
if (dataSerializer != this)
84+
dataSerializer.Serialize(ref obj, mode, stream);
7985

8086
if (obj is EntityComponent)
8187
{

sources/engine/Stride.Engine/Engine/Design/EntityCloner.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ namespace Stride.Engine.Design
2929
[DataSerializerGlobal(typeof(CloneSerializer<OfflineRasterizedSpriteFont>), Profile = "Clone")]
3030
[DataSerializerGlobal(typeof(CloneSerializer<RuntimeRasterizedSpriteFont>), Profile = "Clone")]
3131
[DataSerializerGlobal(typeof(CloneSerializer<SignedDistanceFieldSpriteFont>), Profile = "Clone")]
32+
[DataSerializerGlobal(typeof(CloneSerializer<Prefab>), Profile = "Clone")]
33+
[DataSerializerGlobal(typeof(CloneSerializer<Entity>), Profile = "Clone")]
3234
public class EntityCloner
3335
{
3436
private static readonly CloneContext cloneContext = new CloneContext();
3537
private static SerializerSelector cloneSerializerSelector = null;
38+
private static SerializerSelector entitySerializerSelector = null;
3639
public static readonly PropertyKey<CloneContext> CloneContextProperty = new PropertyKey<CloneContext>("CloneContext", typeof(EntityCloner));
3740

3841
// CloneObject TLS used to clone entities, so that we don't create one everytime we clone
@@ -45,13 +48,13 @@ private static HashSet<object> ClonedObjects()
4548
}
4649

4750
/// <summary>
48-
/// Clones the specified prefab.
49-
/// <see cref="Entity"/>, children <see cref="Entity"/> and their <see cref="EntityComponent"/> will be cloned.
51+
/// Instantiates the content of the prefab provided.
52+
/// <see cref="Prefab.Entities"/>, children <see cref="Entity"/> and their <see cref="EntityComponent"/> will be cloned.
5053
/// Other assets will be shared.
5154
/// </summary>
52-
/// <param name="prefab">The prefab to clone.</param>
53-
/// <returns>A cloned prefab</returns>
54-
public static Prefab Clone(Prefab prefab)
55+
/// <param name="prefab">The prefab to instantiate.</param>
56+
/// <returns>A clone of this prefab's <see cref="Prefab.Entities"/></returns>
57+
public static List<Entity> Instantiate(Prefab prefab)
5558
{
5659
if (prefab == null) throw new ArgumentNullException(nameof(prefab));
5760
var clonedObjects = ClonedObjects();
@@ -61,7 +64,8 @@ public static Prefab Clone(Prefab prefab)
6164
{
6265
CollectEntityTreeHelper(entity, clonedObjects);
6366
}
64-
return Clone(clonedObjects, null, prefab);
67+
68+
return Clone(clonedObjects, null, prefab.Entities);
6569
}
6670
finally
6771
{
@@ -127,17 +131,16 @@ internal static void CollectEntityTreeHelper(Entity entity, HashSet<object> enti
127131
/// <returns>The cloned object.</returns>
128132
private static T Clone<T>(HashSet<object> clonedObjects, TryGetValueFunction<object, object> mappedObjects, T entity) where T : class
129133
{
130-
if (cloneSerializerSelector == null)
131-
{
132-
cloneSerializerSelector = new SerializerSelector(true, false, "Default", "Clone");
133-
}
134+
cloneSerializerSelector ??= new SerializerSelector(true, false, "Default", "Clone");
135+
136+
entitySerializerSelector ??= new SerializerSelector(true, false, "Default");
134137

135138
// Initialize CloneContext
136139
lock (cloneContext)
137140
{
138141
try
139142
{
140-
cloneContext.EntitySerializerSelector = cloneSerializerSelector;
143+
cloneContext.EntitySerializerSelector = entitySerializerSelector;
141144

142145
cloneContext.ClonedObjects = clonedObjects;
143146
cloneContext.MappedObjects = mappedObjects;

sources/engine/Stride.Engine/Engine/Prefab.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ public sealed class Prefab
2929
/// <returns>A collection of entities extracted from the prefab</returns>
3030
public List<Entity> Instantiate()
3131
{
32-
var newPrefab = EntityCloner.Clone(this);
33-
return newPrefab.Entities;
32+
return EntityCloner.Instantiate(this);
3433
}
3534
}
3635
}

0 commit comments

Comments
 (0)