1 module dgt.window; 2 import derelict.opengl; 3 import derelict.sdl2.sdl, derelict.sdl2.image, derelict.sdl2.mixer, derelict.sdl2.ttf; 4 import core.stdc.stdio, core.stdc.stdlib, core.stdc.time, core.thread; 5 6 import std.ascii; 7 import std.typecons : Nullable; 8 9 import dgt.array, dgt.camera, dgt.color, dgt.font, dgt.gamepad, dgt.geom, dgt.gl_backend, dgt.io, dgt.sound, dgt.music, dgt.particle, dgt.sprite, dgt.texture, dgt.tilemap, dgt.util; 10 11 ///The flags used to control a window's initial behavior 12 struct WindowConfig 13 { 14 bool fullscreen, resizable, borderless, minimized, maximized, input_grabbed, vsync = true; 15 16 @property package SDL_WindowFlags flags() const 17 { 18 return SDL_WINDOW_OPENGL | 19 (resizable ? SDL_WINDOW_RESIZABLE : cast(SDL_WindowFlags)0) | 20 (fullscreen ? SDL_WINDOW_FULLSCREEN : cast(SDL_WindowFlags)0) | 21 (borderless ? SDL_WINDOW_BORDERLESS : cast(SDL_WindowFlags)0) | 22 (minimized ? SDL_WINDOW_MINIMIZED : cast(SDL_WindowFlags)0) | 23 (maximized ? SDL_WINDOW_MAXIMIZED : cast(SDL_WindowFlags)0) | 24 (input_grabbed ? SDL_WINDOW_INPUT_GRABBED : cast(SDL_WindowFlags)0); 25 } 26 } 27 28 private static immutable SDL_NUM_KEYS = 284; 29 30 /** 31 The main window 32 33 Handles drawing and input 34 */ 35 struct Window 36 { 37 private: 38 SDL_Window *window; 39 GLBackend ctx; 40 bool shouldContinue = true; 41 bool[SDL_NUM_KEYS] current_keys; //The total number of SDL keys 42 bool[SDL_NUM_KEYS] previous_keys; 43 Vector mousePos = Vector(0, 0), previousMouse = Vector(0, 0); 44 bool mouseLeft = false, mouseRight = false, mouseMiddle = false, 45 mouseLeftPrevious = true, mouseRightPrevious = true, mouseMiddlePrevious = true; 46 //TODO: Add a function to wait on IO 47 Array!Particle particles; 48 int offsetX, offsetY, windowWidth, windowHeight; 49 Texture white; 50 Camera camera; 51 Array!Gamepad connectedGamepads; 52 53 @disable this(); 54 @disable this(this); 55 56 public: 57 ///If the window is drawing in UI Mode, where drawing ignores the camera 58 bool inUIMode = false; 59 ///The number of target frames per second 60 uint fps = 60; 61 ///The target aspect ratio of the window 62 float aspectRatio; 63 64 /** 65 Create a window 66 67 Params: 68 title = The window's stitle 69 width = The window width in units 70 height = The window height in units 71 config = The flags that control the behavior of the window 72 scale = the number of 'units' per pixel 73 bindToGlobal = set the global window reference to this window 74 */ 75 this(in string title, in int width, in int height, in WindowConfig config = WindowConfig(), in bool bindToGlobal = true) 76 { 77 version(Windows) 78 { 79 DerelictSDL2.load(SharedLibVersion(2, 0, 3)); 80 } 81 //Initialize libraries 82 SDL_Init(SDL_INIT_VIDEO); 83 new Thread({ 84 SDL_Init(SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER); 85 version(Windows) 86 { 87 DerelictSDL2Image.load(); 88 DerelictSDL2ttf.load(); 89 DerelictSDL2Mixer.load(); 90 } 91 IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG); 92 TTF_Init(); 93 Mix_Init(MIX_INIT_FLAC | MIX_INIT_MOD | MIX_INIT_MP3 | MIX_INIT_OGG); 94 }).start(); 95 windowWidth = width; 96 windowHeight = height; 97 window = SDL_CreateWindow(title.ptr, 98 SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 99 width, height, 100 config.flags); 101 ctx = GLBackend(window, config.vsync); 102 particles = Array!Particle(128); 103 const region = Rectangle(0, 0, width, height); 104 setTransform(Camera(region, region)); 105 106 ubyte[3] white_pixel = [ 255, 255, 255 ]; 107 white = Texture(white_pixel.ptr, 1, 1, PixelFormat.RGB); 108 glViewport(0, 0, width, height); 109 aspectRatio = cast(float)width / height; 110 111 srand(cast(uint)time(null)); 112 113 if(bindToGlobal) 114 globalWindow = &this; 115 116 thread_joinAll(); 117 Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024); 118 Mix_AllocateChannels(512); 119 connectedGamepads = Array!Gamepad(16); 120 recalculateGamepads(); 121 } 122 123 void loop(Render, Update, A...)(Render render, Update update, A state) 124 { 125 new Thread({ 126 while(isOpen) 127 { 128 update(this, state); 129 Thread.sleep(dur!"msecs"(1000 / fps)); 130 } 131 }).start(); 132 while(isOpen) 133 render(this, state); 134 } 135 136 nothrow @nogc @trusted: 137 138 private void recalculateGamepads() 139 { 140 foreach(gamepad; connectedGamepads) 141 gamepad.destroy(); 142 connectedGamepads.clear(); 143 int joystick_length = SDL_NumJoysticks(); 144 for(int i = 0; i < joystick_length; i++) 145 if(SDL_IsGameController(i)) 146 connectedGamepads.add(Gamepad(SDL_GameControllerOpen(i))); 147 } 148 149 ///Stop keeping the window alive 150 void close() 151 { 152 shouldContinue = false; 153 } 154 155 ~this() 156 { 157 foreach(gamepad; gamepads) 158 gamepad.destroy(); 159 SDL_DestroyWindow(window); 160 TTF_Quit(); 161 Mix_Quit(); 162 IMG_Quit(); 163 SDL_Quit(); 164 } 165 166 167 ///Set the camera transform 168 void setTransform(in Camera cam) 169 { 170 ctx.setTransform(cam.opengl); 171 } 172 173 /** 174 Start a frame 175 Params: 176 bg = The color to clear with 177 */ 178 void begin(in Color bg) 179 { 180 const region = Rectangle(0, 0, width, height); 181 begin(bg, Camera(region, region)); 182 } 183 184 /** 185 Start a frame 186 Params: 187 bg = The color to clear with 188 cam = the region to draw 189 */ 190 void begin(in Color bg, in Camera cam) 191 { 192 inUIMode = false; 193 ctx.clear(bg); 194 previous_keys = current_keys; 195 SDL_Event e; 196 while (shouldContinue && SDL_PollEvent(&e)) 197 { 198 switch (e.type) 199 { 200 case SDL_QUIT: 201 shouldContinue = false; 202 break; 203 case SDL_KEYDOWN: 204 current_keys[e.key.keysym.scancode] = true; 205 break; 206 case SDL_KEYUP: 207 current_keys[e.key.keysym.scancode] = false; 208 break; 209 case SDL_WINDOWEVENT: 210 switch(e.window.event) { 211 case SDL_WINDOWEVENT_RESIZED: 212 case SDL_WINDOWEVENT_SIZE_CHANGED: 213 int w, h; 214 SDL_GL_GetDrawableSize(window, &w, &h); 215 float windowRatio = cast(float)w / h; 216 offsetX = offsetY = 0; 217 if(windowRatio > aspectRatio) 218 { 219 auto oldW = w; 220 w = cast(int)(aspectRatio * h); 221 } 222 else if(windowRatio < aspectRatio) 223 { 224 auto oldH = h; 225 h = cast(int)(w / aspectRatio); 226 offsetY = (oldH - h) / 2; 227 } 228 glViewport(offsetX, offsetY, w, h); 229 windowWidth = w; 230 windowHeight = h; 231 break; 232 default: 233 break; 234 } 235 break; 236 case SDL_CONTROLLERDEVICEADDED: 237 case SDL_CONTROLLERDEVICEREMOVED: 238 case SDL_CONTROLLERDEVICEREMAPPED: 239 recalculateGamepads(); 240 break; 241 default: 242 break; 243 } 244 } 245 int x, y; 246 int button_mask = SDL_GetMouseState(&x, &y); 247 previousMouse = mousePos; 248 mousePos = Vector(x, y); 249 mouseLeftPrevious = mouseLeft; 250 mouseRightPrevious = mouseRight; 251 mouseMiddlePrevious = mouseMiddlePrevious; 252 mouseLeft = (button_mask & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0; 253 mouseRight = (button_mask & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0; 254 mouseMiddle = (button_mask & SDL_BUTTON(SDL_BUTTON_MIDDLE)) != 0; 255 setTransform(cam); 256 } 257 258 private void filterParticles(T)(in Tilemap!T map) 259 { 260 for(size_t i = 0; i < particles.length; i++) 261 { 262 switch (particles[i].behavior) 263 { 264 case ParticleBehavior.Die: 265 if (!map.empty(particles[i].position.x, particles[i].position.y)) 266 particles[i].lifetime = 0; 267 break; 268 case ParticleBehavior.Bounce: 269 if (!map.empty(particles[i].position.x + particles[i].velocity.x, particles[i].position.y)) 270 particles[i].velocity.x *= -1; 271 if (!map.empty(particles[i].position.x, particles[i].position.y + particles[i].velocity.y)) 272 particles[i].velocity.y *= -1; 273 break; 274 default: break; 275 } 276 } 277 } 278 279 ///Update particles and display the drawn objects 280 void end() 281 { 282 for (size_t i = 0; i < particles.length; i++) 283 { 284 particles[i].update(); 285 if (particles[i].lifetime <= 0) 286 { 287 particles.remove(i); 288 i--; 289 } 290 else 291 draw(particles[i].region, particles[i].position.x, particles[i].position.y); 292 } 293 ctx.flip(); 294 } 295 296 ///Update particles, check particles against the tilemap, and display the drawn objects 297 void end(T)(in Tilemap!T map) 298 { 299 filterParticles(map); 300 end(); 301 } 302 303 ///Draw a polygon with each point following the next in a circle around the edge 304 void draw(size_t Len)(in Color color, in Vector[Len] points) 305 { 306 static immutable Indices = (Len - 2) * 3; 307 static assert ( Len >= 3 ); 308 Vertex[Len] vertices; 309 GLuint[Indices] indices; 310 for (size_t i = 0; i < Len; i++) 311 { 312 auto point = points[i]; 313 vertices[i].pos.x = point.x; 314 vertices[i].pos.y = point.y; 315 vertices[i].col = color; 316 } 317 uint current = 1; 318 for (size_t i = 0; i < Indices; i += 3, current += 1) { 319 indices[i] = 0; 320 indices[i + 1] = current; 321 indices[i + 2] = current + 1; 322 } 323 ctx.add(white.id, vertices, indices); 324 } 325 326 327 /** 328 Draw a circle with a given color 329 330 The circle is actually draawn as a polygon, with NumPoints points. Increase or decrease it to increase or decrease the points on the circle 331 */ 332 void draw(size_t NumPoints = 32)(in Color color, in Circle circle) 333 { 334 Vector[NumPoints] points; //A large array of points to simulate a circle 335 auto rotation = Transform.rotate(360 / NumPoints); 336 auto pointer = Vector(0, -circle.radius); 337 for (size_t i = 0; i < NumPoints; i++) 338 { 339 points[i] = circle.center + pointer; 340 pointer = rotation * pointer; 341 } 342 draw(color, points); 343 } 344 345 ///Draw a rectangle with a color 346 void draw(in Color color, in Rectangle rect) 347 { 348 Vector[4] points = [ rect.topLeft, Vector(rect.x + rect.width, rect.y), 349 rect.topLeft + rect.size, Vector(rect.x, rect.y + rect.height)]; 350 draw(color, points); 351 } 352 353 ///Draw a texture at the given position with the given color 354 void draw(in Texture tex, in Vector position, in Color col = Color.white) 355 { 356 draw(tex, position.x, position.y, col); 357 } 358 359 ///Draw a texture at the given units with the given color 360 void draw(in Texture tex, in float x, in float y, in Color col = Color.white) 361 { 362 draw(tex, x, y, tex.size.width, tex.size.height, 0, 0, 0, 1, 1, false, false, col); 363 } 364 365 /** 366 Draw a transformed textur 367 368 Params: 369 tex = the texture 370 area = the space to draw in 371 rot = the rotation angle from 0 to 360 372 origin = the rotational origin of the image 373 scale = the scale to draw at 374 flipHorizontal = if the texture should be flipped horizontally 375 flipVertical = if the texture should be flipped vertically 376 color = the color to blend with 377 */ 378 void draw(in Texture tex, in Rectangle area, in float rotation = 0, in Vector origin = Vector(0, 0), 379 in Vector scale = Vector(1, 1), in bool flipHorizontal = false, in bool flipVertical = false, in Color color = Color.white) 380 { 381 draw(tex, area.x, area.y, area.width, area.height, rotation, origin.x, origin.y, scale.x, scale.y, flipHorizontal, flipVertical, color); 382 } 383 384 /** 385 Draw a transformed texture 386 387 Params: 388 tex = the texture 389 x = the x in units 390 y = the y in units 391 w = the width in units 392 h = the height in units 393 rot = the rotation angle from 0 to 360 394 originX = the x origin 395 originY = the y origin 396 scaleX = the x scale of the draw 397 scaleY = the y scale of the draw 398 flipHorizontal = if the texture should be flipped horizontally 399 flipVertical = if the texture should be flipped vertically 400 color = the color to blend with 401 */ 402 void draw(in Texture tex, in float x, in float y, in float w, in float h, 403 in float rot = 0, in float originX = 0, in float originY = 0, 404 in float scaleX = 1, in float scaleY = 1, 405 in bool flipHorizontal = false, in bool flipVertical = false, 406 in Color color = Color.white) 407 { 408 auto trans = Transform.identity() 409 * Transform.translate(Vector(-originX, -originY)) 410 * Transform.rotate(rot) 411 * Transform.scale(Vector(scaleX, scaleY)) 412 * Transform.translate(Vector(originX, originY)); 413 draw(tex, trans, x, y, w, h, flipHorizontal, flipVertical, color); 414 } 415 416 417 /** 418 Draw a texture with a precalculated transform 419 420 Params: 421 tex = the texture 422 x = the x in units 423 y = the y in units 424 w = the width in units 425 h = the height in units 426 flipHorizontal = if the texture should be flipped horizontally 427 flipVertical = if the texture should be flipped vertically 428 color = the color to blend with 429 */ 430 void draw(in Texture tex, in Transform trans, in float x, in float y, 431 in float w, in float h, in bool flipHorizontal = false, in bool flipVertical = false, 432 in Color color = Color.white) 433 { 434 //Calculate the destination points with the transformation 435 auto tl = trans * Vector(0, 0); 436 auto tr = trans * Vector(w, 0); 437 auto bl = trans * Vector(0, h); 438 auto br = trans * Vector(w, h); 439 440 //Calculate the source points normalized to [0, 1] 441 //The conversion factor for normalizing vectors 442 float conv_factor_x = 1.0f / tex.sourceWidth; 443 float conv_factor_y = 1.0f / tex.sourceHeight; 444 float norm_x = tex.size.x * conv_factor_x; 445 float norm_y = tex.size.y * conv_factor_y; 446 float norm_w = tex.size.width * conv_factor_x; 447 float norm_h = tex.size.height * conv_factor_y; 448 auto src_tl = Vector(norm_x, norm_y); 449 auto src_tr = Vector(norm_x + norm_w, norm_y); 450 auto src_br = Vector(norm_x + norm_w, norm_y + norm_h); 451 auto src_bl = Vector(norm_x, norm_y + norm_h); 452 if (flipHorizontal) { 453 auto tmp = src_tr; 454 src_tr = src_tl; 455 src_tl = tmp; 456 tmp = src_br; 457 src_br = src_bl; 458 src_bl = tmp; 459 } 460 if (flipVertical) { 461 auto tmp = src_tr; 462 src_tr = src_br; 463 src_br = tmp; 464 tmp = src_tl; 465 src_tl = src_bl; 466 src_bl = tmp; 467 } 468 //Add all of the vertices to the context 469 auto translate = Vector(x, y); 470 Vertex[4] vertices = [ Vertex(tl + translate, src_tl, color), 471 Vertex(tr + translate, src_tr, color), 472 Vertex(br + translate, src_br, color), 473 Vertex(bl + translate, src_bl, color)]; 474 GLuint[6] indices = [0, 1, 2, 2, 3, 0]; 475 ctx.add(tex.id, vertices, indices); 476 } 477 478 ///Draw a sprite to the screen 479 void draw(ref scope Sprite sprite) 480 { 481 sprite.update(); 482 draw(sprite.texture, sprite.x, sprite.y, sprite.width, sprite.height, 483 sprite.rotation, sprite.originX, sprite.originY, 484 sprite.scaleX, sprite.scaleY, sprite.flipX, sprite.flipY, sprite.color); 485 } 486 487 ///Draw a character using a font and find the width it took 488 float draw(ref in Font font, in char c, in float x, in float y, in Color col = Color.white) 489 { 490 Texture renderChar = font.render(c); 491 draw(renderChar, x, y, col); 492 return renderChar.size.width; 493 } 494 495 ///Draw a string using a font 496 void draw(ref in Font font, in string str, in float x, in float y, in float lineHeight = 1, in Color col = Color.white) 497 { 498 float position = 0; 499 float cursor = y; 500 //Loop from the beginning to end of the string 501 for(size_t i = 0; i < str.length; i++) 502 { 503 char c = str[i]; 504 if (c == '\t') 505 for (int j = 0; j < 4; j++) 506 position += draw(font, ' ', position + x, cursor, col); 507 else if (c == '\n') 508 { 509 position = 0; 510 cursor += font.characterHeight * lineHeight; 511 } 512 else if (c != '\r') 513 position += draw(font, c, position + x, cursor, col); 514 } 515 } 516 517 ///Draw a wrapped string that can wrap on word or by character 518 void draw(ref in Font font, in string str, in float x, in float y, 519 in float maxWidth, in Color col = Color.white, in bool wrapOnWord = true, float lineHeight = 1) 520 { 521 size_t left = 0; 522 float cursor = y; 523 while(left < str.length) 524 { 525 size_t right = str.length; 526 while(right > left && font.getSizeOfString(str[left..right]).width > maxWidth) 527 { 528 do 529 { 530 right--; 531 } while(wrapOnWord && right > left && str[right - 1].isAlphaNum); 532 } 533 if(right == left) 534 right = str.length; 535 draw(font, str[left..right], x, cursor, lineHeight, col); 536 cursor += font.getSizeOfString(str[left..right], lineHeight).height; 537 left = right; 538 } 539 } 540 541 ///Create a burst of particles using the emitter 542 void addParticleBurst(in ParticleEmitter emitter) 543 { 544 int parts = randomRange(emitter.particle_min, emitter.particle_max); 545 for (int i = 0; i < parts; i++) 546 particles.add(emitter.emit()); 547 } 548 549 ///Sets the GLSL shader, see the GL backend docs 550 void setShader(in string vertexShader, 551 in string fragmentShader, 552 in string transformAttributeName = "transform", 553 in string positionAttributeName = "position", 554 in string texPositionAttributeName = "tex_coord", 555 in string colorAttributeName = "color", 556 in string textureAttributeName = "tex", 557 in string colorOutputName = "outColor") 558 { 559 ctx.setShader(vertexShader, fragmentShader, 560 transformAttributeName, positionAttributeName, 561 texPositionAttributeName, colorAttributeName, 562 textureAttributeName, colorOutputName); 563 } 564 565 ///Checks if a key is being held down by a key name 566 bool isKeyDown(in string name) const 567 { 568 return current_keys[SDL_GetScancodeFromName(name.ptr)]; 569 } 570 571 ///Checks if key was down previously by a key name 572 bool wasKeyDown(in string name) const 573 { 574 return previous_keys[SDL_GetScancodeFromName(name.ptr)]; 575 } 576 577 private static Window* globalWindow; 578 ///Get a global instance of the window 579 public static @nogc Window* getInstance() 580 { 581 return globalWindow; 582 } 583 584 pure: 585 ///Get the position of the mouse 586 @property Vector mouse() const 587 { 588 return camera.unproject * mousePos; 589 } 590 @property bool mouseLeftPressed() const { return mouseLeft; } 591 @property bool mouseRightPressed() const { return mouseRight; } 592 @property bool mouseMiddlePressed() const { return mouseMiddle; } 593 @property bool mouseLeftReleased() const { return !mouseLeft && mouseLeftPrevious; } 594 @property bool mouseRightReleased() const { return !mouseRight && mouseRightPrevious; } 595 @property bool mouseMiddleReleased() const { return !mouseMiddle && mouseMiddlePrevious; } 596 @property bool isOpen() const { return shouldContinue; } 597 @property Gamepad[] gamepads() { return connectedGamepads.array; } 598 @property int width() const { return windowWidth; } 599 @property int height() const { return windowHeight; } 600 601 }