1 module prova.core.scene;
2 
3 import derelict.openal.al;
4 import prova;
5 
6 ///
7 class Scene
8 {
9   ///
10   public Camera camera;
11   package(prova) AudioSource[] audioSources;
12   package(prova) SpacialMap2D collider2DMap;
13   package Game _game;
14   package bool isSetup;
15   private Entity[] rootEntities;
16   private Entity[] allEntities;
17 
18   ///
19   this()
20   {
21     camera = new Camera();
22     collider2DMap = new SpacialMap2D();
23 
24     addEntity(camera);
25   }
26 
27   ///
28   final @property Game game()
29   {
30     assert(_game, "Scene should be attached to a game");
31 
32     return _game;
33   }
34 
35   ///
36   final @property Input input()
37   {
38     return game.input;
39   }
40 
41   ///
42   final void addEntity(Entity entity)
43   {
44     assert(entity._scene != this, "Entity should not already be included in this scene");
45 
46     if(!entity.parent)
47       rootEntities ~= entity;
48 
49     allEntities ~= entity;
50 
51     entity._scene = this;
52 
53     foreach(Entity child; entity.children)
54       if(child._scene != this)
55         addEntity(child);
56 
57     foreach(AudioSource source; entity.audioSources)
58       audioSources ~= source;
59 
60     collider2DMap.add(entity.colliders2d);
61 
62     if(!entity.isSetup) {
63       entity.setup();
64       entity.isSetup = true;
65     }
66 
67     entity.start();
68   }
69 
70   ///
71   final void removeEntity(Entity entity)
72   {
73     assert(entity._scene == this, "Entity should already be added to this scene");
74 
75     disassociateEntity(entity);
76 
77     // detach from parent to not be drawn
78     if(entity.parent)
79       entity.parent.detach(entity);
80   }
81 
82   package void disassociateEntity(Entity entity)
83   {
84     foreach(Entity child; entity.children)
85       disassociateEntity(entity);
86 
87     foreach(AudioSource source; entity.audioSources) {
88       audioSources = audioSources.removeElement(source);
89     }
90 
91     collider2DMap.remove(entity.colliders2d);
92 
93     if(!entity.parent)
94       rootEntities = rootEntities.removeElement(entity);
95 
96     allEntities = allEntities.removeElement(entity);
97 
98     entity._scene = null;
99   }
100 
101   /// Finds the closest entity to this entity
102   Entity findClosestEntity(Entity entity)
103   {
104     return findClosestEntity(entity, 0, false);
105   }
106 
107   /// Finds the closest entity with the matching tag
108   Entity findClosestEntity(Entity entity, int tag)
109   {
110     return findClosestEntity(entity, tag, true);
111   }
112 
113   private Entity findClosestEntity(Entity entity, int tag, bool needsTag)
114   {
115     Vector3 worldPosition = entity.getWorldPosition();
116     float closestDistance = -1;
117     Entity closestEntity = null;
118 
119     foreach(Entity other; allEntities)
120     {
121       // make sure we aren't matching with self
122       if(other == entity)
123         continue;
124 
125       // make sure tag matches
126       if(needsTag && !other.hasTag(tag))
127         continue;
128 
129       const float distance = worldPosition.distanceTo(other.getWorldPosition());
130 
131       if(distance < closestDistance || closestDistance == -1) {
132         closestDistance = distance;
133         closestEntity = other;
134       }
135     }
136 
137     return closestEntity;
138   }
139 
140   /// Called when attached to a scene for the first time
141   void setup() { }
142   /// Called when attached to a scene
143   void start() { }
144 
145   /// Call super.update() to update entities if overridden
146   void update()
147   {
148     updateEntities();
149     updateCollisions();
150   }
151 
152   /// Called by Scene.update()
153   void updateEntities()
154   {
155     foreach(Entity entity; allEntities) {
156       entity.update();
157       entity.position += entity.velocity;
158       entity.velocity *= 1 - entity.friction;
159     }
160   }
161 
162   /// Called by Scene.update()
163   void updateCollisions()
164   {
165     collider2DMap.mapColliders();
166     collider2DMap.markCollisions();
167     collider2DMap.resolveCollisions();
168   }
169 
170   package void updateAudio()
171   {
172     Vector3 position = camera.getWorldPosition() / Audio.scale;
173     Quaternion rotation = camera.getWorldRotation();
174 
175     Vector3[] orientation = [
176       rotation * Vector3(0, 0, -1), // forward
177       rotation * Vector3(0, 1, 0) // up
178     ];
179 
180     alListener3f(AL_POSITION, position.x, position.y, position.z);
181     alListenerfv(AL_ORIENTATION, cast(float*) orientation.ptr);
182 
183     foreach(AudioSource source; audioSources)
184       source.update();
185   }
186 
187   /**
188    * All draw operations performed here will be affected by the camera
189    *
190    * Call super.draw(renderTarget) to render entities if overridden
191    */
192   void draw(RenderTarget renderTarget)
193   {
194     import std.algorithm : filter, sort;
195     import std.array : array;
196 
197     const auto sortDelegate = camera.getSortDelegate();
198 
199     auto sortedEntities = rootEntities.sort!(sortDelegate);
200 
201     foreach(Entity entity; sortedEntities) {
202       entity.draw(renderTarget, entity.getLocalTransformMatrix());
203     }
204   }
205 
206   /**
207    * All draw operations here will be affected by a static orthographic perspective
208    *
209    * Origin is moved to the bottom left of the window
210    */
211   void drawStatic(RenderTarget renderTarget) { }
212 }