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