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