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 /// oversampling factor for internal rendering 72 public static int render_oversample = 1; 73 74 /// whether to automatically scale things to compensate for hidpi 75 /// NOTE: raylib.ConfigFlags.FLAG_WINDOW_HIGHDPI also exists, but we're not using it right now 76 public static bool auto_compensate_hidpi = true; 77 78 /// whether to automatically oversample for hidpi 79 public static bool auto_oversample_hidpi = false; 80 81 /// whether to rescale the mouse position to compensate for hidpi 82 public static bool auto_rescale_mouse_hidpi = false; 83 84 /// whether to automatically resize the render target to the window size 85 public static bool sync_render_window_resolution = false; 86 87 /// the default render resolution for all scenes 88 public static Vector2 default_resolution; 89 90 /// the default texture filtering mode for render targets 91 public static raylib.TextureFilter default_filter_mode 92 = raylib.TextureFilter.TEXTURE_FILTER_POINT; 93 94 version (vr) { 95 import re.gfx.vr; 96 97 public static VRSupport vr; 98 } 99 100 /// sets up a game core 101 this(int width, int height, string title) { 102 log = new Logger(Logger.Verbosity.Info); 103 log.sinks ~= new Logger.ConsoleSink(); 104 105 version (unittest) { 106 } else { 107 log.info("initializing rengfx core"); 108 } 109 110 default_resolution = Vector2(width, height); 111 if (!Core.headless) { 112 window = new Window(width, height); 113 window.initialize(); 114 window.set_title(title); 115 if (auto_compensate_hidpi) { 116 handle_hidpi_compensation(); 117 } 118 } 119 120 // disable default exit key 121 raylib.SetExitKey(raylib.KeyboardKey.KEY_NULL); 122 123 content = new ContentManager(); 124 125 jar = new Jar(); 126 127 add_manager(new TweenManager()); 128 129 debug { 130 debugger = new Debugger(); 131 } 132 133 version (vr) { 134 import re.gfx.vr; 135 136 vr = new VRSupport(); 137 } 138 139 version (unittest) { 140 } else { 141 log.info("initializing game"); 142 } 143 144 initialize(); 145 } 146 147 @property public static int fps() { 148 return raylib.GetFPS(); 149 } 150 151 /// sets up the game 152 abstract void initialize(); 153 154 /// starts the game 155 public void run() { 156 running = true; 157 // start the game loop 158 while (running) { 159 if (!headless) { 160 running = !raylib.WindowShouldClose(); 161 } 162 163 update(); 164 draw(); 165 166 version (unittest) { 167 if (Time.frame_count >= frame_limit) { 168 running = false; 169 } 170 } 171 } 172 } 173 174 /// gracefully exits the game 175 public static void exit() { 176 running = false; 177 version (unittest) { 178 } else { 179 log.info("gracefully exiting"); 180 } 181 } 182 183 protected void update() { 184 // update window 185 if (!Core.headless) { 186 if (pause_on_focus_lost && raylib.IsWindowMinimized()) { 187 return; // pause 188 } 189 if (exit_on_escape_pressed && raylib.IsKeyPressed(raylib.KeyboardKey.KEY_ESCAPE)) { 190 exit(); 191 } 192 if (raylib.IsWindowResized()) { 193 handle_window_resize(); 194 } 195 } 196 197 version (unittest) { 198 Time.update(1f / target_fps); // 60 fps 199 } else { 200 Time.update(raylib.GetFrameTime()); 201 } 202 foreach (manager; managers) { 203 manager.update(); 204 } 205 // update input 206 Input.update(); 207 // update scenes 208 foreach (scene; _scenes) { 209 scene.update(); 210 } 211 debug { 212 debugger.update(); 213 } 214 } 215 216 protected void draw() { 217 if (Core.headless) 218 return; 219 if (raylib.IsWindowMinimized()) { 220 return; // suppress draw 221 } 222 raylib.BeginDrawing(); 223 foreach (scene; _scenes) { 224 // render scene 225 scene.render(); 226 // post-render 227 scene.post_render(); 228 // composite (blit)screen render to window 229 // when the scene is rendered, it is rendered to a texture. this texture is then composited onto the main display buffer. 230 231 version (vr) { 232 bool vr_distort = false; 233 if (vr.enabled) { 234 assert(vr.distortion_shader != raylib.Shader.init, "vr.distortion_shader is not initialized"); 235 vr_distort = true; 236 } 237 238 if (vr_distort) 239 raylib.BeginShaderMode(vr.distortion_shader); 240 } 241 242 RenderExt.draw_render_target(scene.render_target, Rectangle(0, 0, 243 window.width, window.height), scene.composite_mode.color); 244 245 version (vr) { 246 if (vr_distort) 247 raylib.EndShaderMode(); 248 } 249 } 250 debug { 251 debugger.render(); 252 } 253 raylib.EndDrawing(); 254 } 255 256 public static T get_scene(T)() { 257 import std.algorithm.searching : find; 258 259 // find a scene matching the type 260 auto matches = _scenes.find!(x => (cast(T) x) !is null); 261 assert(matches.length > 0, "no matching scene was found"); 262 return cast(T) matches.front; 263 } 264 265 public static Nullable!T get_manager(T)() { 266 import std.algorithm.searching : find; 267 268 // find a manager matching the type 269 auto matches = managers.find!(x => (cast(T) x) !is null); 270 if (matches.length > 0) { 271 return Nullable!T(cast(T) matches.front); 272 } 273 return Nullable!T.init; 274 } 275 276 /// adds a global manager 277 public T add_manager(T)(T manager) { 278 managers ~= manager; 279 manager.setup(); 280 return manager; 281 } 282 283 @property public static Scene[] scenes() { 284 return _scenes; 285 } 286 287 @property public static Scene primary_scene() { 288 return _scenes.front; 289 } 290 291 /// sets the current scenes 292 static void load_scenes(Scene[] new_scenes) { 293 foreach (scene; _scenes) { 294 // end old scenes 295 scene.end(); 296 scene = null; 297 } 298 // clear scenes list 299 _scenes = []; 300 301 _scenes ~= new_scenes; 302 // begin new scenes 303 foreach (scene; _scenes) { 304 scene.begin(); 305 } 306 } 307 308 /// releases all resources and cleans up 309 public void destroy() { 310 version (vr) { 311 if (vr.enabled) { 312 assert(vr.config != raylib.VrStereoConfig.init, "vr config was not initialized"); 313 314 raylib.UnloadVrStereoConfig(vr.config); 315 } 316 317 } 318 debug { 319 debugger.destroy(); 320 } 321 content.destroy(); 322 load_scenes([]); // end scenes 323 foreach (manager; managers) { 324 manager.destroy(); 325 } 326 if (!Core.headless) { 327 window.destroy(); 328 } 329 } 330 331 private void handle_hidpi_compensation() { 332 // when hidpi is enabled,, the window is too small 333 // so we scale the real window but keep the render resolution the same 334 // compute the target window size 335 auto scaled_width = window.width_dpi; 336 auto scaled_height = window.height_dpi; 337 // but, if auto-oversampling is enabled, we need to set the oversample factor 338 if (auto_oversample_hidpi) { 339 render_oversample = cast(int) window.scale_dpi; 340 log.info("auto-oversampling enabled, setting oversample factor to %d", render_oversample); 341 } 342 log.info("resizing window from (%s,%s) to (%s,%s) to compensate for dpi scale: %s", 343 window.width, window.height, scaled_width, scaled_height, window.scale_dpi); 344 window.resize(scaled_width, scaled_height); 345 handle_window_resize(); 346 sync_render_resolution(); 347 if (auto_rescale_mouse_hidpi) { 348 // set mouse transform to compensate for dpi scale 349 raylib.SetMouseScale(1 / window.scale_dpi, 1 / window.scale_dpi); 350 } 351 } 352 353 private void handle_window_resize() { 354 log.info("window resized to (%s,%s)", window.width, window.height); 355 // window was resized 356 if (sync_render_window_resolution) { 357 sync_render_resolution(); 358 } 359 // notify the active scenes 360 foreach (scene; _scenes) { 361 scene.on_window_resized(); 362 } 363 } 364 365 private void sync_render_resolution() { 366 // since window was resized, update our render resolution 367 // first get the new window size 368 auto render_res_x = window.width; 369 auto render_res_y = window.height; 370 // if (auto_compensate_hidpi) { 371 // // if hidpi compensation is enabled, we need to scale the window size by the hidpi scale 372 // render_res_x = cast(int)(render_res_x / window.scale_dpi); 373 // render_res_y = cast(int)(render_res_y / window.scale_dpi); 374 // // if hidpi compensation is on then the window size will be scale*resolution 375 // // so we need to divide by the scale to get the render resolution 376 // } 377 // if oversampling is enabled, we need to multiply by the oversampling factor 378 render_res_x *= render_oversample; 379 render_res_y *= render_oversample; 380 // set the render resolution 381 default_resolution = Vector2(render_res_x, render_res_y); 382 // Core.log.info(format("updating render resolution to %s", default_resolution)); 383 Core.log.info(format("updating render resolution to %s (dpi scale %s) (oversample %s)", 384 default_resolution, window.scale_dpi, render_oversample)); 385 } 386 } 387 388 @("core-basic") 389 unittest { 390 import re.util.test : TestGame; 391 import std.string : format; 392 import std.math : isClose; 393 394 class Game : TestGame { 395 override void initialize() { 396 // nothing much 397 } 398 } 399 400 auto game = new Game(); 401 game.run(); 402 403 // ensure time has passed 404 auto target_time = Core.frame_limit / Core.target_fps; 405 assert(isClose(Time.total_time, target_time), 406 format("time did not pass (expected: %s, actual: %s)", target_time, Time.total_time)); 407 408 game.destroy(); // clean up 409 410 assert(game.scenes.length == 0, "scenes were not removed after Game cleanup"); 411 }