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