// WORK IN PROGRESS /++ An add-on for simpledisplay.d, joystick.d, and simpleaudio.d that includes helper functions for writing games (and perhaps other multimedia programs). Usage example: --- final class MyGame : GameHelperBase { /// Called when it is time to redraw the frame /// it will try for a particular FPS override void drawFrame() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT); glLoadIdentity(); glColor3f(1.0, 1.0, 1.0); glTranslatef(x, y, 0); glBegin(GL_QUADS); glVertex2i(0, 0); glVertex2i(16, 0); glVertex2i(16, 16); glVertex2i(0, 16); glEnd(); } int x, y; override void update(Duration deltaTime) { x += 1; y += 1; } override SimpleWindow getWindow() { auto window = create2dWindow("My game"); // load textures and such here return window; } final void fillAudioBuffer(short[] buffer) { } } void main() { auto game = new MyGame(); runGame(game, maxRedrawRate, maxUpdateRate); } --- It provides an audio thread, input scaffold, and helper functions. The MyGame handler is actually a template, so you don't have virtual function indirection and not all functions are required. The interfaces are just to help you get the signatures right, they don't force virtual dispatch at runtime. +/ module arsd.gamehelpers; public import arsd.color; public import simpledisplay; import std.math; public import core.time; public import arsd.joystick; SimpleWindow create2dWindow(string title, int width = 512, int height = 512) { auto window = new SimpleWindow(width, height, title, OpenGlOptions.yes); window.setAsCurrentOpenGlContext(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glClearColor(0,0,0,0); glDepthFunc(GL_LEQUAL); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, width, height, 0, 0, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glDisable(GL_DEPTH_TEST); glEnable(GL_TEXTURE_2D); return window; } /// This is the base class for your game. class GameHelperBase { /// Implement this to draw. abstract void drawFrame(); /// Implement this to update. The deltaTime tells how much real time has passed since the last update. abstract void update(Duration deltaTime); //abstract void fillAudioBuffer(short[] buffer); /// Returns the main game window. This function will only be /// called once if you use runGame. You should return a window /// here like one created with `create2dWindow`. abstract SimpleWindow getWindow(); /// These functions help you handle user input. It offers polling functions for /// keyboard, mouse, joystick, and virtual controller input. /// /// The virtual digital controllers are best to use if that model fits you because it /// works with several kinds of controllers as well as keyboards. JoystickUpdate joystick1; } /// The max rates are given in executions per second /// Redraw will never be called unless there has been at least one update void runGame(T : GameHelperBase)(T game, int maxUpdateRate = 20, int maxRedrawRate = 0) { // this is a template btw because then it can statically dispatch // the members instead of going through the virtual interface. int joystickPlayers = enableJoystickInput(); scope(exit) closeJoysticks(); auto window = game.getWindow(); window.redrawOpenGlScene = &game.drawFrame; auto lastUpdate = MonoTime.currTime; window.eventLoop(1000 / maxUpdateRate, delegate() { if(joystickPlayers) { version(linux) readJoystickEvents(joystickFds[0]); auto update = getJoystickUpdate(0); game.joystick1 = update; } else assert(0); auto now = MonoTime.currTime; game.update(now - lastUpdate); lastUpdate = now; // FIXME: rate limiting window.redrawOpenGlSceneNow(); }, delegate (KeyEvent ke) { // FIXME } ); } /++ Simple class for putting a TrueColorImage in as an OpenGL texture. Doesn't do mipmapping btw. +/ final class OpenGlTexture { private uint _tex; private int _width; private int _height; private float _texCoordWidth; private float _texCoordHeight; /// Calls glBindTexture void bind() { glBindTexture(GL_TEXTURE_2D, _tex); } /// For easy 2d drawing of it void draw(Point where, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) { draw(where.x, where.y, width, height, rotation, bg); } /// void draw(float x, float y, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) { glPushMatrix(); glTranslatef(x, y, 0); glRotatef(rotation, 0,0, 1); if(width == 0) width = this.originalImageWidth; if(height == 0) height = this.originalImageHeight; 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); glBindTexture(GL_TEXTURE_2D, _tex); glBegin(GL_QUADS); glTexCoord2f(0, 0); glVertex2i(0, 0); glTexCoord2f(texCoordWidth, 0); glVertex2i(width, 0); glTexCoord2f(texCoordWidth, texCoordHeight); glVertex2i(width, height); glTexCoord2f(0, texCoordHeight); glVertex2i(0, height); glEnd(); glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture glPopMatrix(); } /// Use for glTexCoord2f float texCoordWidth() { return _texCoordWidth; } float texCoordHeight() { return _texCoordHeight; } /// ditto /// Returns the texture ID uint tex() { return _tex; } /// Returns the size of the image int originalImageWidth() { return _width; } int originalImageHeight() { return _height; } /// ditto /// Make a texture from an image. this(TrueColorImage from) { assert(from.width > 0 && from.height > 0); _width = from.width; _height = from.height; auto _texWidth = _width; auto _texHeight = _height; const(ubyte)[] data = from.imageData.bytes; // gotta round them to the nearest power of two which means padding the image if((_texWidth & (_texWidth - 1)) || (_texHeight & (_texHeight - 1))) { _texWidth = nextPowerOfTwo(_texWidth); _texHeight = nextPowerOfTwo(_texHeight); auto n = new ubyte[](_texWidth * _texHeight * 4); auto size = from.width * 4; auto advance = _texWidth * 4; int at = 0; int at2 = 0; foreach(y; 0 .. from.height) { n[at .. at + size] = from.imageData.bytes[at2 .. at2+ size]; at += advance; at2 += size; } data = n[]; // the rest of data will be initialized to zeros automatically which is fine. } glGenTextures(1, &_tex); glBindTexture(GL_TEXTURE_2D, tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, _texWidth, // needs to be power of 2 _texHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, data.ptr); assert(!glGetError()); _texCoordWidth = cast(float) _width / _texWidth; _texCoordHeight = cast(float) _height / _texHeight; } /// Generates from text. Requires stb_truetype.d /// 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) this(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) { assert(font !is null); int width, height; auto data = font.renderString(text, size, width, height); auto image = new TrueColorImage(width, height); int pos = 0; foreach(y; 0 .. height) foreach(x; 0 .. width) { image.imageData.bytes[pos++] = 255; image.imageData.bytes[pos++] = 255; image.imageData.bytes[pos++] = 255; image.imageData.bytes[pos++] = data[0]; data = data[1 .. $]; } assert(data.length == 0); this(image); } ~this() { glDeleteTextures(1, &_tex); } } // Some math helpers int nextPowerOfTwo(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } void crossProduct( float u1, float u2, float u3, float v1, float v2, float v3, out float s1, out float s2, out float s3) { s1 = u2 * v3 - u3 * v2; s2 = u3 * v1 - u1 * v3; s3 = u1 * v2 - u2 * v1; } void rotateAboutAxis( float theta, // in RADIANS float x, float y, float z, float u, float v, float w, out float xp, out float yp, out float zp) { xp = u * (u*x + v*y + w*z) * (1 - cos(theta)) + x * cos(theta) + (-w*y + v*z) * sin(theta); yp = v * (u*x + v*y + w*z) * (1 - cos(theta)) + y * cos(theta) + (w*x - u*z) * sin(theta); zp = w * (u*x + v*y + w*z) * (1 - cos(theta)) + z * cos(theta) + (-v*x + u*y) * sin(theta); }