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 }