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 }