1 // WORK IN PROGRESS
2 
3 // register cheat code? or even a fighting game combo..
4 
5 /++
6 	An add-on for simpledisplay.d, joystick.d, and simpleaudio.d
7 	that includes helper functions for writing games (and perhaps
8 	other multimedia programs). Whereas simpledisplay works with
9 	an event-driven framework, gamehelpers always uses a consistent
10 	timer for updates.
11 
12 	Usage example:
13 
14 	---
15 	final class MyGame : GameHelperBase {
16 		/// Called when it is time to redraw the frame
17 		/// it will try for a particular FPS
18 		override void drawFrame() {
19 			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT);
20 
21 			glLoadIdentity();
22 
23 			glColor3f(1.0, 1.0, 1.0);
24 			glTranslatef(x, y, 0);
25 			glBegin(GL_QUADS);
26 
27 			glVertex2i(0, 0);
28 			glVertex2i(16, 0);
29 			glVertex2i(16, 16);
30 			glVertex2i(0, 16);
31 
32 			glEnd();
33 		}
34 
35 		int x, y;
36 		override bool update(Duration deltaTime) {
37 			x += 1;
38 			y += 1;
39 			return true;
40 		}
41 
42 		override SimpleWindow getWindow() {
43 			auto window = create2dWindow("My game");
44 			// load textures and such here
45 			return window;
46 		}
47 
48 		final void fillAudioBuffer(short[] buffer) {
49 
50 		}
51 	}
52 
53 	void main() {
54 		auto game = new MyGame();
55 
56 		runGame(game, maxRedrawRate, maxUpdateRate);
57 	}
58 	---
59 
60 	It provides an audio thread, input scaffold, and helper functions.
61 
62 
63 	The MyGame handler is actually a template, so you don't have virtual
64 	function indirection and not all functions are required. The interfaces
65 	are just to help you get the signatures right, they don't force virtual
66 	dispatch at runtime.
67 +/
68 module arsd.gamehelpers;
69 
70 public import arsd.color;
71 public import arsd.simpledisplay;
72 public import arsd.simpleaudio;
73 
74 import std.math;
75 public import core.time;
76 
77 public import arsd.joystick;
78 
79 SimpleWindow create2dWindow(string title, int width = 512, int height = 512) {
80 	auto window = new SimpleWindow(width, height, title, OpenGlOptions.yes);
81 
82 	window.setAsCurrentOpenGlContext();
83 
84 	glEnable(GL_BLEND);
85 	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
86 	glClearColor(0,0,0,0);
87 	glDepthFunc(GL_LEQUAL);
88 
89 	glMatrixMode(GL_PROJECTION);
90 	glLoadIdentity();
91 	glOrtho(0, width, height, 0, 0, 1);
92 
93 	glMatrixMode(GL_MODELVIEW);
94 	glLoadIdentity();
95 	glDisable(GL_DEPTH_TEST);
96 	glEnable(GL_TEXTURE_2D);
97 
98 	return window;
99 }
100 
101 /// This is the base class for your game.
102 class GameHelperBase {
103 	/// Implement this to draw.
104 	abstract void drawFrame();
105 
106 	/// Implement this to update. The deltaTime tells how much real time has passed since the last update.
107 	/// Returns true if anything changed, which will queue up a redraw
108 	abstract bool update(Duration deltaTime);
109 	//abstract void fillAudioBuffer(short[] buffer);
110 
111 	/// Returns the main game window. This function will only be
112 	/// called once if you use runGame. You should return a window
113 	/// here like one created with `create2dWindow`.
114 	abstract SimpleWindow getWindow();
115 
116 	/// Override this and return true to initialize the audio system.
117 	/// Note that trying to use the [audio] member without this will segfault!
118 	bool wantAudio() { return false; }
119 
120 	/// You must override [wantAudio] and return true for this to be valid;
121 	AudioPcmOutThread audio;
122 
123 	this() {
124 		if(wantAudio) {
125 			audio = new AudioPcmOutThread();
126 		}
127 	}
128 
129 	/// These functions help you handle user input. It offers polling functions for
130 	/// keyboard, mouse, joystick, and virtual controller input.
131 	///
132 	/// The virtual digital controllers are best to use if that model fits you because it
133 	/// works with several kinds of controllers as well as keyboards.
134 
135 	JoystickUpdate[4] joysticks;
136 	ref JoystickUpdate joystick1() { return joysticks[0]; }
137 
138 	bool[256] keyboardState;
139 
140 	VirtualController snes;
141 }
142 
143 /++
144 	The virtual controller is based on the SNES. If you need more detail, try using
145 	the joystick or keyboard and mouse members directly.
146 
147 	```
148 	 l          r
149 
150 	 U          X
151 	L R  s  S  Y A
152 	 D          B
153 	```
154 
155 	For Playstation and XBox controllers plugged into the computer,
156 	it picks those buttons based on similar layout on the physical device.
157 
158 	For keyboard control, arrows and WASD are mapped to the d-pad (ULRD in the diagram),
159 	Q and E are mapped to the shoulder buttons (l and r in the diagram).So are U and P.
160 
161 	Z, X, C, V (for when right hand is on arrows) and K,L,I,O (for left hand on WASD) are mapped to B,A,Y,X buttons.
162 
163 	G is mapped to select (s), and H is mapped to start (S).
164 
165 	The space bar and enter keys are also set to button A, with shift mapped to button B.
166 
167 
168 	Only player 1 is mapped to the keyboard.
169 +/
170 struct VirtualController {
171 	ushort state;
172 
173 	enum Button {
174 		Up, Left, Right, Down,
175 		X, A, B, Y,
176 		Select, Start, L, R
177 	}
178 
179 	bool opIndex(Button idx) {
180 		return (state & (1 << (cast(int) idx))) ? true : false;
181 	}
182 	private void opIndexAssign(bool value, Button idx) {
183 		if(value)
184 			state |= (1 << (cast(int) idx));
185 		else
186 			state &= ~(1 << (cast(int) idx));
187 	}
188 }
189 
190 /// The max rates are given in executions per second
191 /// Redraw will never be called unless there has been at least one update
192 void runGame(T : GameHelperBase)(T game, int maxUpdateRate = 20, int maxRedrawRate = 0) {
193 	// this is a template btw because then it can statically dispatch
194 	// the members instead of going through the virtual interface.
195 	if(game.audio !is null) {
196 		game.audio.start();
197 	}
198 
199 	scope(exit)
200 	if(game.audio !is null) {
201 		import std.stdio;
202 		game.audio.stop();
203 		game.audio.join();
204 		game.audio = null;
205 	}
206 
207 
208 	int joystickPlayers = enableJoystickInput();
209 	scope(exit) closeJoysticks();
210 
211 	auto window = game.getWindow();
212 
213 	window.redrawOpenGlScene = &game.drawFrame;
214 
215 	auto lastUpdate = MonoTime.currTime;
216 
217 	window.eventLoop(1000 / maxUpdateRate,
218 		delegate() {
219 			foreach(p; 0 .. joystickPlayers) {
220 				version(linux)
221 					readJoystickEvents(joystickFds[p]);
222 				auto update = getJoystickUpdate(p);
223 
224 				if(p == 0) {
225 					static if(__traits(isSame, Button, PS1Buttons)) {
226 						// PS1 style joystick mapping compiled in
227 						with(Button) with(VirtualController.Button) {
228 							if(update.buttonWasJustPressed(square)) game.snes[Y] = true;
229 							if(update.buttonWasJustPressed(triangle)) game.snes[X] = true;
230 							if(update.buttonWasJustPressed(cross)) game.snes[B] = true;
231 							if(update.buttonWasJustPressed(circle)) game.snes[A] = true;
232 							if(update.buttonWasJustPressed(select)) game.snes[Select] = true;
233 							if(update.buttonWasJustPressed(start)) game.snes[Start] = true;
234 							if(update.buttonWasJustPressed(l1)) game.snes[L] = true;
235 							if(update.buttonWasJustPressed(r1)) game.snes[R] = true;
236 							// note: no need to check analog stick here cuz joystick.d already does it for us (per old playstation tradition)
237 							if(update.axisChange(Axis.horizontalDpad) < 0 && update.axisPosition(Axis.horizontalDpad) < -20000) game.snes[Left] = true;
238 							if(update.axisChange(Axis.horizontalDpad) > 0 && update.axisPosition(Axis.horizontalDpad) > 20000) game.snes[Right] = true;
239 							if(update.axisChange(Axis.verticalDpad) < 0 && update.axisPosition(Axis.verticalDpad) < -20000) game.snes[Up] = true;
240 							if(update.axisChange(Axis.verticalDpad) > 0 && update.axisPosition(Axis.verticalDpad) > 20000) game.snes[Down] = true;
241 
242 							if(update.buttonWasJustReleased(square)) game.snes[Y] = false;
243 							if(update.buttonWasJustReleased(triangle)) game.snes[X] = false;
244 							if(update.buttonWasJustReleased(cross)) game.snes[B] = false;
245 							if(update.buttonWasJustReleased(circle)) game.snes[A] = false;
246 							if(update.buttonWasJustReleased(select)) game.snes[Select] = false;
247 							if(update.buttonWasJustReleased(start)) game.snes[Start] = false;
248 							if(update.buttonWasJustReleased(l1)) game.snes[L] = false;
249 							if(update.buttonWasJustReleased(r1)) game.snes[R] = false;
250 							if(update.axisChange(Axis.horizontalDpad) > 0 && update.axisPosition(Axis.horizontalDpad) > -20000) game.snes[Left] = false;
251 							if(update.axisChange(Axis.horizontalDpad) < 0 && update.axisPosition(Axis.horizontalDpad) < 20000) game.snes[Right] = false;
252 							if(update.axisChange(Axis.verticalDpad) > 0 && update.axisPosition(Axis.verticalDpad) > -20000) game.snes[Up] = false;
253 							if(update.axisChange(Axis.verticalDpad) < 0 && update.axisPosition(Axis.verticalDpad) < 20000) game.snes[Down] = false;
254 
255 						}
256 
257 					} else static if(__traits(isSame, Button, XBox360Buttons)) {
258 						// XBox style mapping
259 						// the reason this exists is if the programmer wants to use the xbox details, but
260 						// might also want the basic controller in here. joystick.d already does translations
261 						// so an xbox controller with the default build actually uses the PS1 branch above.
262 						/+
263 						case XBox360Buttons.a: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_A) ? true : false;
264 						case XBox360Buttons.b: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_B) ? true : false;
265 						case XBox360Buttons.x: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_X) ? true : false;
266 						case XBox360Buttons.y: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_Y) ? true : false;
267 
268 						case XBox360Buttons.lb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) ? true : false;
269 						case XBox360Buttons.rb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) ? true : false;
270 
271 						case XBox360Buttons.back: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) ? true : false;
272 						case XBox360Buttons.start: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_START) ? true : false;
273 						+/
274 					}
275 				}
276 
277 				game.joysticks[p] = update;
278 			}
279 
280 			auto now = MonoTime.currTime;
281 			bool changed = game.update(now - lastUpdate);
282 			lastUpdate = now;
283 
284 			// FIXME: rate limiting
285 			if(changed)
286 				window.redrawOpenGlSceneNow();
287 		},
288 
289 		delegate (KeyEvent ke) {
290 			game.keyboardState[ke.hardwareCode] = ke.pressed;
291 
292 			with(VirtualController.Button)
293 			switch(ke.key) {
294 				case Key.Up, Key.W: game.snes[Up] = ke.pressed; break;
295 				case Key.Down, Key.S: game.snes[Down] = ke.pressed; break;
296 				case Key.Left, Key.A: game.snes[Left] = ke.pressed; break;
297 				case Key.Right, Key.D: game.snes[Right] = ke.pressed; break;
298 				case Key.Q, Key.U: game.snes[L] = ke.pressed; break;
299 				case Key.E, Key.P: game.snes[R] = ke.pressed; break;
300 				case Key.Z, Key.K: game.snes[B] = ke.pressed; break;
301 				case Key.Space, Key.Enter, Key.X, Key.L: game.snes[A] = ke.pressed; break;
302 				case Key.C, Key.I: game.snes[Y] = ke.pressed; break;
303 				case Key.V, Key.O: game.snes[X] = ke.pressed; break;
304 				case Key.G: game.snes[Select] = ke.pressed; break;
305 				case Key.H: game.snes[Start] = ke.pressed; break;
306 				case Key.Shift, Key.Shift_r: game.snes[B] = ke.pressed; break;
307 				default:
308 			}
309 		}
310 	);
311 }
312 
313 /++
314 	Simple class for putting a TrueColorImage in as an OpenGL texture.
315 
316 	Doesn't do mipmapping btw.
317 +/
318 final class OpenGlTexture {
319 	private uint _tex;
320 	private int _width;
321 	private int _height;
322 	private float _texCoordWidth;
323 	private float _texCoordHeight;
324 
325 	/// Calls glBindTexture
326 	void bind() {
327 		glBindTexture(GL_TEXTURE_2D, _tex);
328 	}
329 
330 	/// For easy 2d drawing of it
331 	void draw(Point where, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) {
332 		draw(where.x, where.y, width, height, rotation, bg);
333 	}
334 
335 	///
336 	void draw(float x, float y, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) {
337 		glPushMatrix();
338 		glTranslatef(x, y, 0);
339 
340 		if(width == 0)
341 			width = this.originalImageWidth;
342 		if(height == 0)
343 			height = this.originalImageHeight;
344 
345 		glTranslatef(cast(float) width / 2, cast(float) height / 2, 0);
346 		glRotatef(rotation, 0, 0, 1);
347 		glTranslatef(cast(float) -width / 2, cast(float) -height / 2, 0);
348 
349 		glColor4f(cast(float)bg.r/255.0, cast(float)bg.g/255.0, cast(float)bg.b/255.0, cast(float)bg.a / 255.0);
350 		glBindTexture(GL_TEXTURE_2D, _tex);
351 		glBegin(GL_QUADS); 
352 			glTexCoord2f(0, 0); 				glVertex2i(0, 0);
353 			glTexCoord2f(texCoordWidth, 0); 		glVertex2i(width, 0); 
354 			glTexCoord2f(texCoordWidth, texCoordHeight); 	glVertex2i(width, height); 
355 			glTexCoord2f(0, texCoordHeight); 		glVertex2i(0, height); 
356 		glEnd();
357 
358 		glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture
359 
360 		glPopMatrix();
361 	}
362 
363 	/// Use for glTexCoord2f
364 	float texCoordWidth() { return _texCoordWidth; }
365 	float texCoordHeight() { return _texCoordHeight; } /// ditto
366 
367 	/// Returns the texture ID
368 	uint tex() { return _tex; }
369 
370 	/// Returns the size of the image
371 	int originalImageWidth() { return _width; }
372 	int originalImageHeight() { return _height; } /// ditto
373 
374 	// explicitly undocumented, i might remove this
375 	TrueColorImage from;
376 
377 	/// Make a texture from an image.
378 	this(TrueColorImage from) {
379 		bindFrom(from);
380 	}
381 
382 	/// Generates from text. Requires ttf.d
383 	/// pass a pointer to the TtfFont as the first arg (it is template cuz of lazy importing, not because it actually works with different types)
384 	this(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
385 		bindFrom(font, size, text);
386 	}
387 
388 	/// Creates an empty texture class for you to use with [bindFrom] later
389 	/// Using it when not bound is undefined behavior.
390 	this() {}
391 
392 
393 
394 	/// After you delete it with dispose, you may rebind it to something else with this.
395 	void bindFrom(TrueColorImage from) {
396 		assert(from.width > 0 && from.height > 0);
397 
398 		import core.stdc.stdlib;
399 
400 		_width = from.width;
401 		_height = from.height;
402 
403 		this.from = from;
404 
405 		auto _texWidth = _width;
406 		auto _texHeight = _height;
407 
408 		const(ubyte)* data = from.imageData.bytes.ptr;
409 		bool freeRequired = false;
410 
411 		// gotta round them to the nearest power of two which means padding the image
412 		if((_texWidth & (_texWidth - 1)) || (_texHeight & (_texHeight - 1))) {
413 			_texWidth = nextPowerOfTwo(_texWidth);
414 			_texHeight = nextPowerOfTwo(_texHeight);
415 
416 			auto n = cast(ubyte*) malloc(_texWidth * _texHeight * 4);
417 			if(n is null) assert(0);
418 			scope(failure) free(n);
419 
420 			auto size = from.width * 4;
421 			auto advance = _texWidth * 4;
422 			int at = 0;
423 			int at2 = 0;
424 			foreach(y; 0 .. from.height) {
425 				n[at .. at + size] = from.imageData.bytes[at2 .. at2+ size];
426 				at += advance;
427 				at2 += size;
428 			}
429 
430 			data = n;
431 			freeRequired = true;
432 
433 			// the rest of data will be initialized to zeros automatically which is fine.
434 		}
435 
436 		glGenTextures(1, &_tex);
437 		glBindTexture(GL_TEXTURE_2D, tex);
438 
439 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
440 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
441 		
442 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
443 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
444 		
445 		glTexImage2D(
446 			GL_TEXTURE_2D,
447 			0,
448 			GL_RGBA,
449 			_texWidth, // needs to be power of 2
450 			_texHeight,
451 			0,
452 			GL_RGBA,
453 			GL_UNSIGNED_BYTE,
454 			data);
455 
456 		assert(!glGetError());
457 
458 		_texCoordWidth = cast(float) _width / _texWidth;
459 		_texCoordHeight = cast(float) _height / _texHeight;
460 
461 		if(freeRequired)
462 			free(cast(void*) data);
463 		glBindTexture(GL_TEXTURE_2D, 0);
464 	}
465 
466 	/// ditto
467 	void bindFrom(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
468 		assert(font !is null);
469 		int width, height;
470 		auto data = font.renderString(text, size, width, height);
471 		auto image = new TrueColorImage(width, height);
472 		int pos = 0;
473 		foreach(y; 0 .. height)
474 		foreach(x; 0 .. width) {
475 			image.imageData.bytes[pos++] = 255;
476 			image.imageData.bytes[pos++] = 255;
477 			image.imageData.bytes[pos++] = 255;
478 			image.imageData.bytes[pos++] = data[0];
479 			data = data[1 .. $];
480 		}
481 		assert(data.length == 0);
482 
483 		bindFrom(image);
484 	}
485 
486 	/// Deletes the texture. Using it after calling this is undefined behavior
487 	void dispose() {
488 		glDeleteTextures(1, &_tex);
489 		_tex = 0;
490 	}
491 
492 	~this() {
493 		if(_tex > 0)
494 			dispose();
495 	}
496 }
497 
498 ///
499 void clearOpenGlScreen(SimpleWindow window) {
500 	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT);
501 }
502 
503 
504 // Some math helpers
505 
506 int nextPowerOfTwo(int v) {
507 	v--;
508 	v |= v >> 1;
509 	v |= v >> 2;
510 	v |= v >> 4;
511 	v |= v >> 8;
512 	v |= v >> 16;
513 	v++;
514 	return v;
515 }
516 
517 void crossProduct(
518 	float u1, float u2, float u3,
519 	float v1, float v2, float v3,
520 	out float s1, out float s2, out float s3)
521 {
522 	s1 = u2 * v3 - u3 * v2;
523 	s2 = u3 * v1 - u1 * v3;
524 	s3 = u1 * v2 - u2 * v1;
525 }
526 
527 void rotateAboutAxis(
528 	float theta, // in RADIANS
529 	float x, float y, float z,
530 	float u, float v, float w,
531 	out float xp, out float yp, out float zp)
532 {
533 	xp = u * (u*x + v*y + w*z) * (1 - cos(theta)) + x * cos(theta) + (-w*y + v*z) * sin(theta);
534 	yp = v * (u*x + v*y + w*z) * (1 - cos(theta)) + y * cos(theta) + (w*x - u*z) * sin(theta);
535 	zp = w * (u*x + v*y + w*z) * (1 - cos(theta)) + z * cos(theta) + (-v*x + u*y) * sin(theta);
536 }
537 
538 void rotateAboutPoint(
539 	float theta, // in RADIANS
540 	float originX, float originY,
541 	float rotatingX, float rotatingY,
542 	out float xp, out float yp)
543 {
544 	if(theta == 0) {
545 		xp = rotatingX;
546 		yp = rotatingY;
547 		return;
548 	}
549 
550 	rotatingX -= originX;
551 	rotatingY -= originY;
552 
553 	float s = sin(theta);
554 	float c = cos(theta);
555 
556 	float x = rotatingX * c - rotatingY * s;
557 	float y = rotatingX * s + rotatingY * c;
558 
559 	xp = x + originX;
560 	yp = y + originY;
561 }