Retro Dungeon Crawl is a simple roguelite dungeon crawler I made in two weeks using C++ and OpenGL. I wanted to emulate the feeling of classical fantasy dungeon crawlers, like Dungeons and Dragons, where lethality is high and the ultimate reward was just treasure. You play as an adventurer wandering randomly generated rooms, wielding only a sword and a torch, hoping to find loot before the monsters eventually overwhelm you.
One of the hardest parts of the project was working with the OpenGL Utility Toolkit, or GLUT. All of the visuals were made using either 3d spheres or 2d boxes/polygons.
When working in C++, I would normally avoid using non-constant global variables. However, GLUT requires the use of global variables because it lacks the ability to pass in variables through its functions, so it couldn't be helped.
GLUT definitely gave me an appreciation for the things game engines provide out of the box, like parenting objects and collisions. The pivot of the sword was one of the most difficult aspects to get rights, as the calculations for the visuals were seperate from the logic, leading to some issues with consistency when attacking. I had to specify each corner of the sword's hitbox and rotate them correctly in line with the swing. If I had more time I probably would have attempted making a collider class.
Expand//compute sword movement and color void Sword::computeSword(double elapsedTime, float x, float y, float heldOffset, int damage) { if (elapsedTime > MINIMUM_MILLISECOND_UPDATE) { if (rotator > -90) { angle += rotator; } else { angle = angle - 90; } attack = damage; offset = heldOffset; position[0] = x; position[1] = y; position[0] += cos(convertAngleToRadians(angle)); position[1] += sin(convertAngleToRadians(angle)); left = position[0] - .5 - offset; right = position[0] + .5 + offset; bottom = position[1] + .07; top = position[1] + .12; } swordColor[0] = 1 / (attack * .1); swordColor[1] = .9 / (attack * .1); swordColor[2] = .9 / (attack * .1); } //collide sword with enemy void Sword::collideSword(float aT) { for (int i = 0; i < gEnemies.size(); i++) { //if collide with enemy if (gEnemies[i]->position[0] + gEnemies[i]->size > left && gEnemies[i]->position[0] - gEnemies[i]->size < right && gEnemies[i]->position[1] + gEnemies[i]->size > bottom && gEnemies[i]->position[1] - gEnemies[i]->size < top) { gEnemies[i]->position[0] = gEnemies[i]->prevPosition[0]; gEnemies[i]->position[1] = gEnemies[i]->prevPosition[1]; //hurt enemy (time prevents spam) damageTime = aT; if (damagePrev < damageTime) { gEnemies[i]->life -= attack; cout << gEnemies[i]->life << endl; } damagePrev = damageTime; } } }
One of the parts I'm most proud of is the way I handled procedural room generation. Each room was a grid of 18x18 squares, surrounded by walls that were 2 squares thick. The rooms would first create the door the adventurer entered from, and at least 3 more doors randomly placed within the walls.
For the interior walls, first I would make the program decide how many squares it would place, a random number between 75-175. In a loop, it would pick a random position to place the wall. If that position was taken, it would increment its placement by 1-15, and continue doing that until it found an unused cell. I also made sure that no wall would block any door by being in front of it. I made many iterations of this algorithm prior, but this one provided a surprisingly usable dungeon layout, with variation between corridors and large spaces. I used the same algorithm for placing loot and monsters within those rooms.
Expand//create the inside of the room void setRoomInt() { //create randomized room interior //each enemy and dungeon block will have a unique gGrid element srand(time(NULL)); //create a new grid //grid has x & y coords, and an in use boolean gGrid.clear(); for (int i = 0; i < 17; i++) { for (int j = 0; j < 17; j++) { gGrid.push_back(new Grid(i + 2, j + 2, false)); } } //create the randomized interior blocks of the room gDungeonInside.clear(); for (int i = 0; i < (rand() % 100 + 75); i++) { //get a random grid position int randG = rand() % (gGrid.size() - 1); //if grid position is not used, use it for dungeon block if (gGrid[randG]->used == false) { gDungeonInside.push_back(new Dungeon(gGrid[randG]->gridX, gGrid[randG]->gridY)); gGrid[randG]->used = true; } //if grid pos is used, increment grid until u find unused pos else if (gGrid[randG]->used == true) { do { randG += rand() % 14 + 1; if (randG >= gGrid.size()) { randG = 0; } } while (gGrid[randG]->used == true); gDungeonInside.push_back(new Dungeon(gGrid[randG]->gridX, gGrid[randG]->gridY)); gGrid[randG]->used = true; } } }
Ultimately, this project made me very aware of all the accessibility options offered in current game engines like Unity and Unreal. However, it was a lot of fun building this game. Maybe in the future I'll take on converting this idea into a proper game engine, and add some features it's sorely lacking.