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 }