1 /** represents a set of things to be drawn to screen */
2 
3 module re.ng.scene;
4 
5 import re;
6 import std.string;
7 import re.ecs;
8 import re.gfx;
9 import re.math;
10 import re.ng.manager;
11 import std.typecons;
12 import std.range;
13 static import raylib;
14 
15 public {
16     import re.time;
17     import re.ng.scene2d;
18     import re.ng.scene3d;
19 }
20 
21 /// represents a collection of entities that draw to a texture
22 abstract class Scene {
23     /// the cleared background color
24     public raylib.Color clear_color = Colors.WHITE;
25     /// the entity manager
26     public EntityManager ecs;
27     /// the render target
28     public RenderTarget render_target;
29     private Vector2 _resolution;
30     /// the mode of compositing
31     public CompositeMode composite_mode;
32     /// postprocessors effects
33     public PostProcessor[] postprocessors;
34     /// updatable managers
35     public Manager[] managers;
36 
37     /// the mode for compositing a scene onto the display buffer
38     public struct CompositeMode {
39         /// the texture render tint color
40         raylib.Color color = raylib.Colors.WHITE;
41     }
42 
43     /// creates a new scene
44     this() {
45     }
46 
47     /// gets the render resolution. initialized to Core.default_resolution
48     @property Vector2 resolution() {
49         return _resolution;
50     }
51 
52     /// sets the render resolution and updates the render target
53     @property Vector2 resolution(Vector2 value) {
54         _resolution = value;
55         update_render_target();
56         return value;
57     }
58 
59     /// called at the start of the scene
60     protected void on_start() {
61 
62     }
63 
64     /// called right before cleanup
65     protected void unload() {
66 
67     }
68 
69     void update_updatable(Component component) {
70         auto updatable = cast(Updatable) component;
71         updatable.update();
72     }
73 
74     /// called internally to update ecs. can be overridden, but super.update() must be called.
75     public void update() {
76         // update ecs
77         ecs.update();
78 
79         // update managers
80         foreach (manager; managers) {
81             manager.update();
82         }
83 
84         // update components
85         foreach (component; ecs.storage.updatable_components) {
86             update_updatable(component);
87         }
88         foreach (component; ecs.storage.updatable_renderable_components) {
89             update_updatable(component);
90         }
91     }
92 
93     /// called internally to render ecs
94     public void render() {
95         raylib.BeginTextureMode(render_target);
96         raylib.ClearBackground(clear_color);
97 
98         render_scene();
99 
100         raylib.EndTextureMode();
101     }
102 
103     /// run postprocessors
104     public void post_render() {
105         import std.algorithm : filter;
106         import std.array : array;
107 
108         auto pipeline = postprocessors.filter!(x => x.enabled).array;
109         // skip if no postprocessors
110         if (pipeline.length == 0)
111             return;
112 
113         pipeline[0].process(render_target);
114         auto last_buf = pipeline[0].buffer;
115         for (auto i = 1; i < pipeline.length; i++) {
116             auto postprocessor = pipeline[i];
117             postprocessor.process(last_buf);
118             last_buf = postprocessor.buffer;
119         }
120         // draw the last buf in the chain to the main texture
121         RenderExt.draw_render_target_from(last_buf, render_target);
122     }
123 
124     protected abstract void render_scene();
125 
126     /// may optionally be used to render global things from a scene
127     protected void render_hook() {
128     }
129 
130     private void update_render_target() {
131         if (Core.headless)
132             return;
133         // free any old render target
134         if (render_target != RenderTarget.init) {
135             RenderExt.destroy_render_target(render_target);
136         }
137         // create render target
138         // TODO: use scene resolution instead of window resolution
139         render_target = RenderExt.create_render_target(cast(int) resolution.x, cast(int) resolution.y);
140         // apply texture filter
141         raylib.SetTextureFilter(render_target.texture, Core.default_filter_mode);
142     }
143 
144     /// called internally on scene creation
145     public void begin() {
146         setup();
147 
148         on_start();
149     }
150 
151     /// setup that hapostprocessorsens after begin, but before the child scene starts
152     protected void setup() {
153         // set up ecs
154         ecs = new EntityManager;
155 
156         resolution = Core.default_resolution;
157     }
158 
159     /// called internally on scene destruction
160     public void end() {
161         unload();
162 
163         ecs.destroy();
164         ecs = null;
165 
166         foreach (postprocessor; postprocessors) {
167             postprocessor.destroy();
168         }
169         postprocessors = [];
170 
171         foreach (manager; managers) {
172             manager.destroy();
173         }
174 
175         if (!Core.headless) {
176             // free render target
177             RenderExt.destroy_render_target(render_target);
178         }
179     }
180 
181     public Nullable!T get_manager(T)() {
182         import std.algorithm.searching : find;
183 
184         // find a manager matching the type
185         auto matches = managers.find!(x => (cast(T) x) !is null);
186         if (matches.length > 0) {
187             return Nullable!T(cast(T) matches.front);
188         }
189         return Nullable!T.init;
190     }
191 
192     /// adds a manager to this scene
193     public T add_manager(T)(T manager) {
194         managers ~= manager;
195         manager.scene = this;
196         manager.setup();
197         return manager;
198     }
199 
200     // - ecs
201 
202     /// create an entity given a name
203     public Entity create_entity(string name) {
204         auto nt = ecs.create_entity();
205         nt.name = name;
206         nt.scene = this;
207         return nt;
208     }
209 
210     /// create an entity given a name and a 2d position
211     public Entity create_entity(string name, Vector2 pos = Vector2(0, 0)) {
212         auto nt = create_entity(name);
213         nt.position2 = pos;
214         return nt;
215     }
216 
217     /// create an entity given a name and a 3d position
218     public Entity create_entity(string name, Vector3 pos = Vector3(0, 0, 0)) {
219         auto nt = create_entity(name);
220         nt.position = pos;
221         return nt;
222     }
223 
224     public Entity get_entity(string name) {
225         return ecs.get_entity(name);
226     }
227 }
228 
229 @("scene-lifecycle")
230 unittest {
231     class TestScene : Scene2D {
232         override void on_start() {
233             auto apple = create_entity("apple");
234             assert(get_entity("apple") == apple, "could not get entity by name");
235         }
236     }
237 
238     Core.headless = true;
239     auto scene = new TestScene();
240     scene.begin();
241     scene.update();
242     scene.end();
243 }
244 
245 @("scene-load")
246 unittest {
247     class TestScene : Scene2D {
248     }
249 
250     Core.headless = true;
251 
252     auto scene = new TestScene();
253     Core.load_scenes([scene]);
254     assert(Core.get_scene!TestScene == scene);
255 }
256 
257 /// create a test game, with a test scene, and update it
258 @("scene-full")
259 unittest {
260     import re.util.test : TestGame;
261 
262     static class TestScene : Scene2D {
263         class Plant : Component, Updatable {
264             public int height = 0;
265 
266             void update() {
267                 height++;
268             }
269         }
270 
271         override void on_start() {
272             // create a basic entity
273             auto nt = create_entity("apple");
274             // add a basic component
275             nt.add_component(new Plant());
276         }
277     }
278 
279     auto my_scene = new TestScene();
280 
281     class Game : TestGame {
282         override void initialize() {
283             load_scenes([my_scene]);
284         }
285     }
286 
287     auto game = new Game();
288     game.run();
289 
290     // make sure scene is accessible
291     assert(game.primary_scene == my_scene, "primary scene does not match loaded scene");
292 
293     // make sure components worked
294     assert(my_scene.get_entity("apple").get_component!(TestScene.Plant)()
295             .height > 0, "test Updatable was not updated");
296 
297     game.destroy(); // clean up
298 
299     // make sure scene is cleaned up
300     assert(my_scene.ecs is null, "scene was not cleaned up");
301 }