The Making of Panic In The Dark (Part 1)

Panic in the Dark Image

Initially, the idea for Panic in the Dark came from a group think tank for the second project at the University of New Hampshire Coding Boot Camp. We effectively had two weeks to produce something amazing over the 4th of July weekend (for us New England folk, it’s like Christmas in July). The brainstorming involved all kinds of ideas from inventory management systems to scheduling applications and advanced navigation algorithms. Heck, we even toyed with the idea of an app that makes art using math equations. The major challenge was going to be staying focused over a vacation week to produce the best possible product we could. That’s when the idea for making a game came about.

Let’s talk about design. Before a single piece of code was written we had to come up with a concept for the game. We knew we wanted to have a player with limited vision running through a maze and we knew we wanted to make the game multiplayer so our peers could all play on Demo Day. I started taking notes in a Google Doc, which ended up becoming our one-page Game Design Document.

GAME DESIGN DOCUMENT:

The Game Design Document started with simple notes in Google Docs, however, it has become an ever-evolving document that outlines the high-level overview of the game. The major components we wanted to outline initially were: What type of game is it going to be? What perspective does the player have? How will players control their character? What are the objectives of the game? And, What story are we trying to tell to tie this all together? Below is what our 1-page GDD looked like leading up to the completion of the playable demo phase:

Genre: Isometric, Multiplayer, Maze Chase Game

ESRB Rating: E10+ (Content is generally suitable for ages 10 and up)

Controls: Keyboard

Thematic Setting: Post-Apocalyptic Hospital

Tech Stack: HTML5, Canvas, JavaScript, Node.js, Express, MySQL, Socket.IO

Platform(s): Browser

Story: You wake up in a post-apocalyptic hospital. As you are wandering the halls you notice the only two exits are blocked, the power flickers and you hear a groaning coming from the other room. Suddenly, the power cuts out completely and you start being chased. You can barely see a few feet in front of you, but you know you must run. You hear someone cry out, “Help!” from another room. You rush in just in time to see the other person getting attacked by zombies. The power comes back on and you notice the zombie is now dazed and unable to see. You then take out the zombie while it’s vulnerable, but once again the power starts flickering. Can you survive the human race?

Game Summary: Panic in the Dark is a zombie-themed game of cat and mouse. Players log in to face off as either humans or zombies with 2 to 8 players in an abandoned hospital where the power surges every 10 seconds. Humans can only see when the lights are on, Zombies can only see in the “dark”. When your team has a vision of the map you are the “Hunting Team” and can tag enemy players by running into them. When your team is in the dark you must run from enemy players. If you are tagged by an enemy player, you have to sit out until the next power surge in order to come back into the match.

Scoring: You accumulate points for time survived while being hunted (during darkness) and you gain points for enemies you’ve tagged while your team is hunting (you have full map vision). You do not accumulate points while you are sitting out for a round after being tagged.

Controls: Movement is done with the keyboard: Up Arrow (or W), Down Arrow (or S), Left Arrow (or A), Right Arrow (or D)

Camera: Camera is a fixed isometric view of the entire game map. The player will have limited vision around the character when they are being hunted.

BLANK CANVAS:

Now that we had a vision it was time to get started. The first thing I did was take out a pen and paper and began drawing the map. I wanted a hallway in the middle with three rooms on the top half and three more on the bottom half, each containing a mini-maze.

The first block of code I wrote to get started was:

<canvas id="ctx" width="640" height="480" style="border:1px solid #6e0000;">
    <!-- If browser does not support canvas, display error: --><p>Your browser does not support HTML5 :(</p>
</canvas>

We used canvas to create our game world. Pieces started falling together once I got a green square to move around on our canvas; it was a beautiful thing. Once I replaced the green square with a sprite and added the map as the background is when it got exciting. It started to feel like a real game that people would play.

It wasn’t until much later that I added the “darkness” layer, which for the playable demo was just another canvas on top of our map and sprite canvas. This canvas was activated by the draw function:

function draw(data, i) {
    let x = data.player[i].x;
    let y = data.player[i].y;

    let default_gco = ctx.globalCompositeOperation;
    ctxv.clearRect(0, 0, 640, 480);
    ctxv.globalAlpha = darkness;
    ctxv.fillStyle = dark_color;
    ctxv.fillRect(0, 0, WIDTH, HEIGHT);
    ctxv.globalCompositeOperation = "destination-out";
    let dark_gd = ctxv.createRadialGradient(
        x,
        y,
        vision_rd,
        x,
        y,
        vision_rd / 1.75
    );
    dark_gd.addColorStop(0, "rgba(0,0,0,0");
    dark_gd.addColorStop(1, "rgba(0,0,0,1");
    ctxv.fillStyle = dark_gd;
    ctxv.beginPath();
    ctxv.arc(x, y, vision_rd, 0, 2 * Math.PI);
    ctxv.closePath();
    ctxv.fill();
    ctxv.globalCompositeOperation = default_gco;
}

We pass two parameters into the draw function, the first is our “data,” which is a pack that contains several things, but the most important is a list of players. The second parameter is the “i” iterator to control which player in the list this function gets broadcast to.

Basically, this function takes the current player’s x and y coordinates, maps them to the x and y of the 360-degree arc in which the canvas will be transparent, and gradient out 1.75 into fully opaque darkness. It only draws the darkness for the WIDTH and HEIGHT of the canvas (640x480px).

We will have to change this setup for final release because it’s easily exploitable, but it was quick to produce and worked out perfectly for our playable demo deadline.

MAP COLLISION:

I recreated the hand-drawn game map using Tiled Map Editor and tilesets I acquired specifically for this project. Once the game world was complete, I had to figure out how to prevent our character from running through walls.

Map collision was probably one of my favorite challenges to tackle. The utility I used to draw the map has an option to export a map as JSON. This enabled me to create a new layer called “collision,” which added red tiles over all the places on the map I didn’t want the character to move. I opened up the JSON map in Visual Studio Code and was able to get a 1-D array of my collision layer. For simplicity’s sake I converted the array into a 2-D array which looks like this:

const grid =[
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,1,1,1,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1],
[1,1,0,0,1,1,1,1,1,0,0,0,0,1,1,0,0,0,1,1,1,1,0,0,0,1,1,1,0,0,0,0,1,1,1,1,0,0,1,1],
[1,1,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
[1,1,0,0,1,1,1,1,1,0,0,1,1,1,1,0,0,0,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1],
[1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1,1,1,0,0,0,0,0,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1],
[1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1],
[1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,1,1,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,1,1,1,0,1,1,0,0,1],
[1,1,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,1,1,1,1,1,1,0,0,1],
[1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,0,0,0,0,1],
[1,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,0,0,0,0,1],
[1,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],
[1,0,0,0,1,1,0,0,1,1,0,0,0,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0,1,1,0,0,0,0,0,0,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
];

Panic in the Dark ScreenshotWhen you put the array next to the map you can visually see the areas that have collision detection. The number “0” is passable terrain and the number “1” is an area the character cannot move to.

The client requests movement to the server, the server checks to see if the location is a “0” or a “1” and returns that value. If the position is > 0, then the server sends back the player’s original location (they do not move). If the position they are attempting to move to is a “0,” the server updates their x & y to reflect the new position and sends it back to the client. This comes with some minor flaws, for example, if you are pressing two directions at once and you reach collision in either direction it stops your character dead (pun intended). This has been noted in our bug report and should just require some simple logic to clean up for production.

MULTIPLAYER:

So now that the core game was in place we just had to add multiple players, easy right? It actually was using Socket.IO! The hardest part was splitting up the files from our test setup into an MVC methodology, but sending data to and from the client to the server, and vice versa, was easy. Following the documentation on the Socket.IO page I was quickly able to get “A User Connected” and “A User Has Disconnected” up and running for each instance that connected to my Express server. I then got a bit carried away on a sidebar going through the tutorial and added a chat feature to the game (something we didn’t really intend on initially but decided the class would have a lot of fun smack talking one another).

Movement was implemented starting from the client’s key-press of either the WASD or arrow keys and it would emit “keyPress” to the server. Later I added a check to make sure the chat wasn’t the active element, although it was funny watching people move in little circles as they typed.

document.onkeydown = function (event) {
    if (chatInput !== document.activeElement) {
        if (event.keyCode === 68 || event.keyCode === 39)
            //d or right arrow
            socket.emit("keyPress", { inputId: "right", state: true });
        else if (event.keyCode === 83 || event.keyCode === 40)
            //s or down arrow
            socket.emit("keyPress", { inputId: "down", state: true });
        else if (event.keyCode === 65 || event.keyCode === 37)
            //a or left arrow
            socket.emit("keyPress", { inputId: "left", state: true });
        else if (event.keyCode === 87 || event.keyCode === 38)
            // w or up arrow
            socket.emit("keyPress", { inputId: "up", state: true });
    }
};

On the server side, there is a socket listener for the “keyPress” event that our client sends. It then takes the data that was sent from the client and tells our player controller that it’s pressing a direction.

socket.on("keyPress", function (data) {
    if (data.inputId === "left") player.pressingLeft = data.state;
    else if (data.inputId === "right") player.pressingRight = data.state;
    else if (data.inputId === "up") player.pressingUp = data.state;
    else if (data.inputId === "down") player.pressingDown = data.state;
});

Now we have a way for the client to tell the server it wants to move and we have a way for the server to gather which direction the client wants it to go. We then broadcast the x and y of the character with the socket.id that sent the request to everyone and all the clients will update their canvas with the new position of the player (assuming it’s not running into a wall).

WRAPPING UP:

The playable demo was completed on time and worked flawlessly for the presentation. We were able to log 27 unique players into the game and have them running around catching one another with a live-updating leaderboard, fully functional chat that displayed users logging in and out of the game, unique spawn points that would trigger if a player was standing on a spawn location (to prevent instant death and a poor player experience), and a simple login system so each player had a unique username that displayed above their character’s head.

Looking through my logs I personally recorded 122 hours on the project over the course of 2.5 weeks, and with the input of two other group members, we totaled just under 200 hours to complete Phase One and have a fully functional, playable demo.

WHAT’S NEXT:

We are continuing work on this project to bring it into production. There were two bugs reported after our initial playable demo session that need to be fixed and some polishing up of the landing page, as well as the game client itself. We intend on animating the sprites and making each game instance one that has a clear winner. One of our most-anticipated stretch goals is to create an intelligent AI that could be played against.

The full breakdown of what is slated to bring this game into production can be found below on my Trello board:

Thank you for reading about one of my favorite projects to date, and keep an eye out for the Phase Two update by the end of August 2018!