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