1 // FIXME: the audio thread needs to trigger an event in the event of its death too 2 3 // i could add a "time" uniform for the shaders automatically. unity does a float4 i think with ticks in it 4 // register cheat code? or even a fighting game combo.. 5 /++ 6 An add-on for simpledisplay.d, joystick.d, and simpleaudio.d 7 that includes helper functions for writing simple games (and perhaps 8 other multimedia programs). Whereas simpledisplay works with 9 an event-driven framework, arsd.game always uses a consistent 10 timer for updates. 11 12 $(PITFALL 13 I AM NO LONGER HAPPY WITH THIS INTERFACE AND IT WILL CHANGE. 14 15 At least, I am going to change the delta time over to drawFrame 16 for fractional interpolation while keeping the time step fixed. 17 18 If you depend on it the way it is, you'll want to fork. 19 ) 20 21 22 The general idea is you provide a game class which implements a minimum of 23 three functions: `update`, `drawFrame`, and `getWindow`. Your main function 24 calls `runGame!YourClass();`. 25 26 `getWindow` is called first. It is responsible for creating the window and 27 initializing your setup. Then the game loop is started, which will call `update`, 28 to update your game state, and `drawFrame`, which draws the current state. 29 30 `update` is called on a consistent timer. It should always do exactly one delta-time 31 step of your game work and the library will ensure it is called often enough to keep 32 game time where it should be with real time. `drawFrame` will be called when an opportunity 33 arises, possibly more or less often than `update` is called. `drawFrame` gets an argument 34 telling you how close it is to the next `update` that you can use for interpolation. 35 36 How, exactly, you decide to draw and update is up to you, but I strongly recommend that you 37 keep your game state inside the game class, or at least accessible from it. In other words, 38 avoid using global and static variables. 39 40 It might be easier to understand by example. Behold: 41 42 --- 43 import arsd.game; 44 45 final class MyGame : GameHelperBase { 46 /// Called when it is time to redraw the frame. The interpolate member 47 /// tells you the fraction of an update has passed since the last update 48 /// call; you can use this to make smoother animations if you like. 49 override void drawFrame(float interpolate) { 50 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT); 51 52 glLoadIdentity(); 53 54 glColor3f(1.0, 1.0, 1.0); 55 glTranslatef(x, y, 0); 56 glBegin(GL_QUADS); 57 58 glVertex2i(0, 0); 59 glVertex2i(16, 0); 60 glVertex2i(16, 16); 61 glVertex2i(0, 16); 62 63 glEnd(); 64 } 65 66 int x, y; 67 override bool update() { 68 x += 1; 69 y += 1; 70 return true; 71 } 72 73 override SimpleWindow getWindow() { 74 // if you want to use OpenGL 3 or nanovega or whatever, you can set it up in here too. 75 auto window = create2dWindow("My game"); 76 // load textures and such here 77 return window; 78 } 79 } 80 81 void main() { 82 runGame!MyGame(20 /*targetUpdateRate - shoot for 20 updates per second of game state*/); 83 // please note that it can draw faster than this; updates should be less than drawn frames per second. 84 } 85 --- 86 87 Of course, this isn't much of a game, since there's no input. The [GameHelperBase] provides a few ways for your 88 `update` function to check for user input: you can check the current state of and transition since last update 89 of a SNES-style [VirtualController] through [GameHelperBase.snes], or the computer keyboard and mouse through 90 [GameHelperBase.keyboardState] and (FIXME: expose mouse). Touch events are not implemented at this time and I have 91 no timetable for when they will be, but I do want to add them at some point. 92 93 The SNES controller is great if your game can work with it because it will automatically map to various gamepads 94 as well as to the standard computer keyboard. This gives the user a lot of flexibility in how they control the game. 95 If it doesn't though, you can try the other models. However, I don't recommend you try to mix them in the same game mode, 96 since you wouldn't want a user to accidentally trigger the controller while trying to type their name, for example. 97 98 If you just do the basics here, you'll have a working basic game. You can also get additional 99 features by implementing more functions, like `override bool wantAudio() { return true; } ` will 100 enable audio, for example. You can then trigger sounds and music to play in your `update` function. 101 102 Let's expand the example to show this: 103 104 // FIXME: paste in game2.d contents here 105 106 A game usually isn't just one thing, and it might help to separate these out. I call these [GameScreen]s. 107 The name might not be perfect, but the idea is that even a basic game might still have, for example, a 108 title screen and a gameplay screen. These are likely to have different controls, different drawing, and some 109 different state. 110 111 112 The MyGame handler is actually a template, so you don't have virtual 113 function indirection and not all functions are required. The interfaces 114 are just to help you get the signatures right, they don't force virtual 115 dispatch at runtime. 116 117 $(H2 Input) 118 119 In the overview, I mentioned that there's input available through a few means. Among the functions are: 120 121 Gamepads, mouse buttons, and keyboards: 122 wasPressed - returns true if the button was not pressed but became pressed over the update period. 123 wasReleased - returns true if the button was pressed, but was released over the update period 124 wasClicked - returns true if the button was released but became pressed and released again since you last asked without much other movement in between 125 isHeld - returns true if the button is currently held down 126 Gamepad specific (remember the keyboard emulates a basic gamepad): 127 startRecordingButtons - starts recording buttons 128 getRecordedButtons - gets the sequence of button presses with associated times 129 stopRecordingButtons - stops recording buttons 130 131 You might use this to check for things like cheat codes and fighting game style special moves. 132 Keyboard-specific: 133 startRecordingCharacters - starts recording keyboard character input 134 getRecordedCharacters - returns the characters typed since you started recording characters 135 stopRecordingCharacters - stops recording characters and clears the recording 136 137 You might use this for taking input for chat or character name selection. 138 Mouse and joystick: 139 startRecordingPath - starts recording paths, each point coming off the operating system is noted with a timestamp relative to when the recording started 140 getRecordedPath - gets the current recorded path 141 stopRecordingPath - stops recording the path and clears the recording. 142 143 You might use this for things like finding circles in Mario Party. 144 Mouse-specific: 145 // actually instead of capture/release i might make it a property of the screen. we'll see. 146 captureCursor - captures the cursor inside the window 147 releaseCursor - releases any existing capture 148 currentPosition - returns the current position over the window, in pixels, with (0,0) being the upper left. 149 changeInPosition - returns the change in position since last time you asked 150 wheelMotion - change in wheel ticks since last time you asked 151 Joystick-specific (be aware that the mouse will act as an emulated joystick): 152 currentPosition - returns the current position of the stick, 0,0 being centered and -1, 1 being the upper left corner and 1,-1 being the lower right position. Note that there is a dead zone in the middle of joysticks that does not count so minute wiggles are filtered out. 153 changeInPosition - returns the change in position since last time you asked 154 155 There may also be raw input data available, since this uses arsd.joystick. 156 157 $(H2 Window control) 158 159 FIXME: no public functions for this yet. 160 161 You can check for resizes and if the user wants to close to give you a chance to save the game before closing. You can also call `window.close();`. The library normally takes care of this for you. 162 163 Minimized windows will put the game on hold automatically. Maximize and full screen is handled automatically. You can request full screen when creating the window, or use the simpledisplay functions in runInGuiThreadAsync (but don't if you don't need to). 164 165 Showing and hiding cursor can be done in sdpy too. 166 167 Text drawing prolly shouldn't bitmap scale when the window is blown up, e.g. hidpi. Other things can just auto scale tho. The library should take care of this automatically. 168 169 You can set window title and icon when creating it too. 170 171 $(H2 Drawing) 172 173 I try not to force any one drawing model upon you. I offer four options out of the box and any opengl library has a good chance of working with appropriate setup. 174 175 The out-of-the-box choices are: 176 177 $(LIST 178 * Old-style OpenGL, 2d or 3d, with glBegin, glEnd, glRotate, etc. For text, you can use [arsd.ttf.OpenGlLimitedFont] 179 180 * New-style OpenGL, 2d or 3d, with shaders and your own math libraries. For text, you can use [arsd.ttf.OpenGlLimitedFont] with new style flag enabled. 181 182 * [Nanovega|arsd.nanovega] 2d vector graphics. Nanovega supports its own text drawing functions. 183 184 * The `BasicDrawing` functions provided by `arsd.game`. To some extent, you'll be able to mix and match these with other drawing models. It is just bare minimum functionality you might find useful made in a more concise form than even old-style opengl. 185 ) 186 187 Please note that the simpledisplay ScreenPainter will NOT work in a game `drawFrame` function. 188 189 You can switch between 2d and 3d modes when drawing either with opengl functions or with my helper functions like go2d (FIXME: not in the right module yet). 190 191 $(H3 Images) 192 193 use arsd.image and the OpenGlTexture object. 194 195 $(H3 Text) 196 197 use [OpenGlLimitedFont] and maybe [OperatingSystemFont] 198 199 $(H3 3d models) 200 201 FIXME add something 202 203 $(H2 Audio) 204 205 done through arsd.simpleaudio 206 207 $(H2 Collision detection) 208 209 Nanovega actually offers this but generally you're on your own. arsd's Rectangle functions offer some too. 210 211 $(H2 Labeling variables) 212 213 You can label and categorize variables in your game to help get and set them automatically. For example, marking them as `@Saved` and `@ResetOnNewDungeon` which you use to do batch updates. FIXME: implement this. 214 215 $(H2 Random numbers) 216 217 std.random works but might want another thing so the seed is saved with the game. 218 219 $(H2 Screenshots) 220 221 simpledisplay has a function for it. FIXME give a one-stop function here. 222 223 $(H2 Stuff missing from raylib that might be useful) 224 225 the screen space functions. the 3d model stuff. 226 227 $(H2 Online play) 228 229 FIXME: not implemented 230 231 If you make your games input strictly use the virtual controller functions, it supports multiple players. Locally, they can be multiple gamepads plugged in to the computer. Over the network, you can have multiple players connect to someone acting as a server and it sends input from each player's computers to everyone else which is exposed to the game as other virtual controllers. 232 233 The way this works is before your game actually starts running, if the game was run with the network flag (which can come from command line or through the `runGame` parameter), one player will act as the server and others will connect to them 234 235 There is also a chat function built in. 236 237 getUserChat(recipients, prompt) - tells the input system that you want to accept a user chat message. 238 drawUserChat(Point, Color, Font) - returns null if not getting user chat, otherwise returns the current string (what about the carat?) 239 cancelGetChat - cancels a getUserChat. 240 241 sendBotChat(recipients, sender, message) - sends a chat from your program to the other users (will be marked as a bot message) 242 243 getChatHistory 244 getLatestChat - returns the latest chat not yet returned, or null if none have come in recently 245 246 Chat messages take an argument defining the recipients, which you might want to limit if there are teams. 247 248 In your Game object, there is a `filterUserChat` method you can optionally implement. This is given the message they typed. If you return the message, it will send it to other players. Or you can return null to cancel sending it on the network. You might then use the chat function to implement cheat codes like the old Warcraft and Starcraft games. If the player is not connected on the network, nothing happens even if you do return a message, since there is nobody to send it to. 249 250 You can also implement a `chatHistoryLength` which tells how many messages to keep in memory. 251 252 Finally, you can send custom network messages with `sendNetworkUpdate` and `getNetworkUpdate`, which work with your own arbitrary structs that represent data packets. Each one can be sent to recipients like chat messages but this is strictly for the program to read These take an argument to decide if it should be the tcp or udp connections. 253 254 $(H2 Split screen) 255 256 When playing locally, you might want to split your window for multiple players to see. The library might offer functions to help you in future versions. Your code should realize when it is split screen and adjust the ui accordingly regardless. 257 258 $(H2 Library internals) 259 260 To better understand why things work the way they do, here's an overview of the internal architecture of the library. Much of the information here may be changed in future versions of the library, so try to think more about the concepts than the specifics as you read. 261 262 $(H3 The game clock) 263 264 $(H3 Thread layout) 265 266 It runs four threads: a UI thread, a graphics thread, an audio thread, and a game thread. 267 268 The UI thread runs your `getWindow` function, but otherwise is managed by the library. It handles input messages, window resizes, and other things. Being built on [arsd.simpledisplay], it is possible for you to do work in it with the `runInGuiThread` and `runInGuiThreadAsync` functions, which might be useful if, for example, you wanted to open other windows. But you should generally avoid it. 269 270 The graphics thread runs your `load` and `drawFrame` functions. It gets the OpenGL context bound to it after the window is created, and expects to always have it. Since OpenGL contexts cannot be simultaneously shared across two threads, this means your other functions shouldn't try to access any of these objects. (It is possible to release the context from one thread, then attach it in another - indeed, the library does this between `getWindow` and `load` - but doing this in your user code is not supported and you'd try it at your own risk.) 271 272 The audio thread is created if `wantAudio` is true and is communicated to via the `audio` object in your game class. The library manages it for you and the methods in the `audio` object tell it what to do. You are permitted to call these from your `update` function, or to load sound assets from your `load` function. 273 274 Finally, the game thread is responsible for running your `update` function at a regular interval. The library coordinates sharing your game state between it and the graphics thread with a mutex. You can get more fine-grained control over this by overriding `updateWithManualLock`. The default is for `drawFrame` and `update` to never run simultaneously to keep data sharing to a minimum, but if you know what you're doing, you can make the lock time very very small by limiting the amount of writable data is actually shared. The default is what it is to keep things simple for you and should work most the time, though. 275 276 Most computer programs are written either as batch processors or as event-driven applications. Batch processors do their work when requested, then exit. Event-driven applications, including many video games, wait for something to happen, like the user pressing a key or clicking the mouse, respond to it, then go back to waiting. These might do some animations, but this is the exception to its run time, not the rule. You are assumed to be waiting for events, but can `requestAnimationFrame` for the special occasions. 277 278 But this is the rule for the third category of programs: time-driven programs, and many video games fall into this category. This is what `arsd.game` tries to make easy. It assumes you want a timed `update` and a steady stream of animation frames, and if you want to make an exception, you can pause updates until an event comes in. FIXME: `pauseUntilNextInput`. 279 280 $(H3 Webassembly implementation) 281 282 See_Also: 283 [arsd.ttf.OpenGlLimitedFont] 284 285 History: 286 The [GameHelperBase], indeed most the module, was completely redesigned in November 2022. If you 287 have code that depended on the old way, you're probably better off keeping a copy of the old module 288 and not updating it again. 289 290 However, if you want to update it, you can approximate the old behavior by making a single `GameScreen` 291 and moving most your code into it, especially the `drawFrame` and `update` methods, and returning that 292 as the `firstScreen`. 293 +/ 294 module arsd.game; 295 296 /+ 297 Platformer demo: 298 dance of sugar plum fairy as you are the fairy jumping around 299 Board game demo: 300 good old chess 301 3d first person demo: 302 orbit simulator. your instruments show the spacecraft orientation relative to direction of motion (0 = prograde, 180 = retrograde yaw then the pitch angle relative to the orbit plane with up just being a thing) and your orbit params (apogee, perigee, phase, etc. also show velocity and potential energy relative to planet). and your angular velocity in three dimensions 303 304 you just kinda fly around. goal is to try to actually transfer to another station successfully. 305 306 play blue danube song lol 307 308 +/ 309 310 311 // i will want to keep a copy of these that the events update, then the pre-frame update call just copies it in 312 // just gotta remember potential cross-thread issues; the write should prolly be protected by a mutex so it all happens 313 // together when the frame begins 314 struct VirtualJoystick { 315 // the mouse sets one thing and the right stick sets another 316 // both will update it, so hopefully people won't move mouse and joystick at the same time. 317 private float[2] currentPosition_ = 0.0; 318 private float[2] positionLastAsked_ = 0.0; 319 320 float[2] currentPosition() { 321 return currentPosition_; 322 } 323 324 float[2] changeInPosition() { 325 auto tmp = positionLastAsked_; 326 positionLastAsked_ = currentPosition_; 327 return [currentPosition_[0] - tmp[0], currentPosition_[1] - tmp[1]]; 328 } 329 330 } 331 332 struct MouseAccess { 333 // the mouse buttons can be L and R on the virtual gamepad 334 int[2] currentPosition_; 335 } 336 337 struct KeyboardAccess { 338 // state based access 339 340 int lastChange; // in terms of the game clock's frame counter 341 342 void startRecordingCharacters() { 343 344 } 345 346 string getRecordedCharacters() { 347 return ""; 348 } 349 350 void stopRecordingCharacters() { 351 352 } 353 } 354 355 struct MousePath { 356 static struct Waypoint { 357 // Duration timestamp 358 // x, y 359 // button flags 360 } 361 362 Waypoint[] path; 363 364 } 365 366 struct JoystickPath { 367 static struct Waypoint { 368 // Duration timestamp 369 // x, y 370 // button flags 371 } 372 373 Waypoint[] path; 374 } 375 376 /++ 377 See [GameScreen] for the thing you are supposed to use. This is just for internal use by the arsd.game library. 378 +/ 379 class GameScreenBase { 380 abstract inout(GameHelperBase) game() inout; 381 abstract void update(); 382 abstract void drawFrame(float interpolate); 383 abstract void load(); 384 385 private bool loaded; 386 } 387 388 /+ 389 you ask for things to be done - foo(); 390 and other code asks you to do things - foo() { } 391 392 393 Recommended drawing methods: 394 old opengl 395 new opengl 396 nanovega 397 398 FIXME: 399 for nanovega, load might want a withNvg() 400 both load and drawFrame might want a nvgFrame() 401 402 game.nvgFrame((nvg) { 403 404 }); 405 +/ 406 407 /++ 408 Tip: if your screen is a generic component reused across many games, you might pass `GameHelperBase` as the `Game` parameter. 409 +/ 410 class GameScreen(Game) : GameScreenBase { 411 private Game game_; 412 413 // convenience accessors 414 final AudioOutputThread audio() { 415 if(this is null || game is null) return AudioOutputThread.init; 416 return game.audio; 417 } 418 419 final VirtualController snes() { 420 if(this is null || game is null) return VirtualController.init; 421 return game.snes; 422 } 423 424 /+ 425 manual draw mode turns off the automatic timer to render and only 426 draws when you specifically trigger it. might not be worth tho. 427 +/ 428 429 430 // You are not supposed to call this. 431 final void setGame(Game game) { 432 assert(game_ is null); 433 assert(game !is null); 434 this.game_ = game; 435 } 436 437 /++ 438 Gives access to your game object for use through the screen. 439 +/ 440 public override inout(Game) game() inout { 441 if(game_ is null) 442 throw new Exception("The game screen isn't showing!"); 443 return game_; 444 } 445 446 /++ 447 `update`'s responsibility is to: 448 449 $(LIST 450 * Process player input 451 * Update game state - object positions, do collision detection, etc. 452 * Run any character AI 453 * Kick off any audio associated with changes in this update 454 * Transition to other screens if appropriate 455 ) 456 457 It is NOT supposed to: 458 459 $(LIST 460 * draw - that's the job of [drawFrame] 461 * load files, bind textures, or similar - that's the job of [load] 462 * set uniforms or other OpenGL objects - do one-time things in [load] and per-frame things in [drawFrame] 463 ) 464 +/ 465 override abstract void update(); 466 467 /++ 468 `drawFrame`'s responsibility is to draw a single frame. It can use the `interpolate` method to smooth animations between updates. 469 470 It should NOT change any variables in the game state or attempt to do things like collision detection - that's [update]'s job. When interpolating, just assume the objects are going to keep doing what they're doing. 471 472 It should also NOT load any files, create textures, or any other setup task - [load] is supposed to have already done that. 473 +/ 474 override abstract void drawFrame(float interpolate); 475 476 /++ 477 Load your graphics and other assets in this function. You are allowed to draw to the screen while loading, but note you'll have to manage things like buffer swapping yourself if you do. [drawFrame] and [update] will be paused until loading is complete. This function will be called exactly once per screen object, right as it is first shown. 478 +/ 479 override void load() {} 480 } 481 482 /// ditto 483 //alias GenericGameScreen = GameScreen!GameHelperBase; 484 485 /// 486 unittest { 487 // The TitleScreen has a simple job: show the title until the user presses start. After that, it will progress to the GameplayScreen. 488 489 static // exclude from docs 490 class DemoGame : GameHelperBase { 491 // I put this inside DemoGame for this demo, but you could define them in separate files if you wanted to 492 static class TitleScreen : GameScreen!DemoGame { 493 override void update() { 494 // you can always access your main Game object through the screen objects 495 if(game.snes[VirtualController.Button.Start]) { 496 //game.showScreen(new GameplayScreen()); 497 } 498 } 499 500 override void drawFrame(float interpolate) { 501 502 } 503 } 504 505 // and the minimum boilerplate the game itself must provide for the library 506 // is the window it wants to use and the first screen to load into it. 507 override TitleScreen firstScreen() { 508 return new TitleScreen(); 509 } 510 511 override SimpleWindow getWindow() { 512 auto window = create2dWindow("Demo game"); 513 return window; 514 } 515 } 516 517 void main() { 518 runGame!DemoGame(); 519 } 520 521 main(); // exclude from docs 522 } 523 524 /+ 525 Networking helper: just send/receive messages and manage some connections 526 527 It might offer a controller queue you can put local and network events in to get fair lag and transparent ultiplayer 528 529 split screen?!?! 530 531 +/ 532 533 /+ 534 ADD ME: 535 Animation helper like audio style. Your game object 536 has a particular image attached as primary. 537 538 You can be like `animate once` or `animate indefinitely` 539 and it takes care of it, then set new things and it does that too. 540 +/ 541 542 public import arsd.gamehelpers; 543 public import arsd.color; 544 public import arsd.simpledisplay; 545 public import arsd.simpleaudio; 546 547 import std.math; 548 public import core.time; 549 550 public import arsd.joystick; 551 552 /++ 553 Creates a simple 2d opengl simpledisplay window. It sets the matrix for pixel coordinates and enables alpha blending and textures. 554 +/ 555 SimpleWindow create2dWindow(string title, int width = 512, int height = 512) { 556 auto window = new SimpleWindow(width, height, title, OpenGlOptions.yes); 557 558 window.setAsCurrentOpenGlContext(); 559 560 glEnable(GL_BLEND); 561 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 562 glClearColor(0,0,0,0); 563 glDepthFunc(GL_LEQUAL); 564 565 glMatrixMode(GL_PROJECTION); 566 glLoadIdentity(); 567 glOrtho(0, width, height, 0, 0, 1); 568 569 glMatrixMode(GL_MODELVIEW); 570 glLoadIdentity(); 571 glDisable(GL_DEPTH_TEST); 572 glEnable(GL_TEXTURE_2D); 573 574 window.windowResized = (newWidth, newHeight) { 575 int x, y, w, h; 576 577 // FIXME: this works for only square original sizes 578 if(newWidth < newHeight) { 579 w = newWidth; 580 h = newWidth * height / width; 581 x = 0; 582 y = (newHeight - h) / 2; 583 } else { 584 w = newHeight * width / height; 585 h = newHeight; 586 x = (newWidth - w) / 2; 587 y = 0; 588 } 589 590 glViewport(x, y, w, h); 591 window.redrawOpenGlSceneSoon(); 592 }; 593 594 return window; 595 } 596 597 /++ 598 This is the base class for your game. Create a class based on this, then pass it to [runGame]. 599 +/ 600 abstract class GameHelperBase { 601 /++ 602 Implement this to draw. 603 604 The `interpolateToNextFrame` argument tells you how close you are to the next frame. You should 605 take your current state and add the estimated next frame things multiplied by this to get smoother 606 animation. interpolateToNextFrame will always be >= 0 and < 1.0. 607 608 History: 609 Previous to August 27, 2022, this took no arguments. It could thus not interpolate frames! 610 +/ 611 deprecated("Move to void drawFrame(float) in a GameScreen instead") void drawFrame(float interpolateToNextFrame) { 612 if(currentScreen is null) 613 return; 614 if(!currentScreen.loaded) { 615 // FIXME: unpause the update thread when it is done 616 currentScreen.load(); 617 currentScreen.loaded = true; 618 return; 619 } 620 currentScreen.drawFrame(interpolateToNextFrame); 621 } 622 623 ushort snesRepeatRate() { return ushort.max; } 624 ushort snesRepeatDelay() { return snesRepeatRate(); } 625 626 /++ 627 Implement this to update your game state by a single fixed timestep. You should 628 check for user input state here. 629 630 Return true if something visibly changed to queue a frame redraw asap. 631 632 History: 633 Previous to August 27, 2022, this took an argument. This was a design flaw. 634 +/ 635 deprecated("Move to void update in a GameScreen instead") bool update() { return false; } 636 637 /+ 638 override this to have more control over synchronization 639 640 its main job is to lock on `this` and update what [update] changes 641 and call `bookkeeping` while inside the lock 642 643 but if you have some work that can be done outside the lock - things 644 that are read-only on the game state - you might split it up here and 645 batch your update. as long as nothing that the [drawFrame] needs is mutated 646 outside the lock you'll be ok. 647 648 History: 649 Added November 12, 2022 650 +/ 651 bool updateWithManualLock(scope void delegate() bookkeeping) shared { 652 if(currentScreen is null) 653 return false; 654 synchronized(this) { 655 (cast() this).currentScreen.update(); 656 bookkeeping(); 657 return false; 658 } 659 } 660 //abstract void fillAudioBuffer(short[] buffer); 661 662 /++ 663 Returns the main game window. This function will only be 664 called once if you use runGame. You should return a window 665 here like one created with `create2dWindow`. 666 +/ 667 abstract SimpleWindow getWindow(); 668 669 /++ 670 Override this and return true to initialize the audio system. If you return `true` 671 here, the [audio] member can be used. 672 +/ 673 bool wantAudio() { return false; } 674 675 /++ 676 Override this and return true if you are compatible with separate render and update threads. 677 +/ 678 bool multithreadCompatible() { return true; } 679 680 /// You must override [wantAudio] and return true for this to be valid; 681 AudioOutputThread audio; 682 683 this() { 684 audio = AudioOutputThread(wantAudio()); 685 } 686 687 protected bool redrawForced; 688 689 private GameScreenBase currentScreen; 690 691 /+ 692 // it will also need a configuration in time and such 693 enum ScreenTransition { 694 none, 695 crossFade 696 } 697 +/ 698 699 /++ 700 Shows the given screen, making it actively responsible for drawing and updating, 701 optionally through the given transition effect. 702 +/ 703 void showScreen(this This, Screen)(Screen cs, GameScreenBase transition = null) { 704 cs.setGame(cast(This) this); 705 currentScreen = cs; 706 // FIXME: pause the update thread here, and fast forward the game clock when it is unpaused 707 // (this actually SHOULD be called from the update thread, except for the initial load... and even that maybe it will then) 708 // but i have to be careful waiting here because it can deadlock with teh mutex still locked. 709 } 710 711 /++ 712 Returns the first screen of your game. 713 +/ 714 abstract GameScreenBase firstScreen(); 715 716 /++ 717 Returns the number of game updates per second your game is designed for. 718 719 This isn't necessarily the number of frames drawn per second, which may be more 720 or less due to frame skipping and interpolation, but it is the number of times 721 your screen's update methods will be called each second. 722 723 You actually want to make this as small as possible without breaking your game's 724 physics and feeling of responsiveness to the controls. Remember, the display FPS 725 is different - you can interpolate frames for smooth animation. What you want to 726 ensure here is that the design fps is big enough that you don't have problems like 727 clipping through walls or sluggishness in player control, but not so big that the 728 computer is busy doing collision detection, etc., all the time and has no time 729 left over to actually draw the game. 730 731 I personally find 20 actually works pretty well, though the default set here is 60 732 due to how common that number is. You are encouraged to override this and use what 733 works for you. 734 +/ 735 int designFps() { return 60; } 736 737 /// Forces a redraw even if update returns false 738 final public void forceRedraw() { 739 redrawForced = true; 740 } 741 742 /// These functions help you handle user input. It offers polling functions for 743 /// keyboard, mouse, joystick, and virtual controller input. 744 /// 745 /// The virtual digital controllers are best to use if that model fits you because it 746 /// works with several kinds of controllers as well as keyboards. 747 748 JoystickUpdate[4] joysticks; 749 ref JoystickUpdate joystick1() { return joysticks[0]; } 750 751 bool[256] keyboardState; 752 753 // FIXME: add a mouse position and delta thing too. 754 755 /++ 756 757 +/ 758 VirtualController snes; 759 } 760 761 /++ 762 The virtual controller is based on the SNES. If you need more detail, try using 763 the joystick or keyboard and mouse members directly. 764 765 ``` 766 l r 767 768 U X 769 L R s S Y A 770 D B 771 ``` 772 773 For Playstation and XBox controllers plugged into the computer, 774 it picks those buttons based on similar layout on the physical device. 775 776 For keyboard control, arrows and WASD are mapped to the d-pad (ULRD in the diagram), 777 Q and E are mapped to the shoulder buttons (l and r in the diagram).So are U and P. 778 779 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. 780 781 G is mapped to select (s), and H is mapped to start (S). 782 783 The space bar and enter keys are also set to button A, with shift mapped to button B. 784 785 Additionally, the mouse is mapped to the virtual joystick, and mouse buttons left and right are mapped to shoulder buttons L and R. 786 787 788 Only player 1 is mapped to the keyboard. 789 +/ 790 struct VirtualController { 791 ushort previousState; 792 ushort state; 793 794 // for key repeat 795 ushort truePreviousState; 796 ushort lastStateChange; 797 bool repeating; 798 799 /// 800 enum Button { 801 Up, Left, Right, Down, 802 X, A, B, Y, 803 Select, Start, L, R 804 } 805 806 @nogc pure nothrow @safe: 807 808 /++ 809 History: Added April 30, 2020 810 +/ 811 bool justPressed(Button idx) const { 812 auto before = (previousState & (1 << (cast(int) idx))) ? true : false; 813 auto after = (state & (1 << (cast(int) idx))) ? true : false; 814 return !before && after; 815 } 816 /++ 817 History: Added April 30, 2020 818 +/ 819 bool justReleased(Button idx) const { 820 auto before = (previousState & (1 << (cast(int) idx))) ? true : false; 821 auto after = (state & (1 << (cast(int) idx))) ? true : false; 822 return before && !after; 823 } 824 825 /+ 826 +/ 827 828 VirtualJoystick stick; 829 830 /// 831 bool opIndex(Button idx) const { 832 return (state & (1 << (cast(int) idx))) ? true : false; 833 } 834 private void opIndexAssign(bool value, Button idx) { 835 if(value) 836 state |= (1 << (cast(int) idx)); 837 else 838 state &= ~(1 << (cast(int) idx)); 839 } 840 } 841 842 struct ButtonCheck { 843 bool wasPressed() { 844 return false; 845 } 846 bool wasReleased() { 847 return false; 848 } 849 bool wasClicked() { 850 return false; 851 } 852 bool isHeld() { 853 return false; 854 } 855 856 bool opCast(T : bool)() { 857 return isHeld(); 858 } 859 } 860 861 /++ 862 Deprecated, use the other overload instead. 863 864 History: 865 Deprecated on May 9, 2020. Instead of calling 866 `runGame(your_instance);` run `runGame!YourClass();` 867 instead. If you needed to change something in the game 868 ctor, make a default constructor in your class to do that 869 instead. 870 +/ 871 deprecated("Use runGame!YourGameType(updateRate, redrawRate); instead now.") 872 void runGame()(GameHelperBase game, int targetUpdateRate = 20, int maxRedrawRate = 0) { assert(0, "this overload is deprecated, use runGame!YourClass instead"); } 873 874 /++ 875 Runs your game. It will construct the given class and destroy it at end of scope. 876 Your class must have a default constructor and must implement [GameHelperBase]. 877 Your class should also probably be `final` for a small, but easy performance boost. 878 879 $(TIP 880 If you need to pass parameters to your game class, you can define 881 it as a nested class in your `main` function and access the local 882 variables that way instead of passing them explicitly through the 883 constructor. 884 ) 885 886 Params: 887 targetUpdateRate = The number of game state updates you get per second. You want this to be quick enough that players don't feel input lag, but conservative enough that any supported computer can keep up with it easily. 888 maxRedrawRate = The maximum draw frame rate. 0 means it will only redraw after a state update changes things. It will be automatically capped at the user's monitor refresh rate. Frames in between updates can be interpolated or skipped. 889 +/ 890 void runGame(T : GameHelperBase)(int targetUpdateRate = 0, int maxRedrawRate = 0) { 891 892 auto game = new T(); 893 scope(exit) .destroy(game); 894 895 if(targetUpdateRate == 0) 896 targetUpdateRate = game.designFps(); 897 898 // this is a template btw because then it can statically dispatch 899 // the members instead of going through the virtual interface. 900 901 auto window = game.getWindow(); 902 game.showScreen(game.firstScreen()); 903 904 auto lastUpdate = MonoTime.currTime; 905 bool isImmediateUpdate; 906 907 int joystickPlayers; 908 909 window.redrawOpenGlScene = null; 910 911 /* 912 The game clock should always be one update ahead of the real world clock. 913 914 If it is behind the real world clock, it needs to run update faster, so it will 915 double up on its timer to try to update and skip some render frames to make cpu time available. 916 Generally speaking the render should never be more than one full frame ahead of the game clock, 917 and since the game clock should always be a bit ahead of the real world clock, if the game clock 918 is behind the real world clock, time to skip. 919 920 If there's a huge jump in the real world clock - more than a couple seconds between 921 updates - this probably indicates the computer went to sleep or something. We can't 922 catch up, so this will just resync the clock to real world and not try to catch up. 923 */ 924 MonoTime gameClock; 925 // FIXME: render thread should be lower priority than the ui thread 926 927 int rframeCounter = 0; 928 auto drawer = delegate bool() { 929 if(gameClock is MonoTime.init) 930 return false; // can't draw uninitialized info 931 auto time = MonoTime.currTime; 932 if(gameClock + (1000.msecs / targetUpdateRate) < time) { 933 import std.stdio; writeln("frame skip ", gameClock, " vs ", time); 934 return false; // we're behind on updates, skip this frame 935 } 936 937 if(false && isImmediateUpdate) { 938 game.drawFrame(0.0); 939 isImmediateUpdate = false; 940 } else { 941 auto now = MonoTime.currTime - lastUpdate; 942 Duration nextFrame = msecs(1000 / targetUpdateRate); 943 auto delta = cast(float) ((nextFrame - now).total!"usecs") / cast(float) nextFrame.total!"usecs"; 944 945 if(delta < 0) { 946 return false; // the render is too far ahead of the updater! time to skip frames to let it catch up 947 } 948 949 game.drawFrame(1.0 - delta); 950 } 951 952 rframeCounter++; 953 if(rframeCounter % 60 == 0) { 954 import std.stdio; 955 writeln("frame"); 956 } 957 958 return true; 959 }; 960 961 import core.thread; 962 import core..volatile; 963 Thread renderThread; 964 Thread updateThread; 965 966 // shared things to communicate with threads 967 ubyte exit; 968 ulong newWindowSize; 969 ubyte loadRequired; // if the screen changed and you need to call load again in the render thread 970 971 ubyte workersPaused; 972 // Event unpauseRender; // maybe a manual reset so you set it then reset after unpausing 973 // Event unpauseUpdate; 974 975 // the input buffers should prolly be double buffered generally speaking 976 977 // FIXME: i might just want an asset cache thing 978 // FIXME: ffor audio, i want to be able to play a sound to completion without necessarily letting it play twice simultaneously and then replay it later. this would be a sound effect thing. but you might also play it twice anyway if there's like two shots so meh. and then i'll need BGM controlling in the game and/or screen. 979 980 Timer renderTimer; 981 Timer updateTimer; 982 983 auto updater = delegate() { 984 if(gameClock is MonoTime.init) { 985 gameClock = MonoTime.currTime; 986 } 987 988 foreach(p; 0 .. joystickPlayers) { 989 version(linux) 990 readJoystickEvents(joystickFds[p]); 991 auto update = getJoystickUpdate(p); 992 993 if(p == 0) { 994 static if(__traits(isSame, Button, PS1Buttons)) { 995 // PS1 style joystick mapping compiled in 996 with(Button) with(VirtualController.Button) { 997 // so I did the "wasJustPressed thing because it interplays 998 // better with the keyboard as well which works on events... 999 if(update.buttonWasJustPressed(square)) game.snes[Y] = true; 1000 if(update.buttonWasJustPressed(triangle)) game.snes[X] = true; 1001 if(update.buttonWasJustPressed(cross)) game.snes[B] = true; 1002 if(update.buttonWasJustPressed(circle)) game.snes[A] = true; 1003 if(update.buttonWasJustPressed(select)) game.snes[Select] = true; 1004 if(update.buttonWasJustPressed(start)) game.snes[Start] = true; 1005 if(update.buttonWasJustPressed(l1)) game.snes[L] = true; 1006 if(update.buttonWasJustPressed(r1)) game.snes[R] = true; 1007 // note: no need to check analog stick here cuz joystick.d already does it for us (per old playstation tradition) 1008 if(update.axisChange(Axis.horizontalDpad) < 0 && update.axisPosition(Axis.horizontalDpad) < -8) game.snes[Left] = true; 1009 if(update.axisChange(Axis.horizontalDpad) > 0 && update.axisPosition(Axis.horizontalDpad) > 8) game.snes[Right] = true; 1010 if(update.axisChange(Axis.verticalDpad) < 0 && update.axisPosition(Axis.verticalDpad) < -8) game.snes[Up] = true; 1011 if(update.axisChange(Axis.verticalDpad) > 0 && update.axisPosition(Axis.verticalDpad) > 8) game.snes[Down] = true; 1012 1013 if(update.buttonWasJustReleased(square)) game.snes[Y] = false; 1014 if(update.buttonWasJustReleased(triangle)) game.snes[X] = false; 1015 if(update.buttonWasJustReleased(cross)) game.snes[B] = false; 1016 if(update.buttonWasJustReleased(circle)) game.snes[A] = false; 1017 if(update.buttonWasJustReleased(select)) game.snes[Select] = false; 1018 if(update.buttonWasJustReleased(start)) game.snes[Start] = false; 1019 if(update.buttonWasJustReleased(l1)) game.snes[L] = false; 1020 if(update.buttonWasJustReleased(r1)) game.snes[R] = false; 1021 if(update.axisChange(Axis.horizontalDpad) > 0 && update.axisPosition(Axis.horizontalDpad) > -8) game.snes[Left] = false; 1022 if(update.axisChange(Axis.horizontalDpad) < 0 && update.axisPosition(Axis.horizontalDpad) < 8) game.snes[Right] = false; 1023 if(update.axisChange(Axis.verticalDpad) > 0 && update.axisPosition(Axis.verticalDpad) > -8) game.snes[Up] = false; 1024 if(update.axisChange(Axis.verticalDpad) < 0 && update.axisPosition(Axis.verticalDpad) < 8) game.snes[Down] = false; 1025 } 1026 1027 } else static if(__traits(isSame, Button, XBox360Buttons)) { 1028 static assert(0); 1029 // XBox style mapping 1030 // the reason this exists is if the programmer wants to use the xbox details, but 1031 // might also want the basic controller in here. joystick.d already does translations 1032 // so an xbox controller with the default build actually uses the PS1 branch above. 1033 /+ 1034 case XBox360Buttons.a: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_A) ? true : false; 1035 case XBox360Buttons.b: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_B) ? true : false; 1036 case XBox360Buttons.x: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_X) ? true : false; 1037 case XBox360Buttons.y: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_Y) ? true : false; 1038 1039 case XBox360Buttons.lb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) ? true : false; 1040 case XBox360Buttons.rb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) ? true : false; 1041 1042 case XBox360Buttons.back: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) ? true : false; 1043 case XBox360Buttons.start: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_START) ? true : false; 1044 +/ 1045 } 1046 } 1047 1048 game.joysticks[p] = update; 1049 } 1050 1051 int runs; 1052 1053 again: 1054 1055 auto now = MonoTime.currTime; 1056 bool changed; 1057 changed = (cast(shared)game).updateWithManualLock({ lastUpdate = now; }); 1058 auto stateChange = game.snes.truePreviousState ^ game.snes.state; 1059 game.snes.previousState = game.snes.state; 1060 game.snes.truePreviousState = game.snes.state; 1061 1062 if(stateChange == 0) { 1063 game.snes.lastStateChange++; 1064 auto r = game.snesRepeatRate(); 1065 if(r != typeof(r).max && !game.snes.repeating && game.snes.lastStateChange == game.snesRepeatDelay()) { 1066 game.snes.lastStateChange = 0; 1067 game.snes.repeating = true; 1068 } else if(r != typeof(r).max && game.snes.repeating && game.snes.lastStateChange == r) { 1069 game.snes.lastStateChange = 0; 1070 game.snes.previousState = 0; 1071 } 1072 } else { 1073 game.snes.repeating = false; 1074 } 1075 1076 if(game.redrawForced) { 1077 changed = true; 1078 game.redrawForced = false; 1079 } 1080 1081 gameClock += 1.seconds / targetUpdateRate; 1082 1083 if(++runs < 3 && gameClock < MonoTime.currTime) 1084 goto again; 1085 1086 // FIXME: rate limiting 1087 // FIXME: triple buffer it. 1088 if(changed) { 1089 isImmediateUpdate = true; 1090 window.redrawOpenGlSceneSoon(); 1091 } 1092 }; 1093 1094 //window.vsync = false; 1095 1096 const maxRedrawTime = maxRedrawRate > 0 ? (1000.msecs / maxRedrawRate) : 4.msecs; 1097 1098 if(game.multithreadCompatible()) { 1099 window.redrawOpenGlScene = null; 1100 renderThread = new Thread({ 1101 // FIXME: catch exception and inform the parent 1102 int frames = 0; 1103 1104 Duration renderTime; 1105 Duration flipTime; 1106 Duration renderThrottleTime; 1107 1108 while(!volatileLoad(&exit)) { 1109 MonoTime start = MonoTime.currTime; 1110 { 1111 window.mtLock(); 1112 scope(exit) 1113 window.mtUnlock(); 1114 window.setAsCurrentOpenGlContext(); 1115 } 1116 1117 bool actuallyDrew; 1118 1119 synchronized(game) 1120 actuallyDrew = drawer(); 1121 1122 MonoTime end = MonoTime.currTime; 1123 if(actuallyDrew) { 1124 window.mtLock(); 1125 scope(exit) 1126 window.mtUnlock(); 1127 window.swapOpenGlBuffers(); 1128 } 1129 // want to ensure the vsync wait occurs here, outside the window and locks 1130 // some impls will do it on glFinish, some on the next touch of the 1131 // front buffer, hence the clear being done here. 1132 if(actuallyDrew) { 1133 glFinish(); 1134 clearOpenGlScreen(window); 1135 } 1136 MonoTime flip = MonoTime.currTime; 1137 1138 renderTime += end - start; 1139 flipTime += flip - end; 1140 1141 if(flip - start < maxRedrawTime) { 1142 renderThrottleTime += maxRedrawTime - (flip - start); 1143 Thread.sleep(maxRedrawTime - (flip - start)); 1144 } 1145 frames++; 1146 //import std.stdio; if(frames % 60 == 0) writeln("frame"); 1147 } 1148 1149 import std.stdio; 1150 writeln("Average render time: ", renderTime / frames); 1151 writeln("Average flip time: ", flipTime / frames); 1152 writeln("Average throttle time: ", renderThrottleTime / frames); 1153 }); 1154 1155 updateThread = new Thread({ 1156 // FIXME: catch exception and inform the parent 1157 int frames; 1158 1159 joystickPlayers = enableJoystickInput(); 1160 scope(exit) closeJoysticks(); 1161 1162 Duration updateTime; 1163 Duration waitTime; 1164 1165 while(!volatileLoad(&exit)) { 1166 MonoTime start = MonoTime.currTime; 1167 updater(); 1168 MonoTime end = MonoTime.currTime; 1169 1170 updateTime += end - start; 1171 1172 frames++; 1173 import std.stdio; if(frames % game.designFps == 0) writeln("update"); 1174 1175 const now = MonoTime.currTime - lastUpdate; 1176 Duration nextFrame = msecs(1000) / targetUpdateRate; 1177 const sleepTime = nextFrame - now; 1178 if(sleepTime.total!"msecs" <= 0) { 1179 // falling behind on update... 1180 } else { 1181 waitTime += sleepTime; 1182 // import std.stdio; writeln(sleepTime); 1183 Thread.sleep(sleepTime); 1184 } 1185 } 1186 1187 import std.stdio; 1188 writeln("Average update time:" , updateTime / frames); 1189 writeln("Average wait time:" , waitTime / frames); 1190 }); 1191 } else { 1192 // single threaded, vsync a bit dangeresque here since it 1193 // puts the ui thread to sleep! 1194 window.vsync = false; 1195 } 1196 1197 // FIXME: when single threaded, set the joystick here 1198 // actually just always do the joystick in the event thread regardless 1199 1200 int frameCounter; 1201 1202 auto first = window.visibleForTheFirstTime; 1203 window.visibleForTheFirstTime = () { 1204 if(first) 1205 first(); 1206 1207 if(updateThread) { 1208 updateThread.start(); 1209 } else { 1210 updateTimer = new Timer(1000 / targetUpdateRate, { 1211 frameCounter++; 1212 updater(); 1213 }); 1214 } 1215 1216 if(renderThread) { 1217 window.suppressAutoOpenglViewport = true; // we don't want the context being pulled back by the other thread now, we'll check it over here. 1218 // FIXME: set viewport prior to render if width/height changed 1219 window.releaseCurrentOpenGlContext(); // need to let the render thread take it 1220 renderThread.start(); 1221 } else { 1222 window.redrawOpenGlScene = { drawer(); }; 1223 renderTimer = new Timer(1000 / 60, { window.redrawOpenGlSceneSoon(); }); 1224 } 1225 }; 1226 1227 window.onClosing = () { 1228 volatileStore(&exit, 1); 1229 1230 if(updateTimer) { 1231 updateTimer.dispose(); 1232 updateTimer = null; 1233 } 1234 if(renderTimer) { 1235 renderTimer.dispose(); 1236 renderTimer = null; 1237 } 1238 1239 if(renderThread) { 1240 renderThread.join(); 1241 renderThread = null; 1242 } 1243 if(updateThread) { 1244 updateThread.join(); 1245 updateThread = null; 1246 } 1247 }; 1248 1249 window.eventLoop(0, 1250 delegate (KeyEvent ke) { 1251 game.keyboardState[ke.hardwareCode] = ke.pressed; 1252 1253 with(VirtualController.Button) 1254 switch(ke.key) { 1255 case Key.Up, Key.W: game.snes[Up] = ke.pressed; break; 1256 case Key.Down, Key.S: game.snes[Down] = ke.pressed; break; 1257 case Key.Left, Key.A: game.snes[Left] = ke.pressed; break; 1258 case Key.Right, Key.D: game.snes[Right] = ke.pressed; break; 1259 case Key.Q, Key.U: game.snes[L] = ke.pressed; break; 1260 case Key.E, Key.P: game.snes[R] = ke.pressed; break; 1261 case Key.Z, Key.K: game.snes[B] = ke.pressed; break; 1262 case Key.Space, Key.Enter, Key.X, Key.L: game.snes[A] = ke.pressed; break; 1263 case Key.C, Key.I: game.snes[Y] = ke.pressed; break; 1264 case Key.V, Key.O: game.snes[X] = ke.pressed; break; 1265 case Key.G: game.snes[Select] = ke.pressed; break; 1266 case Key.H: game.snes[Start] = ke.pressed; break; 1267 case Key.Shift, Key.Shift_r: game.snes[B] = ke.pressed; break; 1268 default: 1269 } 1270 } 1271 ); 1272 } 1273 1274 /++ 1275 Simple class for putting a TrueColorImage in as an OpenGL texture. 1276 +/ 1277 // Doesn't do mipmapping btw. 1278 final class OpenGlTexture { 1279 private uint _tex; 1280 private int _width; 1281 private int _height; 1282 private float _texCoordWidth; 1283 private float _texCoordHeight; 1284 1285 /// Calls glBindTexture 1286 void bind() { 1287 glBindTexture(GL_TEXTURE_2D, _tex); 1288 } 1289 1290 /// For easy 2d drawing of it 1291 void draw(Point where, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) { 1292 draw(where.x, where.y, width, height, rotation, bg); 1293 } 1294 1295 /// 1296 void draw(float x, float y, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) { 1297 glPushMatrix(); 1298 glTranslatef(x, y, 0); 1299 1300 if(width == 0) 1301 width = this.originalImageWidth; 1302 if(height == 0) 1303 height = this.originalImageHeight; 1304 1305 glTranslatef(cast(float) width / 2, cast(float) height / 2, 0); 1306 glRotatef(rotation, 0, 0, 1); 1307 glTranslatef(cast(float) -width / 2, cast(float) -height / 2, 0); 1308 1309 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); 1310 glBindTexture(GL_TEXTURE_2D, _tex); 1311 glBegin(GL_QUADS); 1312 glTexCoord2f(0, 0); glVertex2i(0, 0); 1313 glTexCoord2f(texCoordWidth, 0); glVertex2i(width, 0); 1314 glTexCoord2f(texCoordWidth, texCoordHeight); glVertex2i(width, height); 1315 glTexCoord2f(0, texCoordHeight); glVertex2i(0, height); 1316 glEnd(); 1317 1318 glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture 1319 1320 glPopMatrix(); 1321 } 1322 1323 /// Use for glTexCoord2f 1324 float texCoordWidth() { return _texCoordWidth; } 1325 float texCoordHeight() { return _texCoordHeight; } /// ditto 1326 1327 /// Returns the texture ID 1328 uint tex() { return _tex; } 1329 1330 /// Returns the size of the image 1331 int originalImageWidth() { return _width; } 1332 int originalImageHeight() { return _height; } /// ditto 1333 1334 // explicitly undocumented, i might remove this 1335 TrueColorImage from; 1336 1337 /// Make a texture from an image. 1338 this(TrueColorImage from) { 1339 bindFrom(from); 1340 } 1341 1342 /// Generates from text. Requires ttf.d 1343 /// 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) 1344 this(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) { 1345 bindFrom(font, size, text); 1346 } 1347 1348 /// Creates an empty texture class for you to use with [bindFrom] later 1349 /// Using it when not bound is undefined behavior. 1350 this() {} 1351 1352 1353 1354 /// After you delete it with dispose, you may rebind it to something else with this. 1355 void bindFrom(TrueColorImage from) { 1356 assert(from !is null); 1357 assert(from.width > 0 && from.height > 0); 1358 1359 import core.stdc.stdlib; 1360 1361 _width = from.width; 1362 _height = from.height; 1363 1364 this.from = from; 1365 1366 auto _texWidth = _width; 1367 auto _texHeight = _height; 1368 1369 const(ubyte)* data = from.imageData.bytes.ptr; 1370 bool freeRequired = false; 1371 1372 // gotta round them to the nearest power of two which means padding the image 1373 if((_texWidth & (_texWidth - 1)) || (_texHeight & (_texHeight - 1))) { 1374 _texWidth = nextPowerOfTwo(_texWidth); 1375 _texHeight = nextPowerOfTwo(_texHeight); 1376 1377 auto n = cast(ubyte*) malloc(_texWidth * _texHeight * 4); 1378 if(n is null) assert(0); 1379 scope(failure) free(n); 1380 1381 auto size = from.width * 4; 1382 auto advance = _texWidth * 4; 1383 int at = 0; 1384 int at2 = 0; 1385 foreach(y; 0 .. from.height) { 1386 n[at .. at + size] = from.imageData.bytes[at2 .. at2+ size]; 1387 at += advance; 1388 at2 += size; 1389 } 1390 1391 data = n; 1392 freeRequired = true; 1393 1394 // the rest of data will be initialized to zeros automatically which is fine. 1395 } 1396 1397 glGenTextures(1, &_tex); 1398 glBindTexture(GL_TEXTURE_2D, tex); 1399 1400 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 1401 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 1402 1403 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 1404 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 1405 1406 glTexImage2D( 1407 GL_TEXTURE_2D, 1408 0, 1409 GL_RGBA, 1410 _texWidth, // needs to be power of 2 1411 _texHeight, 1412 0, 1413 GL_RGBA, 1414 GL_UNSIGNED_BYTE, 1415 data); 1416 1417 assert(!glGetError()); 1418 1419 _texCoordWidth = cast(float) _width / _texWidth; 1420 _texCoordHeight = cast(float) _height / _texHeight; 1421 1422 if(freeRequired) 1423 free(cast(void*) data); 1424 glBindTexture(GL_TEXTURE_2D, 0); 1425 } 1426 1427 /// ditto 1428 void bindFrom(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) { 1429 assert(font !is null); 1430 int width, height; 1431 auto data = font.renderString(text, size, width, height); 1432 auto image = new TrueColorImage(width, height); 1433 int pos = 0; 1434 foreach(y; 0 .. height) 1435 foreach(x; 0 .. width) { 1436 image.imageData.bytes[pos++] = 255; 1437 image.imageData.bytes[pos++] = 255; 1438 image.imageData.bytes[pos++] = 255; 1439 image.imageData.bytes[pos++] = data[0]; 1440 data = data[1 .. $]; 1441 } 1442 assert(data.length == 0); 1443 1444 bindFrom(image); 1445 } 1446 1447 /// Deletes the texture. Using it after calling this is undefined behavior 1448 void dispose() { 1449 glDeleteTextures(1, &_tex); 1450 _tex = 0; 1451 } 1452 1453 ~this() { 1454 if(_tex > 0) 1455 dispose(); 1456 } 1457 } 1458 1459 /+ 1460 FIXME: i want to do stbtt_GetBakedQuad for ASCII and use that 1461 for simple cases especially numbers. for other stuff you can 1462 create the texture for the text above. 1463 +/ 1464 1465 /// 1466 void clearOpenGlScreen(SimpleWindow window) { 1467 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT); 1468 } 1469 1470