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