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