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     /// recreate the render target
131     private void update_render_target() {
132         if (Core.headless)
133             return;
134         // free any old render target
135         if (render_target != RenderTarget.init) {
136             RenderExt.destroy_render_target(render_target);
137         }
138         // create render target
139         // TODO: use scene resolution instead of window resolution
140         render_target = RenderExt.create_render_target(cast(int) resolution.x, cast(int) resolution
141                 .y);
142         Core.log.info(format("recreated render target of size %s", resolution));
143         // apply texture filter
144         raylib.SetTextureFilter(render_target.texture, Core.default_filter_mode);
145     }
146 
147     /// called internally on scene creation
148     public void begin() {
149         setup();
150 
151         on_start();
152     }
153 
154     /// setup that hapostprocessorsens after begin, but before the child scene starts
155     protected void setup() {
156         // set up ecs
157         ecs = new EntityManager;
158 
159         resolution = Core.default_resolution;
160     }
161 
162     /// called internally on scene destruction
163     public void end() {
164         unload();
165 
166         ecs.destroy();
167         ecs = null;
168 
169         foreach (postprocessor; postprocessors) {
170             postprocessor.destroy();
171         }
172         postprocessors = [];
173 
174         foreach (manager; managers) {
175             manager.destroy();
176         }
177 
178         if (!Core.headless) {
179             // free render target
180             RenderExt.destroy_render_target(render_target);
181         }
182     }
183 
184     /// window resize event
185     void on_window_resized() {
186         // if the option is enabled, resize the render target to the new window size
187         if (Core.sync_render_window_resolution) {
188             resolution = Core.default_resolution;
189         }
190     }
191 
192     public Nullable!T get_manager(T)() {
193         import std.algorithm.searching : find;
194 
195         // find a manager matching the type
196         auto matches = managers.find!(x => (cast(T) x) !is null);
197         if (matches.length > 0) {
198             return Nullable!T(cast(T) matches.front);
199         }
200         return Nullable!T.init;
201     }
202 
203     /// adds a manager to this scene
204     public T add_manager(T)(T manager) {
205         managers ~= manager;
206         manager.scene = this;
207         manager.setup();
208         return manager;
209     }
210 
211     // - ecs
212 
213     /// create an entity given a name
214     public Entity create_entity(string name) {
215         auto nt = ecs.create_entity();
216         nt.name = name;
217         nt.scene = this;
218         return nt;
219     }
220 
221     /// create an entity given a name and a 2d position
222     public Entity create_entity(string name, Vector2 pos = Vector2(0, 0)) {
223         auto nt = create_entity(name);
224         nt.position2 = pos;
225         return nt;
226     }
227 
228     /// create an entity given a name and a 3d position
229     public Entity create_entity(string name, Vector3 pos = Vector3(0, 0, 0)) {
230         auto nt = create_entity(name);
231         nt.position = pos;
232         return nt;
233     }
234 
235     public Entity get_entity(string name) {
236         return ecs.get_entity(name);
237     }
238 }
239 
240 @("scene-lifecycle")
241 unittest {
242     class TestScene : Scene2D {
243         override void on_start() {
244             auto apple = create_entity("apple");
245             assert(get_entity("apple") == apple, "could not get entity by name");
246         }
247     }
248 
249     Core.headless = true;
250     auto scene = new TestScene();
251     scene.begin();
252     scene.update();
253     scene.end();
254 }
255 
256 @("scene-load")
257 unittest {
258     class TestScene : Scene2D {
259     }
260 
261     Core.headless = true;
262 
263     auto scene = new TestScene();
264     Core.load_scenes([scene]);
265     assert(Core.get_scene!TestScene == scene);
266 }
267 
268 /// create a test game, with a test scene, and update it
269 @("scene-full")
270 unittest {
271     import re.util.test : TestGame;
272 
273     static class TestScene : Scene2D {
274         class Plant : Component, Updatable {
275             public int height = 0;
276 
277             void update() {
278                 height++;
279             }
280         }
281 
282         override void on_start() {
283             // create a basic entity
284             auto nt = create_entity("apple");
285             // add a basic component
286             nt.add_component(new Plant());
287         }
288     }
289 
290     auto my_scene = new TestScene();
291 
292     class Game : TestGame {
293         override void initialize() {
294             load_scenes([my_scene]);
295         }
296     }
297 
298     auto game = new Game();
299     game.run();
300 
301     // make sure scene is accessible
302     assert(game.primary_scene == my_scene, "primary scene does not match loaded scene");
303 
304     // make sure components worked
305     assert(my_scene.get_entity("apple").get_component!(TestScene.Plant)()
306             .height > 0, "test Updatable was not updated");
307 
308     game.destroy(); // clean up
309 
310     // make sure scene is cleaned up
311     assert(my_scene.ecs is null, "scene was not cleaned up");
312 }