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 private void update_render_target() { 131 if (Core.headless) 132 return; 133 // free any old render target 134 if (render_target != RenderTarget.init) { 135 RenderExt.destroy_render_target(render_target); 136 } 137 // create render target 138 // TODO: use scene resolution instead of window resolution 139 render_target = RenderExt.create_render_target(cast(int) resolution.x, cast(int) resolution.y); 140 // apply texture filter 141 raylib.SetTextureFilter(render_target.texture, Core.default_filter_mode); 142 } 143 144 /// called internally on scene creation 145 public void begin() { 146 setup(); 147 148 on_start(); 149 } 150 151 /// setup that hapostprocessorsens after begin, but before the child scene starts 152 protected void setup() { 153 // set up ecs 154 ecs = new EntityManager; 155 156 resolution = Core.default_resolution; 157 } 158 159 /// called internally on scene destruction 160 public void end() { 161 unload(); 162 163 ecs.destroy(); 164 ecs = null; 165 166 foreach (postprocessor; postprocessors) { 167 postprocessor.destroy(); 168 } 169 postprocessors = []; 170 171 foreach (manager; managers) { 172 manager.destroy(); 173 } 174 175 if (!Core.headless) { 176 // free render target 177 RenderExt.destroy_render_target(render_target); 178 } 179 } 180 181 public Nullable!T get_manager(T)() { 182 import std.algorithm.searching : find; 183 184 // find a manager matching the type 185 auto matches = managers.find!(x => (cast(T) x) !is null); 186 if (matches.length > 0) { 187 return Nullable!T(cast(T) matches.front); 188 } 189 return Nullable!T.init; 190 } 191 192 /// adds a manager to this scene 193 public T add_manager(T)(T manager) { 194 managers ~= manager; 195 manager.scene = this; 196 manager.setup(); 197 return manager; 198 } 199 200 // - ecs 201 202 /// create an entity given a name 203 public Entity create_entity(string name) { 204 auto nt = ecs.create_entity(); 205 nt.name = name; 206 nt.scene = this; 207 return nt; 208 } 209 210 /// create an entity given a name and a 2d position 211 public Entity create_entity(string name, Vector2 pos = Vector2(0, 0)) { 212 auto nt = create_entity(name); 213 nt.position2 = pos; 214 return nt; 215 } 216 217 /// create an entity given a name and a 3d position 218 public Entity create_entity(string name, Vector3 pos = Vector3(0, 0, 0)) { 219 auto nt = create_entity(name); 220 nt.position = pos; 221 return nt; 222 } 223 224 public Entity get_entity(string name) { 225 return ecs.get_entity(name); 226 } 227 } 228 229 @("scene-lifecycle") 230 unittest { 231 class TestScene : Scene2D { 232 override void on_start() { 233 auto apple = create_entity("apple"); 234 assert(get_entity("apple") == apple, "could not get entity by name"); 235 } 236 } 237 238 Core.headless = true; 239 auto scene = new TestScene(); 240 scene.begin(); 241 scene.update(); 242 scene.end(); 243 } 244 245 @("scene-load") 246 unittest { 247 class TestScene : Scene2D { 248 } 249 250 Core.headless = true; 251 252 auto scene = new TestScene(); 253 Core.load_scenes([scene]); 254 assert(Core.get_scene!TestScene == scene); 255 } 256 257 /// create a test game, with a test scene, and update it 258 @("scene-full") 259 unittest { 260 import re.util.test : TestGame; 261 262 static class TestScene : Scene2D { 263 class Plant : Component, Updatable { 264 public int height = 0; 265 266 void update() { 267 height++; 268 } 269 } 270 271 override void on_start() { 272 // create a basic entity 273 auto nt = create_entity("apple"); 274 // add a basic component 275 nt.add_component(new Plant()); 276 } 277 } 278 279 auto my_scene = new TestScene(); 280 281 class Game : TestGame { 282 override void initialize() { 283 load_scenes([my_scene]); 284 } 285 } 286 287 auto game = new Game(); 288 game.run(); 289 290 // make sure scene is accessible 291 assert(game.primary_scene == my_scene, "primary scene does not match loaded scene"); 292 293 // make sure components worked 294 assert(my_scene.get_entity("apple").get_component!(TestScene.Plant)() 295 .height > 0, "test Updatable was not updated"); 296 297 game.destroy(); // clean up 298 299 // make sure scene is cleaned up 300 assert(my_scene.ecs is null, "scene was not cleaned up"); 301 }