com.artromskiy.deltaecs.unity 0.0.47 → 0.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.52
4
+
5
+ - Published with the matching DeltaECS NuGet packages.
6
+
7
+ ## 0.0.51
8
+
9
+ - Published with the matching DeltaECS NuGet packages.
10
+
3
11
  ## 0.0.47
4
12
 
5
13
  - Published with the matching DeltaECS NuGet packages.
package/README.md CHANGED
@@ -18,7 +18,7 @@ Add the npm-compatible registry and package to the Unity project's
18
18
  }
19
19
  ],
20
20
  "dependencies": {
21
- "com.artromskiy.deltaecs.unity": "0.0.47"
21
+ "com.artromskiy.deltaecs.unity": "0.0.52"
22
22
  }
23
23
  }
24
24
  ```
@@ -75,5 +75,29 @@ public struct Position
75
75
  }
76
76
  ```
77
77
 
78
+ ## Initialize views when they are bound
79
+
80
+ Implement `IEntityView` on a `MonoBehaviour` when a view needs to receive its
81
+ entity after binding. `OnBind` receives an `EntityContext` containing the
82
+ owning world and entity:
83
+
84
+ ```csharp
85
+ using Delta.ECS.Unity;
86
+ using UnityEngine;
87
+
88
+ public sealed class UnitView : MonoBehaviour, IEntityView
89
+ {
90
+ private EntityContext _context;
91
+
92
+ public void OnBind(EntityContext context) => _context = context;
93
+ }
94
+ ```
95
+
96
+ The binder calls `OnBind` for implementations on the view and its child
97
+ GameObjects, including inactive children. It runs after the view is associated
98
+ with the entity and after the ECS query has finished, so the callback can use
99
+ the regular `World` API. A stored context can later be checked with
100
+ `context.IsAlive` before using its entity.
101
+
78
102
  See the [DeltaECS documentation](https://github.com/Artromskiy/DeltaECS/wiki)
79
103
  for the runtime and generator APIs.
@@ -0,0 +1,121 @@
1
+ #nullable enable
2
+
3
+ using System;
4
+ using Delta.ECS;
5
+
6
+ namespace Delta.ECS.Unity
7
+ {
8
+ /// <summary>A stable entity handle paired with the world that owns it.</summary>
9
+ public readonly struct EntityContext
10
+ {
11
+ /// <summary>Gets the entity handle.</summary>
12
+ public Entity Entity { get; }
13
+
14
+ /// <summary>Gets the owning world, or <see langword="null"/> when this is the default context.</summary>
15
+ public World? World { get; }
16
+
17
+ /// <summary>Gets whether this context refers to a live entity.</summary>
18
+ public bool IsAlive => World != null && World.IsAlive(Entity);
19
+
20
+ /// <summary>Reports whether the entity owns the specified component registration.</summary>
21
+ public bool Has(ComponentId componentId) => World?.Has(Entity, componentId) ?? false;
22
+
23
+ /// <summary>Reports whether the entity owns the primary registration for <typeparamref name="T"/>.</summary>
24
+ public bool Has<T>() => World?.Has<T>(Entity) ?? false;
25
+
26
+ /// <summary>Reports whether the entity owns the specified registration of <typeparamref name="T"/>.</summary>
27
+ public bool Has<T>(ComponentId componentId) => World?.Has<T>(Entity, componentId) ?? false;
28
+
29
+ /// <summary>Attempts to read the primary component for <typeparamref name="T"/>.</summary>
30
+ public bool TryGet<T>(out T value)
31
+ {
32
+ World? world = World;
33
+ if (world is null)
34
+ {
35
+ value = default!;
36
+ return false;
37
+ }
38
+
39
+ return world.TryGet(Entity, out value);
40
+ }
41
+
42
+ /// <summary>Attempts to read the specified registration of <typeparamref name="T"/>.</summary>
43
+ public bool TryGet<T>(ComponentId componentId, out T value)
44
+ {
45
+ World? world = World;
46
+ if (world is null)
47
+ {
48
+ value = default!;
49
+ return false;
50
+ }
51
+
52
+ return world.TryGet(Entity, componentId, out value);
53
+ }
54
+
55
+ /// <summary>Reads the primary component for <typeparamref name="T"/> or throws when it is missing.</summary>
56
+ public T Get<T>() => GetWorld().Get<T>(Entity);
57
+
58
+ /// <summary>Reads the specified component registration as <typeparamref name="T"/> or throws when it is missing.</summary>
59
+ public T Get<T>(ComponentId componentId) => GetWorld().Get<T>(Entity, componentId);
60
+
61
+ /// <summary>Gets a writable reference to the primary component for <typeparamref name="T"/>.</summary>
62
+ public ref T GetRef<T>() => ref GetWorld().GetRef<T>(Entity);
63
+
64
+ /// <summary>Gets a writable reference to the specified registration of <typeparamref name="T"/>.</summary>
65
+ public ref T GetRef<T>(ComponentId componentId) => ref GetWorld().GetRef<T>(Entity, componentId);
66
+
67
+ /// <summary>Gets a read-only reference to the primary component for <typeparamref name="T"/>.</summary>
68
+ public ref readonly T GetReadRef<T>() => ref GetWorld().GetReadRef<T>(Entity);
69
+
70
+ /// <summary>Gets a read-only reference to the specified registration of <typeparamref name="T"/>.</summary>
71
+ public ref readonly T GetReadRef<T>(ComponentId componentId) => ref GetWorld().GetReadRef<T>(Entity, componentId);
72
+
73
+ /// <summary>Attempts to read the stamp for the primary component of <typeparamref name="T"/>.</summary>
74
+ public bool TryGetComponentStamp<T>(out Stamp stamp)
75
+ {
76
+ World? world = World;
77
+ if (world is null)
78
+ {
79
+ stamp = default;
80
+ return false;
81
+ }
82
+
83
+ return world.TryGetComponentStamp<T>(Entity, out stamp);
84
+ }
85
+
86
+ /// <summary>Attempts to read the stamp for the specified component registration.</summary>
87
+ public bool TryGetComponentStamp(ComponentId componentId, out Stamp stamp)
88
+ {
89
+ World? world = World;
90
+ if (world is null)
91
+ {
92
+ stamp = default;
93
+ return false;
94
+ }
95
+
96
+ return world.TryGetComponentStamp(Entity, componentId, out stamp);
97
+ }
98
+
99
+ /// <summary>Attempts to read the stamp for the specified registration of <typeparamref name="T"/>.</summary>
100
+ public bool TryGetComponentStamp<T>(ComponentId componentId, out Stamp stamp)
101
+ {
102
+ World? world = World;
103
+ if (world is null)
104
+ {
105
+ stamp = default;
106
+ return false;
107
+ }
108
+
109
+ return world.TryGetComponentStamp<T>(Entity, componentId, out stamp);
110
+ }
111
+
112
+ internal EntityContext(World world, Entity entity)
113
+ {
114
+ World = world;
115
+ Entity = entity;
116
+ }
117
+
118
+ private World GetWorld()
119
+ => World ?? UnityThrowHelpers.ThrowMissingEntityContextWorld();
120
+ }
121
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 7d9c2a99dcf04bc7a88de3c1468ef821
@@ -32,6 +32,7 @@ namespace Delta.ECS.Unity
32
32
  public World World => _world;
33
33
  internal IEcsWorld Integration => _integration;
34
34
  internal SceneData SceneData => _sceneData;
35
+ internal Scene UnityScene => _authoring != null ? _authoring.gameObject.scene : default;
35
36
  internal bool IsBound => !_disposed;
36
37
 
37
38
  /// <summary>Returns the integration-owned runtime context for a loaded Unity scene.</summary>
@@ -9,6 +9,23 @@ namespace Delta.ECS.Unity
9
9
  {
10
10
  public static class SceneWorldExtensions
11
11
  {
12
+ /// <summary>Returns the live ECS context bound to this GameObject, or the default context when unbound.</summary>
13
+ public static EntityContext GetEntity(this GameObject? view)
14
+ => TryGetEntity(view, out EntityContext context) ? context : default;
15
+
16
+ /// <summary>Returns whether this GameObject is bound to a live ECS entity.</summary>
17
+ public static bool TryGetEntity(this GameObject? view, out EntityContext context)
18
+ {
19
+ if (view != null && SceneWorld.TryGetBoundEntity(view, out World world, out Entity entity))
20
+ {
21
+ context = new EntityContext(world, entity);
22
+ return true;
23
+ }
24
+
25
+ context = default;
26
+ return false;
27
+ }
28
+
12
29
  /// <summary>Returns the entity bound to this GameObject, or the default entity when no binding exists.</summary>
13
30
  /// <param name="world">The bound live ECS world, or <see langword="null"/> when no binding exists.</param>
14
31
  public static Entity GetEntity(this GameObject? view, out World? world)
@@ -40,6 +57,22 @@ namespace Delta.ECS.Unity
40
57
  return TryGetEntity(behaviour, out Entity entity, out world) ? entity : default;
41
58
  }
42
59
 
60
+ /// <summary>Returns the live ECS context bound to this MonoBehaviour, or the default context when unbound.</summary>
61
+ public static EntityContext GetEntity(this MonoBehaviour? behaviour)
62
+ => TryGetEntity(behaviour, out EntityContext context) ? context : default;
63
+
64
+ /// <summary>Returns whether this MonoBehaviour is bound to a live ECS entity.</summary>
65
+ public static bool TryGetEntity(this MonoBehaviour? behaviour, out EntityContext context)
66
+ {
67
+ if (behaviour != null)
68
+ {
69
+ return behaviour.gameObject.TryGetEntity(out context);
70
+ }
71
+
72
+ context = default;
73
+ return false;
74
+ }
75
+
43
76
  /// <summary>Returns whether this MonoBehaviour is bound to a live ECS entity.</summary>
44
77
  public static bool TryGetEntity(
45
78
  this MonoBehaviour? behaviour,
@@ -1,6 +1,10 @@
1
+ #nullable enable
2
+
1
3
  using System;
2
4
  using System.Diagnostics.CodeAnalysis;
5
+ using System.Runtime.CompilerServices;
3
6
  using System.Runtime.ExceptionServices;
7
+ using Delta.ECS;
4
8
 
5
9
  namespace Delta.ECS.Unity
6
10
  {
@@ -40,5 +44,10 @@ namespace Delta.ECS.Unity
40
44
  ExceptionDispatchInfo.Capture(exception).Throw();
41
45
  return default;
42
46
  }
47
+
48
+ [DoesNotReturn]
49
+ [MethodImpl(MethodImplOptions.NoInlining)]
50
+ internal static World ThrowMissingEntityContextWorld()
51
+ => throw new InvalidOperationException("The default entity context does not reference a world.");
43
52
  }
44
53
  }
@@ -0,0 +1,119 @@
1
+ #nullable enable
2
+
3
+ using System.Collections.Generic;
4
+ using Delta.ECS;
5
+ using UnityEngine;
6
+ using UnityEngine.SceneManagement;
7
+
8
+ namespace Delta.ECS.Unity
9
+ {
10
+ /// <summary>Creates and binds prefab views for entities carrying an unbound <see cref="ViewToken"/>.</summary>
11
+ public sealed class ViewBindingSystem
12
+ {
13
+ private readonly SceneWorld _sceneWorld;
14
+ private readonly World _world;
15
+ private readonly ViewPrefabCatalog _catalog;
16
+ private readonly ComponentId _viewBoundId;
17
+ private readonly EcsOperation<ForEachEntityOperationInvoker> _bindOperation;
18
+ private readonly List<Entity> _entitiesToBind = new();
19
+ private readonly List<MonoBehaviour> _viewBehaviours = new();
20
+
21
+ /// <summary>Creates a view binder for one scene world and prefab catalog.</summary>
22
+ public ViewBindingSystem(SceneWorld sceneWorld, ViewPrefabCatalog catalog)
23
+ {
24
+ _sceneWorld = sceneWorld ?? UnityThrowHelpers.ThrowArgumentNull<SceneWorld>(nameof(sceneWorld));
25
+ _catalog = catalog ?? UnityThrowHelpers.ThrowArgumentNull<ViewPrefabCatalog>(nameof(catalog));
26
+ _world = _sceneWorld.World;
27
+
28
+ ComponentId viewTokenId = _world.Layouts.GetPrimary<ViewToken>();
29
+ _viewBoundId = _world.Layouts.GetPrimary<ViewBound>();
30
+ Query query = _world.WhereAll(stackalloc ComponentId[1] { viewTokenId })
31
+ .WhereNone(stackalloc ComponentId[1] { _viewBoundId });
32
+ _bindOperation = _world.ForEachEntity(in query, QueueViewBinding);
33
+ }
34
+
35
+ /// <summary>Binds views for currently unbound entities with a matching token.</summary>
36
+ public void BindPendingViews()
37
+ {
38
+ _entitiesToBind.Clear();
39
+ _bindOperation.Invoke();
40
+
41
+ for (int i = 0; i < _entitiesToBind.Count; i++)
42
+ {
43
+ BindView(_entitiesToBind[i]);
44
+ }
45
+
46
+ _entitiesToBind.Clear();
47
+ }
48
+
49
+ private void QueueViewBinding(EntityRef entityRef) => _entitiesToBind.Add(entityRef);
50
+
51
+ private void BindView(Entity entity)
52
+ {
53
+ if (!_world.IsAlive(entity) || _world.Has(entity, _viewBoundId))
54
+ {
55
+ return;
56
+ }
57
+
58
+ if (_sceneWorld.TryGetView(entity, out GameObject existingView))
59
+ {
60
+ _world.Add(entity, _viewBoundId);
61
+ NotifyViewBound(existingView, entity);
62
+ return;
63
+ }
64
+
65
+ ViewToken token = _world.Get<ViewToken>(entity);
66
+ if (!_catalog.TryGetPrefab(token.Id, out GameObject? prefab))
67
+ {
68
+ return;
69
+ }
70
+
71
+ Object instance = Object.Instantiate((Object)prefab);
72
+ GameObject? view = instance as GameObject;
73
+ if (view == null)
74
+ {
75
+ string instanceType = instance == null ? "null" : instance.GetType().ToString();
76
+ Debug.LogError(
77
+ $"View prefab '{token.Id}' ({prefab.GetType()}) instantiated as '{instanceType}'.",
78
+ prefab);
79
+ if (instance != null)
80
+ {
81
+ Object.Destroy(instance);
82
+ }
83
+
84
+ return;
85
+ }
86
+
87
+ Scene scene = _sceneWorld.UnityScene;
88
+ if (scene.IsValid() && scene.isLoaded && view.scene != scene)
89
+ {
90
+ SceneManager.MoveGameObjectToScene(view, scene);
91
+ }
92
+
93
+ if (!_sceneWorld.BindView(entity, view))
94
+ {
95
+ Object.Destroy(view);
96
+ return;
97
+ }
98
+
99
+ _world.Add(entity, _viewBoundId);
100
+ NotifyViewBound(view, entity);
101
+ }
102
+
103
+ private void NotifyViewBound(GameObject view, Entity entity)
104
+ {
105
+ _viewBehaviours.Clear();
106
+ view.GetComponentsInChildren(true, _viewBehaviours);
107
+ EntityContext context = new EntityContext(_world, entity);
108
+ for (int i = 0; i < _viewBehaviours.Count; i++)
109
+ {
110
+ if (_viewBehaviours[i] != null && _viewBehaviours[i] is IEntityView entityView)
111
+ {
112
+ entityView.OnBind(context);
113
+ }
114
+ }
115
+
116
+ _viewBehaviours.Clear();
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: a31d3098209444b0b8635a1874556c21
@@ -0,0 +1,28 @@
1
+ using Delta.ECS;
2
+
3
+ namespace Delta.ECS.Unity
4
+ {
5
+ /// <summary>Receives the entity context after a Unity view is bound to an entity.</summary>
6
+ public interface IEntityView
7
+ {
8
+ /// <summary>Initializes this view for its bound entity.</summary>
9
+ void OnBind(EntityContext context);
10
+ }
11
+
12
+ /// <summary>Selects the Unity prefab used to represent an entity.</summary>
13
+ [DeltaEcsComponent(SchemaId = 0x405B11F5A17E14A1UL)]
14
+ public readonly struct ViewToken
15
+ {
16
+ /// <summary>Gets the identifier of the view prefab in a <see cref="ViewPrefabCatalog"/>.</summary>
17
+ public string Id { get; }
18
+
19
+ /// <summary>Creates a token that selects a view prefab by identifier.</summary>
20
+ public ViewToken(string id) => Id = id;
21
+ }
22
+
23
+ /// <summary>Marks an entity whose Unity view has already been bound.</summary>
24
+ [DeltaEcsComponent(SchemaId = 0xB31CB42A81D3B164UL)]
25
+ public struct ViewBound
26
+ {
27
+ }
28
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 18a1b2ddb0f34526b96a93e733879150
@@ -0,0 +1,68 @@
1
+ #nullable enable
2
+
3
+ using System;
4
+ using System.Collections.Generic;
5
+ using System.Diagnostics.CodeAnalysis;
6
+ using UnityEngine;
7
+
8
+ namespace Delta.ECS.Unity
9
+ {
10
+ /// <summary>Maps <see cref="ViewToken.Id"/> values to Unity view prefabs.</summary>
11
+ [CreateAssetMenu(fileName = "ViewPrefabCatalog", menuName = "Delta ECS/View Prefab Catalog")]
12
+ public sealed class ViewPrefabCatalog : ScriptableObject
13
+ {
14
+ [SerializeField] private List<ViewPrefabEntry> _entries = new();
15
+ [NonSerialized] private Dictionary<string, GameObject>? _prefabsById;
16
+
17
+ /// <summary>Resolves a token identifier to its configured prefab.</summary>
18
+ public bool TryGetPrefab(string id, [NotNullWhen(true)] out GameObject? prefab)
19
+ {
20
+ EnsureIndex();
21
+ if (!string.IsNullOrEmpty(id)
22
+ && _prefabsById!.TryGetValue(id, out prefab)
23
+ && prefab != null)
24
+ {
25
+ return true;
26
+ }
27
+
28
+ prefab = null;
29
+ return false;
30
+ }
31
+
32
+ private void OnValidate() => _prefabsById = null;
33
+
34
+ private void EnsureIndex()
35
+ {
36
+ if (_prefabsById != null)
37
+ {
38
+ return;
39
+ }
40
+
41
+ _prefabsById = new Dictionary<string, GameObject>(StringComparer.Ordinal);
42
+ _entries ??= new List<ViewPrefabEntry>();
43
+ for (int i = 0; i < _entries.Count; i++)
44
+ {
45
+ ViewPrefabEntry entry = _entries[i];
46
+ if (entry == null || string.IsNullOrEmpty(entry.Id) || entry.Prefab == null)
47
+ {
48
+ continue;
49
+ }
50
+
51
+ if (!_prefabsById.TryAdd(entry.Id, entry.Prefab))
52
+ {
53
+ Debug.LogError($"Duplicate view prefab identifier '{entry.Id}' in '{name}'.", this);
54
+ }
55
+ }
56
+ }
57
+
58
+ [Serializable]
59
+ private sealed class ViewPrefabEntry
60
+ {
61
+ [SerializeField] private string _id = string.Empty;
62
+ [SerializeField] private GameObject? _prefab;
63
+
64
+ internal string Id => _id;
65
+ internal GameObject? Prefab => _prefab;
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 91f157d4af3b4bd6b4d5beb36a1337a6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.artromskiy.deltaecs.unity",
3
- "version": "0.0.47",
3
+ "version": "0.0.52",
4
4
  "displayName": "DeltaECS Unity",
5
5
  "description": "Unity scene authoring and editor integration for DeltaECS.",
6
6
  "unity": "6000.7",