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 }