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