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     /// adds a manager to this scene
184     public T add_manager(T)(T manager) {
185         managers ~= manager;
186         manager.scene = this;
187         manager.setup();
188         return manager;
189     }
190 
191     // - ecs
192 
193     /// create an entity given a name
194     public Entity create_entity(string name) {
195         auto nt = ecs.create_entity();
196         nt.name = name;
197         nt.scene = this;
198         return nt;
199     }
200 
201     /// create an entity given a name and a 2d position
202     public Entity create_entity(string name, Vector2 pos = Vector2(0, 0)) {
203         auto nt = create_entity(name);
204         nt.position2 = pos;
205         return nt;
206     }
207 
208     /// create an entity given a name and a 3d position
209     public Entity create_entity(string name, Vector3 pos = Vector3(0, 0, 0)) {
210         auto nt = create_entity(name);
211         nt.position = pos;
212         return nt;
213     }
214 
215     public Entity get_entity(string name) {
216         return ecs.get_entity(name);
217     }
218 }
219 
220 @("scene-lifecycle")
221 unittest {
222     class TestScene : Scene2D {
223         override void on_start() {
224             auto apple = create_entity("apple");
225             assert(get_entity("apple") == apple, "could not get entity by name");
226         }
227     }
228 
229     Core.headless = true;
230     auto scene = new TestScene();
231     scene.begin();
232     scene.update();
233     scene.end();
234 }
235 
236 @("scene-load")
237 unittest {
238     class TestScene : Scene2D {
239     }
240 
241     Core.headless = true;
242 
243     auto scene = new TestScene();
244     Core.load_scenes([scene]);
245     assert(Core.get_scene!TestScene == scene);
246 }
247 
248 /// create a test game, with a test scene, and update it
249 @("scene-full")
250 unittest {
251     import re.util.test : TestGame;
252 
253     static class TestScene : Scene2D {
254         class Plant : Component, Updatable {
255             public int height = 0;
256 
257             void update() {
258                 height++;
259             }
260         }
261 
262         override void on_start() {
263             // create a basic entity
264             auto nt = create_entity("apple");
265             // add a basic component
266             nt.add_component(new Plant());
267         }
268     }
269 
270     auto my_scene = new TestScene();
271 
272     class Game : TestGame {
273         override void initialize() {
274             load_scenes([my_scene]);
275         }
276     }
277 
278     auto game = new Game();
279     game.run();
280 
281     // make sure scene is accessible
282     assert(game.primary_scene == my_scene, "primary scene does not match loaded scene");
283 
284     // make sure components worked
285     assert(my_scene.get_entity("apple").get_component!(TestScene.Plant)()
286             .height > 0, "test Updatable was not updated");
287 
288     game.destroy(); // clean up
289 
290     // make sure scene is cleaned up
291     assert(my_scene.ecs is null, "scene was not cleaned up");
292 }