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     void update_updatable(Component component) {
68         auto updatable = cast(Updatable) component;
69         updatable.update();
70     }
71 
72     /// called internally to update ecs. can be overridden, but super.update() must be called.
73     public void update() {
74         // update ecs
75         ecs.update();
76 
77         // update managers
78         foreach (manager; managers) {
79             manager.update();
80         }
81 
82         // update components
83         foreach (component; ecs.storage.updatable_components) {
84             update_updatable(component);
85         }
86         foreach (component; ecs.storage.updatable_renderable_components) {
87             update_updatable(component);
88         }
89     }
90 
91     /// called internally to render ecs
92     public void render() {
93         raylib.BeginTextureMode(render_target);
94         raylib.ClearBackground(clear_color);
95 
96         render_scene();
97 
98         raylib.EndTextureMode();
99     }
100 
101     /// run postprocessors
102     public void post_render() {
103         import std.algorithm : filter;
104         import std.array : array;
105 
106         auto pipeline = postprocessors.filter!(x => x.enabled).array;
107         // skip if no postprocessors
108         if (pipeline.length == 0)
109             return;
110 
111         pipeline[0].process(render_target);
112         auto last_buf = pipeline[0].buffer;
113         for (auto i = 1; i < pipeline.length; i++) {
114             auto postprocessor = pipeline[i];
115             postprocessor.process(last_buf);
116             last_buf = postprocessor.buffer;
117         }
118         // draw the last buf in the chain to the main texture
119         RenderExt.draw_render_target_from(last_buf, render_target);
120     }
121 
122     protected abstract void render_scene();
123 
124     /// may optionally be used to render global things from a scene
125     protected void render_hook() {
126     }
127 
128     private void update_render_target() {
129         if (Core.headless)
130             return;
131         // free any old render target
132         if (render_target != raylib.RenderTexture2D.init) {
133             raylib.UnloadRenderTexture(render_target);
134         }
135         // create render target
136         // TODO: use scene resolution instead of window resolution
137         render_target = raylib.LoadRenderTexture(cast(int) resolution.x, cast(int) resolution.y);
138         // apply texture filter
139         raylib.SetTextureFilter(render_target.texture, Core.default_filter_mode);
140     }
141 
142     /// called internally on scene creation
143     public void begin() {
144         setup();
145 
146         on_start();
147     }
148 
149     /// setup that hapostprocessorsens after begin, but before the child scene starts
150     protected void setup() {
151         // set up ecs
152         ecs = new EntityManager;
153 
154         resolution = Core.default_resolution;
155     }
156 
157     /// called internally on scene destruction
158     public void end() {
159         unload();
160 
161         ecs.destroy();
162         ecs = null;
163 
164         foreach (postprocessor; postprocessors) {
165             postprocessor.destroy();
166         }
167         postprocessors = [];
168 
169         foreach (manager; managers) {
170             manager.destroy();
171         }
172 
173         if (!Core.headless) {
174             // free render target
175             raylib.UnloadRenderTexture(render_target);
176         }
177     }
178 
179     public Nullable!T get_manager(T)() {
180         import std.algorithm.searching : find;
181 
182         // find a manager matching the type
183         auto matches = managers.find!(x => (cast(T) x) !is null);
184         if (matches.length > 0) {
185             return Nullable!T(cast(T) matches.front);
186         }
187         return Nullable!T.init;
188     }
189 
190     /// adds a manager to this scene
191     public T add_manager(T)(T manager) {
192         managers ~= manager;
193         manager.scene = this;
194         manager.setup();
195         return manager;
196     }
197 
198     // - ecs
199 
200     /// create an entity given a name
201     public Entity create_entity(string name) {
202         auto nt = ecs.create_entity();
203         nt.name = name;
204         nt.scene = this;
205         return nt;
206     }
207 
208     /// create an entity given a name and a 2d position
209     public Entity create_entity(string name, Vector2 pos = Vector2(0, 0)) {
210         auto nt = create_entity(name);
211         nt.position2 = pos;
212         return nt;
213     }
214 
215     /// create an entity given a name and a 3d position
216     public Entity create_entity(string name, Vector3 pos = Vector3(0, 0, 0)) {
217         auto nt = create_entity(name);
218         nt.position = pos;
219         return nt;
220     }
221 
222     public Entity get_entity(string name) {
223         return ecs.get_entity(name);
224     }
225 }
226 
227 @("scene-lifecycle")
228 unittest {
229     class TestScene : Scene2D {
230         override void on_start() {
231             auto apple = create_entity("apple");
232             assert(get_entity("apple") == apple, "could not get entity by name");
233         }
234     }
235 
236     Core.headless = true;
237     auto scene = new TestScene();
238     scene.begin();
239     scene.update();
240     scene.end();
241 }
242 
243 @("scene-load")
244 unittest {
245     class TestScene : Scene2D {
246     }
247 
248     Core.headless = true;
249 
250     auto scene = new TestScene();
251     Core.load_scenes([scene]);
252     assert(Core.get_scene!TestScene == scene);
253 }
254 
255 /// create a test game, with a test scene, and update it
256 @("scene-full")
257 unittest {
258     import re.util.test : TestGame;
259 
260     static class TestScene : Scene2D {
261         class Plant : Component, Updatable {
262             public int height = 0;
263 
264             void update() {
265                 height++;
266             }
267         }
268 
269         override void on_start() {
270             // create a basic entity
271             auto nt = create_entity("apple");
272             // add a basic component
273             nt.add_component(new Plant());
274         }
275     }
276 
277     auto my_scene = new TestScene();
278 
279     class Game : TestGame {
280         override void initialize() {
281             load_scenes([my_scene]);
282         }
283     }
284 
285     auto game = new Game();
286     game.run();
287 
288     // make sure scene is accessible
289     assert(game.primary_scene == my_scene, "primary scene does not match loaded scene");
290 
291     // make sure components worked
292     assert(my_scene.get_entity("apple").get_component!(TestScene.Plant)()
293             .height > 0, "test Updatable was not updated");
294 
295     game.destroy(); // clean up
296 
297     // make sure scene is cleaned up
298     assert(my_scene.ecs is null, "scene was not cleaned up");
299 }