1 module polyplex.core.game; 2 3 import polyplex.math; 4 static import win = polyplex.core.window; 5 import polyplex.core.windows; 6 import polyplex.core.render; 7 import polyplex.core.input; 8 import polyplex.core.events; 9 import polyplex.core.content; 10 import polyplex.core.audio; 11 import polyplex.utils.logging; 12 13 import polyplex.utils.strutils; 14 import polyplex : InitLibraries, UnInitLibraries; 15 16 import bindbc.sdl; 17 import sev.event; 18 19 import std.math; 20 import std.random; 21 import std.typecons; 22 import std.stdio; 23 import std.conv; 24 25 import core.memory; 26 27 public class GameTime { 28 private ulong ticks; 29 30 public @property ulong BaseValue() { return ticks; } 31 public @property void BaseValue(ulong ticks) { this.ticks = ticks; } 32 33 public @property ulong LMilliseconds() { return ticks; } 34 public @property ulong LSeconds() { return LMilliseconds/1000; } 35 public @property ulong LMinutes() { return LSeconds/60; } 36 public @property ulong LHours() { return LMinutes/60; } 37 38 public @property double Milliseconds() { return cast(double)ticks; } 39 public @property double Seconds() { return Milliseconds/1000; } 40 public @property double Minutes() { return Seconds/60; } 41 public @property double Hours() { return Minutes/60; } 42 43 public static GameTime FromSeconds(ulong seconds) { 44 return new GameTime(seconds*1000); 45 } 46 47 public static GameTime FromMinutes(ulong minutes) { 48 return FromSeconds(minutes*60); 49 } 50 51 public static GameTime FromHours(ulong hours) { 52 return FromMinutes(hours*60); 53 } 54 55 public GameTime opBinary(string op:"+")(GameTime other) { 56 return new GameTime(this.ticks+other.ticks); 57 } 58 59 public GameTime opBinary(string op:"-")(GameTime other) { 60 return new GameTime(this.ticks-other.ticks); 61 } 62 63 public GameTime opBinary(string op:"/")(GameTime other) { 64 return new GameTime(this.ticks/other.ticks); 65 } 66 67 public GameTime opBinary(string op:"*")(GameTime other) { 68 return new GameTime(this.ticks*other.ticks); 69 } 70 71 public float PercentageOf(GameTime other) { 72 return cast(float)this.ticks/cast(float)other.ticks; 73 } 74 75 public string ToString() { 76 return LHours.text ~ ":" ~ (LMinutes%60).text ~ ":" ~ (LSeconds%60).text ~ "." ~ (LMilliseconds%60).text; 77 } 78 79 public string FormatTime(string formatstring) { 80 return Format(formatstring, LHours, LMinutes%60, LSeconds%60, LMilliseconds%60); 81 } 82 83 this(ulong ticks) { 84 this.ticks = ticks; 85 } 86 } 87 88 public class GameTimes { 89 90 this(GameTime total, GameTime delta) { 91 TotalTime = total; 92 DeltaTime = delta; 93 } 94 95 public GameTime TotalTime; 96 public GameTime DeltaTime; 97 } 98 99 public abstract class Game { 100 private: 101 GameEventSystem events; 102 GameTimes times; 103 static uint MAX_SAMPLES = 100; 104 long[] samples; 105 ulong start_frames = 0; 106 ulong delta_frames = 0; 107 ulong last_frames = 0; 108 double avg_fps = 0; 109 bool enable_audio = true; 110 111 protected: 112 //Private properties 113 win.Window window; 114 ContentManager Content; 115 SpriteBatch sprite_batch; 116 117 package: 118 void forceWindowChange(win.Window newWindow) { 119 this.window = newWindow; 120 } 121 122 public: 123 /// Wether the engine should count FPS and frametimes. 124 public bool CountFPS = false; 125 126 public Event OnWindowSizeChanged = new Event(); 127 public @property GameTime TotalTime() { return times.TotalTime; } 128 public @property GameTime DeltaTime() { return times.DeltaTime; } 129 130 public @property bool ShowCursor() { 131 return (SDL_ShowCursor(SDL_QUERY) == SDL_ENABLE); 132 } 133 134 public @property void ShowCursor(bool value) { 135 SDL_ShowCursor(cast(int)value); 136 } 137 138 public @property bool AudioEnabled() { 139 return !(DefaultAudioDevice is null); 140 } 141 142 public @property void AudioEnabled(bool val) { 143 enable_audio = val; 144 if (val == true) DefaultAudioDevice = new AudioDevice(); 145 else DefaultAudioDevice = null; 146 } 147 148 public @property float FPS() { 149 if (delta_frames != 0) { 150 return 1000/delta_frames; 151 } 152 return 0f; 153 } 154 155 public @property int AverageFPS() { 156 if (avg_fps != 0) { 157 return cast(int)(1000/cast(double)avg_fps); 158 } 159 return 0; 160 } 161 162 public @property float Frametime() { 163 return delta_frames; 164 } 165 166 public @property win.Window Window() { return window; } 167 168 this(bool audio = true, bool eventSystem = true) { 169 enable_audio = audio; 170 if (eventSystem) events = new GameEventSystem(); 171 } 172 173 ~this() { 174 UnloadContent(); 175 destroy(window); 176 } 177 178 public void Run() { 179 if (window is null) { 180 window = new SDLGameWindow(new Rectangle(0, 0, 0, 0), false); 181 } 182 InitLibraries(); 183 window.Show(); 184 185 do_update(); 186 UnInitLibraries(); 187 } 188 189 public void PollEvents() { 190 events.Update(); 191 } 192 193 /// Run a single iteration 194 public bool RunOne() { 195 //FPS begin counting. 196 start_frames = SDL_GetTicks(); 197 times.TotalTime.BaseValue = start_frames; 198 199 if (events !is null) { 200 //Update events. 201 PPEvents.PumpEvents(); 202 203 //Do actual updating and drawing. 204 events.Update(); 205 } 206 207 Update(times); 208 Draw(times); 209 210 // Exit the game if the window is closed. 211 if (!window.Visible) { 212 End(); 213 return true; 214 } 215 216 //Swap buffers and chain. 217 if (sprite_batch !is null) sprite_batch.SwapChain(); 218 Renderer.SwapBuffers(); 219 220 if (CountFPS) { 221 //FPS counter. 222 delta_frames = SDL_GetTicks() - start_frames; 223 times.DeltaTime.BaseValue = delta_frames; 224 last_frames = start_frames; 225 226 if (samples.length <= MAX_SAMPLES) { 227 samples.length++; 228 } else { 229 samples[0] = -1; 230 for (int i = 1; i < samples.length; i++) { 231 if (samples[i-1] == -1) { 232 samples[i-1] = samples[i]; 233 samples[i] = -1; 234 } 235 } 236 samples[samples.length-1] = cast(long)delta_frames; 237 } 238 double t = 0; 239 foreach(ulong sample; samples) { 240 t += cast(double)sample; 241 } 242 t /= MAX_SAMPLES; 243 avg_fps = t; 244 } 245 return false; 246 } 247 248 void Prepare(bool waitForVisible = true) { 249 // Preupdate before init, just in case some event functions are use there. 250 if (events !is null) events.Update(); 251 252 //Wait for window to open. 253 Logger.Debug("~~~ Init ~~~"); 254 while (waitForVisible && !window.Visible) {} 255 256 //Update window info. 257 window.UpdateState(); 258 if (events !is null) { 259 events.OnExitRequested ~= (void* sender, EventArgs data) { 260 window.Close(); 261 }; 262 263 events.OnWindowSizeChanged ~= (void* sender, EventArgs data) { 264 window.UpdateState(); 265 Renderer.AdjustViewport(); 266 OnWindowSizeChanged(sender, data); 267 }; 268 } 269 270 times = new GameTimes(new GameTime(0), new GameTime(0)); 271 int avg_c = 0; 272 273 // Init sprite batch 274 this.sprite_batch = Renderer.NewBatcher(); 275 this.Content = new ContentManager(); 276 277 if (enable_audio) DefaultAudioDevice = new AudioDevice(); 278 279 Init(); 280 LoadContent(); 281 Logger.Debug("~~~ Gameloop ~~~"); 282 } 283 284 public void End() { 285 import polyplex.core.audio.music; 286 shouldStop = true; 287 288 Logger.Info("Cleaning up music threads... {0}", openMusicChannels); 289 while (openMusicChannels > 0) {} 290 291 Logger.Info("Cleaning up resources..."); 292 UnloadContent(); 293 294 destroy(DefaultAudioDevice); 295 destroy(window); 296 297 GC.collect(); 298 299 Logger.Success("Cleanup completed..."); 300 301 Logger.Success("~~~ GAME ENDED ~~~"); 302 } 303 304 private void do_update() { 305 Prepare(); 306 while (!RunOne()) { 307 } 308 } 309 310 public void Quit() { 311 this.window.Close(); 312 } 313 314 public abstract void Init(); 315 public abstract void LoadContent(); 316 public abstract void UnloadContent(); 317 public abstract void Update(GameTimes game_time); 318 public abstract void Draw(GameTimes game_time); 319 } 320