This repository has been archived by the owner on Feb 4, 2020. It is now read-only.
/
MainContext.cs
349 lines (314 loc) · 17.6 KB
/
MainContext.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
using Svelto.ECS.Example.Survive.Characters.Enemies;
using Svelto.ECS.Example.Survive.Characters.Player;
using Svelto.ECS.Example.Survive.Characters.Player.Gun;
using Svelto.ECS.Example.Survive.HUD;
using Svelto.Context;
using Svelto.ECS.Example.Survive.Camera;
using Svelto.ECS.Example.Survive.Characters;
using Svelto.ECS.Example.Survive.Characters.Sounds;
using UnityEngine;
using Svelto.ECS.Schedulers.Unity;
using Svelto.Tasks;
//Main is the Application Composition Root.
//A Composition Root is the where all the dependencies are
//created and injected (I talk a lot about this in my articles)
//A composition root belongs to the Context, but
//a context can have more than a composition root.
//For example a factory is a composition root.
//Furthermore an application can have more than a context
//but this is more advanced and not part of this demo
namespace Svelto.ECS.Example.Survive
{
/// <summary>
///IComposition root is part of Svelto.Context
///Svelto.Context is not formally part of Svelto.ECS, but
///it's helpful to use in an environment where a Context is
///not formally present, like in Unity.
/// </summary>
public class Main : IUnityCompositionRoot
{
public Main()
{
InitAssets();
SetupEngines();
SetupEntities();
}
void InitAssets()
{
//Do not copy this. initially I thought it was a good idea to use
//Json serialization to replace resources, but it's less convenient
//than I thought
GameObject.Find("PrefabsSerializer").GetComponent<PrefabSerializer>().Init();
GameObject.Find("EnemySpawnerData").GetComponent<SpawningData>().Init();
}
/// <summary>
/// Before to start a review of Svelto.ECS terminologies:
/// - Entity:
/// it must be a real and concrete entity that you can explain
/// in terms of game design. The name of each entity should reflect
/// a specific concept from the game design domain
/// - Engines (Systems):
/// Where all the logic lies. Engines operates on EntityViews or EntityStructs
/// - EntityViews and EntitiyViewStructs:
/// EntityViews maps Entity Components. The Engines can't
/// access directly to each entity (as a single set of components), but
/// through component sets defined by the EntityView.
/// They act as component filters and expose only the entity components
/// that the Engine is interested in.
/// EntityViews are actually defined with the Engine so they
/// come together with the engine and in the same namespace of the engine.
/// EntityViewStructs should always be used, while EntityViews as
/// class use should be considered an exception.
/// - Component Interfaces:
/// Components must be seen as data holders. There may be implementation
/// exceptions, but the interface must declare a group
/// of readable and/or writeable data.
/// In Svelto.ECS components are always interfaces declaring
/// Setters and Getters of Value Types. DispatchOnSet
/// and DispatchOnChange must not be seen as events, but
/// as pushing of data instead of data polling, similar
/// to the concept of DataBinding.
/// - Implementors:
/// Being components interfaces, they must be implemented through
/// Implementors. The relation Implementors to Components
/// is not 1:1 so that you can group several
/// components into fewer implementors. This allows to easily
/// share data between components. Implementors also act
/// as bridge between the platform and Svelto.ECS.
/// Since Components can hold only value types, Implementors
/// are the objects that can interact directly with the platform
/// objects, I.E.: RigidBody, Transform and so on.
/// Note: IComponents must hold only valuetypes for
/// code design purposes and not optmization purposes.
/// The reason is that all the logic must lie in the engines
/// so Components cannot hold references to instances that can
/// expose functions with logic.
/// - EntityStructs:
/// In order to write Data Oriented Cache Friendly and allocation 0 code, Svelto.ECS
/// also supports EntityStructs.
/// - EntityDescriptors:
/// Gives a way to formalize your Entity in svelto.ECS, it also
/// defoines the EntityViews, EntityStructs and EntityViewStructs that must be generated once the
/// Entity is built
/// </summary>
void SetupEngines()
{
//The Engines Root is the core of Svelto.ECS. You must NEVER inject the EngineRoot
//as it is, therefore the composition root must hold a reference or it will be
//GCed.
//the UnitySumbmissionEntityViewScheduler is the scheduler that is used by the EnginesRoot to know
//when to inject the EntityViews. You shouldn't use a custom one unless you know what you
//are doing or you are not working with Unity.
_enginesRoot = new EnginesRoot(new UnityEntitySubmissionScheduler());
//Engines root can never be held by anything else than the context itself to avoid leaks
//That's why the EntityFactory and EntityFunctions are generated.
//The EntityFactory can be injected inside factories (or engine acting as factories)
//to build new entities dynamically
_entityFactory = _enginesRoot.GenerateEntityFactory();
//The entity functions is a set of utility operations on Entities, including
//removing an entity. I couldn't find a better name so far.
var entityFunctions = _enginesRoot.GenerateEntityFunctions();
//the ISequencer is one of the 2 official ways available in Svelto.ECS
//to communicate. They are mainly used for two specific cases:
//1) specify a strict execution order between engines (engine logic
//is executed horizontally instead than vertically, I will talk about this
//in my articles). 2) filter a data token passed as parameter through
//engines. The ISequencer is also not the common way to communicate
//between engines
PlayerDeathSequencer playerDeathSequence = new PlayerDeathSequencer();
EnemyDeathSequencer enemyDeathSequence = new EnemyDeathSequencer();
//wrap non testable unity static classes, so that
//can be mocked if needed.
IRayCaster rayCaster = new RayCaster();
ITime time = new Time();
//Player related engines. ALL the dependencies must be solved at this point
//through constructor injection.
var playerShootingEngine = new PlayerGunShootingEngine(rayCaster, time);
var playerMovementEngine = new PlayerMovementEngine(rayCaster, time);
var playerAnimationEngine = new PlayerAnimationEngine();
var playerDeathEngine = new PlayerDeathEngine(playerDeathSequence, entityFunctions);
//Enemy related engines
var enemyAnimationEngine = new EnemyAnimationEngine(time, enemyDeathSequence, entityFunctions);
var enemyAttackEngine = new EnemyAttackEngine(time);
var enemyMovementEngine = new EnemyMovementEngine();
//GameObjectFactory allows to create GameObjects without using the Static
//method GameObject.Instantiate. While it seems a complication
//it's important to keep the engines testable and not
//coupled with hard dependencies references (read my articles to understand
//how dependency injection works and why solving dependencies
//with static classes and singletons is a terrible mistake)
GameObjectFactory factory = new GameObjectFactory();
//Factory is one of the few patterns that work very well with ECS. Its use is highly encouraged
IEnemyFactory enemyFactory = new EnemyFactory(factory, _entityFactory);
var enemySpawnerEngine = new EnemySpawnerEngine(enemyFactory, entityFunctions);
var enemyDeathEngine = new EnemyDeathEngine(entityFunctions, enemyDeathSequence);
//hud and sound engines
var hudEngine = new HUDEngine(time);
var damageSoundEngine = new DamageSoundEngine();
var scoreEngine = new ScoreEngine();
//The ISequencer implementation is very simple, but allows to perform
//complex concatenation including loops and conditional branching.
//These two sequencers are a real stretch and are shown only for explanatory purposes.
//Please do not see sequencers as a way to dispatch or broadcast events, they are meant only and exclusively
//to guarantee the order of execution of the involved engines.
//For this reason the use of sequencers is and must be actually rare, as perfectly encapsulated engines
//do not need to be executed in specific order.
//a Sequencer can:
//- ensure the order of execution through one step only (one step executes in order several engines)
//- ensure the order of execution through several steps. Each engine inside each step has the responsibility
//to trigger the next step through the use of the Next() function
//- create paths with branches and loop using the Condition parameter.
playerDeathSequence.SetSequence(
new Steps //sequence of steps, this is a dictionary!
{
{
/*from: */playerDeathEngine, //when the player dies
/*to: */new To<PlayerDeathCondition>
//all these engines in the list will be called in order (which in this
//case was not important at all, so stretched!!)
{
{
PlayerDeathCondition.Death, playerMovementEngine, playerAnimationEngine,
enemyAnimationEngine, damageSoundEngine, hudEngine
}
}
}
}
);
enemyDeathSequence.SetSequence(
new Steps
{
{ //first step
enemyDeathEngine,
new To<EnemyDeathCondition>
{
//TIP: use GO To Type Declaration to go directly to the Class code of the
//engine instance
{ EnemyDeathCondition.Death, scoreEngine }
}
},
{ //second step
enemyAnimationEngine, //after the death animation is actually finished
new To<EnemyDeathCondition>
{
{ EnemyDeathCondition.Death, enemySpawnerEngine }//call the spawner engine
}
}
}
);
//All the logic of the game must lie inside engines
//Player engines
_enginesRoot.AddEngine(playerMovementEngine);
_enginesRoot.AddEngine(playerAnimationEngine);
_enginesRoot.AddEngine(playerShootingEngine);
_enginesRoot.AddEngine(new PlayerInputEngine());
_enginesRoot.AddEngine(new PlayerGunShootingFXsEngine());
_enginesRoot.AddEngine(playerDeathEngine);
//enemy engines
_enginesRoot.AddEngine(enemySpawnerEngine);
_enginesRoot.AddEngine(enemyAttackEngine);
_enginesRoot.AddEngine(enemyMovementEngine);
_enginesRoot.AddEngine(enemyAnimationEngine);
_enginesRoot.AddEngine(enemyDeathEngine);
//other engines
_enginesRoot.AddEngine(new ApplyingDamageToTargetsEngine());
_enginesRoot.AddEngine(new CameraFollowTargetEngine(time));
_enginesRoot.AddEngine(new CharactersDeathEngine());
_enginesRoot.AddEngine(damageSoundEngine);
_enginesRoot.AddEngine(hudEngine);
_enginesRoot.AddEngine(scoreEngine);
}
/// <summary>
/// While until recently I thought that creating entities in the context would be all right, I am coming to
/// realise that engines should always handle the creation of entities. I will refactor this when the right
/// time comes.
/// </summary>
void SetupEntities()
{
BuildPlayerEntities();
BuildCameraEntity();
}
public void OnContextCreated(UnityContext contextHolder)
{
BuildEntitiesFromScene(contextHolder);
}
void BuildPlayerEntities()
{
var prefabsDictionary = new PrefabsDictionary();
var player = prefabsDictionary.Istantiate("Player");
//Building entities explicitly should be always preferred and MUST be used if an implementor doesn't need to
//be a Monobehaviour. You should strive to create implementors not as monobehaviours. Implementors as
//monobehaviours are meant only to function as bridge between Svelto.ECS and Unity3D. Using implementor as
//monobehaviour just to read serialized data from the editor, is also a bad practice, use a Json file
//instead. The Player Entity is made of EntityViewStruct+Implementors as monobehaviours and EntityStructs.
//The PlayerInputDataStruct doesn't need to be initialized (yay!!) but the HealthEntityStruct does.
//Here I show the official method to do it
var initializer = _entityFactory.BuildEntity<PlayerEntityDescriptor>(player.GetInstanceID(), ECSGroups.Player, player.GetComponents<IImplementor>());
initializer.Init(new HealthEntityStruct {currentHealth = 100});
//unluckily the gun is parented in the original prefab, so there is no easy way to create it explicitly, I
//have to create if from the existing gameobject.
var gun = player.GetComponentInChildren<PlayerShootingImplementor>();
_entityFactory.BuildEntity<PlayerGunEntityDescriptor>(gun.gameObject.GetInstanceID(), ECSGroups.Player, new[] {gun});
}
void BuildCameraEntity()
{
var implementor = UnityEngine.Camera.main.gameObject.AddComponent<CameraImplementor>();
_entityFactory.BuildEntity<CameraEntityDescriptor>(UnityEngine.Camera.main.GetInstanceID(), ECSGroups.ExtraStuff, new[] {implementor});
}
/// <summary>
/// This is a possible approach to create Entities from already existing GameObject in the scene
/// It is absolutely not necessary and I wouldn't rely on this in production
/// </summary>
/// <param name="contextHolder"></param>
void BuildEntitiesFromScene(UnityContext contextHolder)
{
//An EntityDescriptorHolder is a special Svelto.ECS class created to exploit
//GameObjects to dynamically retrieve the Entity information attached to it.
//Basically a GameObject can be used to hold all the information needed to create
//an Entity and later queries to build the entitity itself.
//This allows to trigger a sort of polymorphic code that can be re-used to
//create several type of entities.
IEntityDescriptorHolder[] entities = contextHolder.GetComponentsInChildren<IEntityDescriptorHolder>();
//However this common pattern in Svelto.ECS application exists to automatically
//create entities from gameobjects already presented in the scene.
//I still suggest to avoid this method though and create entities always
//manually and explicitly. Basically EntityDescriptorHolder should be avoided
//whenever not strictly necessary.
for (int i = 0; i < entities.Length; i++)
{
var entityDescriptorHolder = entities[i];
var entityViewsToBuild = entityDescriptorHolder.GetDescriptor();
_entityFactory.BuildEntity
(new EGID(((MonoBehaviour) entityDescriptorHolder).gameObject.GetInstanceID(), ECSGroups.ExtraStuff),
entityViewsToBuild,
(entityDescriptorHolder as MonoBehaviour).GetComponentsInChildren<IImplementor>());
}
}
public void OnContextInitialized()
{}
//part of Svelto.Context
public void OnContextDestroyed()
{ //final clean up
_enginesRoot.Dispose();
//Tasks can run across level loading, so if you don't want
//that, the runners must be stopped explicitily.
//carefull because if you don't do it and
//unintentionally leave tasks running, you will cause leaks
TaskRunner.StopAndCleanupAllDefaultSchedulers();
}
EnginesRoot _enginesRoot;
IEntityFactory _entityFactory;
}
public class PlayerDeathSequencer : Sequencer
{}
public class EnemyDeathSequencer : Sequencer
{}
/// <summary>
///At least One GameObject containing a UnityContext must be present in the scene.
///All the monobehaviours existing in gameobjects child of the UnityContext one,
///can be later queried, usually to create entities from statically created
///gameobjects.
/// </summary>
public class MainContext : UnityContext<Main>
{ }
}