Last active
March 13, 2023 02:20
-
-
Save Eideren/0aa59644eb7c5b029d04b7fa5a285e62 to your computer and use it in GitHub Desktop.
A small class to export/save a scene or a group of entities within a running stride game
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Project | |
{ | |
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.IO; | |
using Stride.Core; | |
using Stride.Core.Annotations; | |
using Stride.Core.Mathematics; | |
using Stride.Core.Yaml; | |
using Stride.Core.Yaml.Serialization; | |
using Stride.Engine; | |
/// <summary> | |
/// This class requires NuGet 'Stride.Core.Packages', 'Stride.Core.BuildEngine.Common', | |
/// 'Stride.Core.Assets', 'Stride.Core.Design' and 'Stride.Core.Yaml'. | |
/// Add them as 'PackageReference' to your project. | |
/// </summary> | |
public class StrideExporter | |
{ | |
/// <summary> | |
/// Serializes <paramref name="rootEntities"/> into stride's scene (sdscene) format which can be read by the editor. | |
/// </summary> | |
/// <param name="stream">The output stream, a file stream pointing to a *.sdscene file for example</param> | |
/// <param name="rootEntities">The entities at the root of the scene</param> | |
public static void AsYamlScene( Stream stream, List<Entity> rootEntities ) | |
{ | |
var sceneProxy = new SceneAssetProxy(); | |
var root = sceneProxy.Hierarchy.RootParts; | |
var all = sceneProxy.Hierarchy.Parts; | |
root.AddRange( rootEntities ); | |
foreach( Entity e in root ) | |
all.Add( new Wrapper { Entity = e } ); | |
// Collect all entities | |
for( int i = 0; i < all.Count; i++ ) | |
{ | |
var e = all[ i ].Entity; | |
foreach( var child in e.Transform.Children ) | |
all.Add( new Wrapper { Entity = child.Entity } ); | |
} | |
// Ensure ids are unique | |
var ogIdMapping = new Dictionary<IIdentifiable, Guid>(); | |
try | |
{ | |
var uniqueIdTest = new HashSet<Guid>(); | |
foreach( var wrapper in all ) | |
{ | |
var e = wrapper.Entity; | |
ogIdMapping.Add( e, e.Id ); | |
while( uniqueIdTest.Contains( e.Id ) ) | |
e.Id = Guid.NewGuid(); | |
uniqueIdTest.Add( e.Id ); | |
foreach( var comp in wrapper.Entity ) | |
{ | |
ogIdMapping.Add( comp, comp.Id ); | |
while( uniqueIdTest.Contains( comp.Id ) ) | |
comp.Id = Guid.NewGuid(); | |
uniqueIdTest.Add( comp.Id ); | |
} | |
} | |
using( MemoryStream memStream = new MemoryStream() ) | |
{ | |
AssetYamlSerializer.Default.Serialize( memStream, sceneProxy ); | |
memStream.Flush(); | |
memStream.Seek( 0, SeekOrigin.Begin ); | |
var yaml = new YamlStream(); | |
using( var reader = new StreamReader( memStream ) ) | |
yaml.Load( reader ); | |
var rootNode = yaml.Documents[ 0 ].RootNode; | |
rootNode.Tag = "!SceneAsset"; | |
// Serialization does not create refs, we're taking care of that here | |
MapRefs( ref rootNode, new Stack<(YamlNode, YamlNode)>(), new HashSet<Guid>() ); | |
using( var writer = new StreamWriter( stream ) ) | |
yaml.Save( writer ); | |
} | |
} | |
finally | |
{ | |
// Reset Ids to previous, potentially not unique, value | |
foreach( var kvp in ogIdMapping ) | |
kvp.Key.Id = kvp.Value; | |
} | |
} | |
static bool MapRefs( ref YamlNode n, Stack<(YamlNode, YamlNode)> stack, HashSet<Guid> refs ) | |
{ | |
if( n is YamlMappingNode m ) | |
{ | |
for( int i = 0; i < m.Children.Count; i++ ) | |
{ | |
var kvp = m.Children[ i ]; | |
// This node has an Id, it most likely is an IIdentifiable, | |
// see if we should transform this node from an instance into a ref instead | |
// this part might create bugs if one of your components has an 'Id' parameter with a valid Guid, | |
// not sure how to work around that without introducing lots of garbage | |
if( kvp.Key is YamlScalarNode ysn | |
&& ysn.Value == "Id" | |
&& kvp.Value is YamlScalarNode ysn2 | |
&& Guid.TryParse( ysn2.Value, out var guid ) ) | |
{ | |
if( refs.Contains( guid ) || IsTransformCompChild( stack ) || IsRoot( stack ) ) | |
{ | |
n = new YamlScalarNode( $"ref!! {guid}" ); | |
return true; | |
} | |
else | |
{ | |
refs.Add( guid ); | |
} | |
} | |
var k = kvp.Key; | |
var v = kvp.Value; | |
stack.Push( ( k, v ) ); | |
if( MapRefs( ref k, stack, refs ) ) | |
m.Children[ i ] = new KeyValuePair<YamlNode, YamlNode>( k, v ); | |
if( MapRefs( ref v, stack, refs ) ) | |
m.Children[ i ] = new KeyValuePair<YamlNode, YamlNode>( k, v ); | |
stack.Pop(); | |
} | |
} | |
else if( n is YamlSequenceNode ysq ) | |
{ | |
for( int i = 0; i < ysq.Children.Count; i++ ) | |
{ | |
stack.Push( ( new YamlScalarNode { Value = "-" }, null ) ); | |
var k = ysq.Children[ i ]; | |
if( MapRefs( ref k, stack, refs ) ) | |
ysq.Children[ i ] = k; | |
stack.Pop(); | |
} | |
} | |
return false; | |
} | |
/// <summary> | |
/// Is stack on a ...Hierarchy.RootParts.-.? | |
/// </summary> | |
static bool IsRoot( Stack<(YamlNode left, YamlNode right)> stack ) | |
{ | |
using var e = stack.GetEnumerator(); | |
if( e.MoveNext() && e.Current.left is YamlScalarNode ysn && ysn.Value == "-" | |
&& e.MoveNext() && e.Current.left is YamlScalarNode ysn2 && ysn2.Value == "RootParts" | |
&& e.MoveNext() && e.Current.left is YamlScalarNode ysn3 && ysn3.Value == "Hierarchy" ) | |
return true; | |
return false; | |
} | |
/// <summary> | |
/// Is stack on a ...!TransformComponent.Children.{id}.? | |
/// </summary> | |
static bool IsTransformCompChild( Stack<(YamlNode left, YamlNode right)> stack ) | |
{ | |
using var e = stack.GetEnumerator(); | |
if( e.MoveNext() // an id | |
&& e.MoveNext() && e.Current.left is YamlScalarNode ysn && ysn.Value == "Children" | |
&& e.MoveNext() && e.Current.right is YamlMappingNode ymn && ymn.Tag == "!TransformComponent" ) | |
return true; | |
return false; | |
} | |
/// <summary> | |
/// We're not using SceneAsset directly as that namespace is a pain in the ass to use, | |
/// so we're replicating the way that class is laid out through this very similar class | |
/// </summary> | |
[ DataContract( "SceneAssetProxy" ) ] | |
public class SceneAssetProxy | |
{ | |
[ DataMember( 0, DataMemberMode.Assign ) ] | |
public Guid Id = Guid.NewGuid(); | |
[ DataMember( 1, DataMemberMode.Assign ), DataStyle( DataStyle.Compact ), DefaultValue( null ), NonIdentifiableCollectionItems ] | |
public Dictionary<string, string> SerializedVersion = new Dictionary<string, string> { { "Stride", "3.1.0.1" } }; | |
[ DataMember( 2 ), NonIdentifiableCollectionItems, MemberCollection( NotNullItems = true ) ] | |
public List<object> Tags = new List<object>(); | |
[ DataMember( 3 ), NonIdentifiableCollectionItems, NotNull ] | |
public List<Guid> ChildrenIds = new List<Guid>(); | |
[ DataMember( 4 ) ] | |
public Vector3 Offset; | |
[ DataMember( 5 ), NotNull ] | |
public HierarchyProxy Hierarchy = new HierarchyProxy(); | |
[ DataContract( "HierarchyProxy" ) ] | |
public class HierarchyProxy | |
{ | |
[ DataMember( 0 ), NonIdentifiableCollectionItems, NotNull ] | |
public List<Entity> RootParts = new List<Entity>(); | |
[ DataMember( 1 ), NonIdentifiableCollectionItems, ItemNotNull, NotNull ] | |
public List<Wrapper> Parts = new List<Wrapper>(); | |
} | |
} | |
/// <summary> | |
/// SceneAsset.Hierarchy.Parts does not use Entity directly, | |
/// we're replicating that behavior through this class | |
/// </summary> | |
[ DataContract( "EntityWrapper" ) ] | |
public class Wrapper | |
{ | |
public Entity Entity; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment