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