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