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