1 /** globally available game core, providing access to most key game services and scene control */ 2 3 module re.core; 4 5 import std.array; 6 import std.typecons; 7 import std.format; 8 9 import re.input; 10 import re.content; 11 import re.time; 12 import re.gfx.window; 13 import re.ng.scene; 14 import re.ng.diag; 15 import re.ng.manager; 16 import re.gfx.render_ext; 17 import re.math; 18 import re.util.logger; 19 import re.util.tweens.tween_manager; 20 import jar; 21 static import raylib; 22 23 /** 24 Core class 25 */ 26 abstract class Core { 27 /// logger utility 28 public static Logger log; 29 30 /// game window 31 public static Window window; 32 33 /// content manager 34 public static ContentManager content; 35 36 /// the current scenes 37 private static Scene[] _scenes; 38 39 /// type registration container 40 public static Jar jar; 41 42 /// global managers 43 public static Manager[] managers; 44 45 /// whether to draw debug information 46 public static bool debug_render; 47 48 /// debugger utility 49 debug public static Debugger debugger; 50 51 /// whether the game is running 52 public static bool running; 53 54 version (unittest) { 55 /// the frame limit (used for testing) 56 public static int frame_limit = 60; 57 } 58 59 /// target frames per second 60 public static int target_fps = 60; 61 62 /// whether graphics should be disabled 63 public static bool headless = false; 64 65 /// whether to pause when unfocused 66 public static bool pause_on_focus_lost = true; 67 68 /// whether to exit when escape pressed 69 public static bool exit_on_escape_pressed = true; 70 71 /// whether to automatically scale things to compensate for hidpi 72 /// NOTE: raylib.ConfigFlags.FLAG_WINDOW_HIGHDPI also exists, but we're not using it right now 73 public static bool auto_compensate_hidpi = true; 74 75 /// the default render resolution for all scenes 76 public static Vector2 default_resolution; 77 78 /// the default texture filtering mode for render targets 79 public static raylib.TextureFilter default_filter_mode 80 = raylib.TextureFilter.TEXTURE_FILTER_POINT; 81 82 /// sets up a game core 83 this(int width, int height, string title) { 84 log = new Logger(Logger.Verbosity.Information); 85 log.sinks ~= new Logger.ConsoleSink(); 86 87 version (unittest) { 88 } else { 89 log.info("initializing rengfx core"); 90 } 91 92 default_resolution = Vector2(width, height); 93 if (!Core.headless) { 94 window = new Window(width, height); 95 window.initialize(); 96 window.set_title(title); 97 if (auto_compensate_hidpi) { 98 // resize window according to dpi scale 99 auto scaled_width = cast(int)(window.width * window.scale_dpi); 100 auto scaled_height = cast(int)(window.height * window.scale_dpi); 101 log.info(format("resizing window from (%s,%s) to (%s,%s) to compensate for dpi scale: %s", 102 window.width, window.height, scaled_width, scaled_height, window.scale_dpi)); 103 window.resize(scaled_width, scaled_height); 104 } 105 } 106 107 // disable default exit key 108 raylib.SetExitKey(raylib.KeyboardKey.KEY_NULL); 109 110 content = new ContentManager(); 111 112 jar = new Jar(); 113 114 add_manager(new TweenManager()); 115 116 debug { 117 debugger = new Debugger(); 118 } 119 120 version (unittest) { 121 } else { 122 log.info("initializing game"); 123 } 124 125 initialize(); 126 } 127 128 @property public static int fps() { 129 return raylib.GetFPS(); 130 } 131 132 /// sets up the game 133 abstract void initialize(); 134 135 /// starts the game 136 public void run() { 137 running = true; 138 // start the game loop 139 while (running) { 140 if (!headless) { 141 running = !raylib.WindowShouldClose(); 142 } 143 144 update(); 145 draw(); 146 147 version (unittest) { 148 if (Time.frame_count >= frame_limit) { 149 running = false; 150 } 151 } 152 } 153 } 154 155 /// gracefully exits the game 156 public static void exit() { 157 running = false; 158 version (unittest) { 159 } else { 160 log.info("gracefully exiting"); 161 } 162 } 163 164 protected void update() { 165 if (pause_on_focus_lost && raylib.IsWindowMinimized()) { 166 return; // pause 167 } 168 if (exit_on_escape_pressed && raylib.IsKeyPressed(raylib.KeyboardKey.KEY_ESCAPE)) { 169 exit(); 170 } 171 version (unittest) { 172 Time.update(1f / target_fps); // 60 fps 173 } else { 174 Time.update(raylib.GetFrameTime()); 175 } 176 foreach (manager; managers) { 177 manager.update(); 178 } 179 Input.update(); 180 foreach (scene; _scenes) { 181 scene.update(); 182 } 183 debug { 184 debugger.update(); 185 } 186 } 187 188 protected void draw() { 189 if (Core.headless) 190 return; 191 if (raylib.IsWindowMinimized()) { 192 return; // suppress draw 193 } 194 raylib.BeginDrawing(); 195 foreach (scene; _scenes) { 196 // render scene 197 scene.render(); 198 // post-render 199 scene.post_render(); 200 // composite screen render to window 201 // TODO: support better compositing 202 RenderExt.draw_render_target(scene.render_target, Rectangle(0, 0, 203 window.width, window.height), scene.composite_mode.color); 204 } 205 debug { 206 debugger.render(); 207 } 208 raylib.EndDrawing(); 209 } 210 211 public static T get_scene(T)() { 212 import std.algorithm.searching : find; 213 214 // find a scene matching the type 215 auto matches = _scenes.find!(x => (cast(T) x) !is null); 216 assert(matches.length > 0, "no matching scene was found"); 217 return cast(T) matches.front; 218 } 219 220 public static Nullable!T get_manager(T)() { 221 import std.algorithm.searching : find; 222 223 // find a manager matching the type 224 auto matches = managers.find!(x => (cast(T) x) !is null); 225 if (matches.length > 0) { 226 return Nullable!T(cast(T) matches.front); 227 } 228 return Nullable!T.init; 229 } 230 231 /// adds a global manager 232 public T add_manager(T)(T manager) { 233 managers ~= manager; 234 manager.setup(); 235 return manager; 236 } 237 238 @property public static Scene[] scenes() { 239 return _scenes; 240 } 241 242 @property public static Scene primary_scene() { 243 return _scenes.front; 244 } 245 246 /// sets the current scenes 247 static void load_scenes(Scene[] new_scenes) { 248 foreach (scene; _scenes) { 249 // end old scenes 250 scene.end(); 251 scene = null; 252 } 253 // clear scenes list 254 _scenes = []; 255 256 _scenes ~= new_scenes; 257 // begin new scenes 258 foreach (scene; _scenes) { 259 scene.begin(); 260 } 261 } 262 263 /// releases all resources and cleans up 264 public void destroy() { 265 debug { 266 debugger.destroy(); 267 } 268 content.destroy(); 269 load_scenes([]); // end scenes 270 foreach (manager; managers) { 271 manager.destroy(); 272 } 273 if (!Core.headless) { 274 window.destroy(); 275 } 276 } 277 } 278 279 @("core-basic") 280 unittest { 281 import re.util.test : TestGame; 282 import std.string : format; 283 import std.math : isClose; 284 285 class Game : TestGame { 286 override void initialize() { 287 // nothing much 288 } 289 } 290 291 auto game = new Game(); 292 game.run(); 293 294 // ensure time has passed 295 auto target_time = Core.frame_limit / Core.target_fps; 296 assert(isClose(Time.total_time, target_time), 297 format("time did not pass (expected: %s, actual: %s)", target_time, Time.total_time)); 298 299 game.destroy(); // clean up 300 301 assert(game.scenes.length == 0, "scenes were not removed after Game cleanup"); 302 }