1 module polyplex.core.game; 2 3 import polyplex.utils.logging; 4 5 import polyplex.math; 6 import polyplex.core.windows; 7 import polyplex.core.render; 8 import polyplex.core.input; 9 import polyplex.core.events; 10 import polyplex.core.content; 11 import polyplex.core.audio; 12 import polyplex.core.time; 13 static import win = polyplex.core.window; 14 15 import polyplex.utils.strutils; 16 import polyplex : InitLibraries, UnInitLibraries; 17 18 import bindbc.sdl; 19 import events; 20 21 import std.math; 22 import std.random; 23 import std.typecons; 24 import std.stdio; 25 import std.conv; 26 27 import core.memory; 28 29 /** 30 The base system to manage a game 31 This class handles the following: 32 * Spawning a GameWindow 33 * Starting an event loop for input 34 * Creating an audio subsystem 35 * Fixed timestep updates 36 * Content management pipeline creation 37 38 Basicly, extend this class to create a game. 39 */ 40 abstract class Game { 41 private: 42 GameEventSystem events; 43 GameTime times; 44 ulong frameTimeStart = 0; 45 double frameTimeDelta = 0; 46 ulong frameTimeLast = 0; 47 bool enableAudio = true; 48 49 double lag = 0.0; 50 double msTarget = 0.250; 51 52 void doUpdate() { 53 Prepare(); 54 while (!RunOne()) { 55 } 56 } 57 58 protected: 59 //Private properties 60 win.Window window; 61 ContentManager Content; 62 SpriteBatch spriteBatch; 63 64 package: 65 void forceWindowChange(win.Window newWindow) { 66 this.window = newWindow; 67 } 68 69 public: 70 71 /** 72 Run a fixed time step update 73 */ 74 void FixedUpdate(double fixedDelta) { } 75 76 // Abstract declarations 77 abstract { 78 /** 79 Initialize core game variables and the like. 80 81 Content can be loaded in LoadContent. 82 */ 83 void Init(); 84 /** 85 Load game assets from disk, etc. 86 */ 87 void LoadContent(); 88 89 /** 90 Unload game assets, etc. 91 92 Use D's `destroy(<*>);` function to unload content 93 */ 94 void UnloadContent(); 95 96 /** 97 Run an update iteration 98 */ 99 void Update(GameTime gameTime); 100 101 /** 102 Run a draw iteration 103 */ 104 void Draw(GameTime gameTime); 105 } 106 107 final: 108 /// Event raised when the window changes its size 109 Event!GameResizeEventArgs OnWindowSizeChanged = new Event!GameResizeEventArgs; 110 111 /// How much time since game started 112 @property GameTimeSpan TotalTime() { return times.TotalTime; } 113 114 /// How much time since last frame 115 @property GameTimeSpan DeltaTime() { return times.DeltaTime; } 116 117 /// Wether the system cursor should be shown while inside the game window. 118 @property bool ShowCursor() { 119 return (SDL_ShowCursor(SDL_QUERY) == SDL_ENABLE); 120 } 121 122 /// ditto. 123 @property void ShowCursor(bool value) { 124 SDL_ShowCursor(cast(int)value); 125 } 126 127 /// Wether the audio backend is enabled. 128 @property bool AudioEnabled() { 129 return !(DefaultAudioDevice is null); 130 } 131 132 /// ditto. 133 @property void AudioEnabled(bool val) { 134 enableAudio = val; 135 if (val == true) DefaultAudioDevice = new AudioDevice(); 136 else DefaultAudioDevice = null; 137 } 138 139 /// How many miliseconds since the last frame was drawn 140 @property double Frametime() { 141 return frameTimeDelta; 142 } 143 144 /// How many miliseconds since the last frame was drawn 145 @property double FixedFrametime() { 146 return msTarget; 147 } 148 149 /// Returns true if the game is running slowly 150 @property bool IsGameLagging() { 151 return lag > 10.0; 152 } 153 154 /// The window the game is being rendered to 155 @property win.Window Window() { return window; } 156 157 /** 158 Create a new game instance. 159 */ 160 this(bool audio = true, bool eventSystem = true) { 161 enableAudio = audio; 162 if (eventSystem) events = new GameEventSystem(); 163 } 164 165 ~this() { 166 UnloadContent(); 167 destroy(window); 168 } 169 170 /** 171 Start/run the game 172 */ 173 void Run() { 174 if (window is null) { 175 window = new SDLGameWindow(Rectanglei(0, 0, 0, 0), false); 176 } 177 InitLibraries(); 178 window.Show(); 179 Renderer.setWindow(window); 180 Renderer.Init(); 181 182 doUpdate(); 183 UnInitLibraries(); 184 } 185 186 /** 187 Poll system events 188 */ 189 void PollEvents() { 190 events.Update(); 191 } 192 193 194 /** 195 Run a single frame of the game 196 */ 197 bool RunOne() { 198 //FPS begin counting. 199 frameTimeStart = SDL_GetPerformanceCounter(); 200 times.updateTotal(cast(double)SDL_GetTicks()); 201 202 //Update events. 203 if (events !is null) { 204 PPEvents.PumpEvents(); 205 206 //Do actual updating and drawing. 207 events.Update(); 208 } 209 210 // Run user set update and draw functions 211 Update(times); 212 213 lag += frameTimeDelta; 214 while (lag >= msTarget) { 215 216 FixedUpdate(msTarget); 217 lag -= msTarget; 218 } 219 if (lag < 0) lag = 0; 220 221 Draw(times); 222 223 // Exit the game if the window is closed. 224 if (!window.Visible) { 225 Quit(); 226 return true; 227 } 228 229 //Swap buffers and chain. 230 if (spriteBatch !is null) spriteBatch.SwapChain(); 231 Renderer.SwapBuffers(); 232 233 // Update frametime delta 234 frameTimeDelta = cast(double)((SDL_GetPerformanceCounter() - frameTimeStart) * 1000) / cast(double)SDL_GetPerformanceFrequency(); 235 times.updateDelta(frameTimeDelta); 236 frameTimeLast = frameTimeStart; 237 238 return false; 239 } 240 241 242 /** 243 Prepare the backend for use. 244 */ 245 void Prepare(bool waitForVisible = true) { 246 // Preupdate before init, just in case some event functions are use there. 247 if (events !is null) events.Update(); 248 249 //Wait for window to open. 250 Logger.Debug("~~~ Init ~~~"); 251 while (waitForVisible && !window.Visible) {} 252 253 //Update window info. 254 window.UpdateState(); 255 if (events !is null) { 256 events.OnExitRequested ~= (void* sender, EventArgs data) { 257 window.Close(); 258 }; 259 260 events.OnWindowSizeChanged ~= (void* sender, GameResizeEventArgs data) { 261 window.UpdateState(); 262 Renderer.AdjustViewport(); 263 OnWindowSizeChanged(sender, data); 264 }; 265 } 266 267 times = new GameTime(new GameTimeSpan(0), new GameTimeSpan(0)); 268 269 // Init sprite batch 270 this.spriteBatch = new SpriteBatch(); 271 this.Content = new ContentManager(); 272 273 if (enableAudio) DefaultAudioDevice = new AudioDevice(); 274 275 Init(); 276 LoadContent(); 277 Logger.Debug("~~~ Gameloop ~~~"); 278 } 279 280 /** 281 Quits the game 282 */ 283 void Quit() { 284 import polyplex.core.audio.music; 285 286 287 // Stop music thread(s) and wait... 288 stopMusicThread(); 289 290 Logger.Info("Cleaning up resources..."); 291 UnloadContent(); 292 293 destroy(DefaultAudioDevice); 294 destroy(window); 295 296 GC.collect(); 297 298 Logger.Success("Cleanup completed..."); 299 Logger.Success("~~~ GAME ENDED ~~~"); 300 } 301 } 302