For my final project I created a fully-functioning game in p5.js that will run in the browser (tested on Chrome). The premise of the game is a single or multiplayer fighting game, where the player is a square that tries to knock his/her opponent (another square) out of a ring. The squares are physics driven, making the action very natural in feel and appearance. Players have the option to "paint" their fighter using brush tools or to use pre-loaded images to personalize their fighter. Both controller and keyboard controls are supported for both players. The game tracks score between two players (or the player and an AI fighter), which resets if player 1 wishes to return to the the pre-game setup screen. The sketch accomplishes the following:
- Browser gamepad support.
- Demonstrates mastery of the p5.play.js library.
- Triggers events, such as scene changes, using sprite positions.
- Uses images pulled from wikimedia.org instead of locally-stored files.
- Play against others remotely.
- Save player-made images to a database where others can browse them.
- Make the game play well on phones
- My original idea was a game that looked like a Roomba fighting game, where users could make designs for their Roomba and save that image onto a server, so that the player could access their custom images later or share with others. Also the multiplayer aspect of the game would be accomplished over Internet connection using Socket.io.
- Long story short, socket.io could not send data from my remote server to clients fast enough to make the gameplay worth it. Instead I decided to scrap that idea and focus on getting local multiplayer with gamepads to work.
- Saving custom images fizzled out as well. For some reason or another, client programs (browsers that aren't the server) could send info to a remote server without any problem, but returning any information, even just a byte of data, took updwards of 15 minutes. Saving to and loading from a local machine would accomplish nothing and just muddie up the interface. My solution was to have several preloaded images the user could draw over easily.
- the original fighter movement scheme involved pointing and clicking on or touching the screen. The first prototype of this feature had the fighter turn towards the point and accelerate -- imagine how a normal car steers and moves. However this movement scheme had a strange glitch with trig quadrants and range. The fighters would not rotate the optimal direction when provoked (would turn in the opposite direction than the shortest distance). To make my life easier I changed the control scheme to keyboard and/or gamepad controls and implementing the final move method.
- The first feature I coded was the paint tools, and went off without a hitch and hasn't change much other than the interface.
- I wanted to display score and user names after each fight.
- I was absolutely floored by the amount of functionality granted by the p5.play library. I decided I wanted EVERYTHING -- buttons, text boxes, transition frames -- to be a sprites. This took away the headache of coding the movement and event triggers for all of the elements in the game.
- I wanted to transition between stages using some sort of animations.
- It was easier to code collisions and image mapping with a square sprite instead of a circle, so that changed too.
Brush tools and graphics:
- Pretty straight forward approach to making a brush tool. The not-straight forward part of the process came from making a p5.Renderer object that was not a visibile canvas, and then assign that renderer to the fighter's renderer. I made the renderer using the "createGraphics(x, y) function. Then, whenever the brush is activated (clicking the mouse on the geometry of the fighter), the sketch drew an ellipse/rectangle of a certain size and color based on user-set parameters. When playing the game, the fighter sprite draws the graphic in its draw() function. When playing with two players, there are two graphics: one for each fighter.
- Everything is a sprite. All of the menu buttons on the drawing screen are sprites. This allowed me to have full control over the animation, layering and functionality of the buttons through colliders and draw functions. I also made several sprites, such as the color selection pallete and "image selectors" (used when applying preloaded images), through custom objects. This made controlling everything nice and easy.
- Colors are applied to the brush by clicking on the colored squares in the pallette. Colors are changed by typing new RGB values in the input dom object, which only appears when the cursor hovers over the pallette. The hiding of the input field makes the screen transition look much neater (I got stuck trying to find a simple way to layer dom objects).
- Clicking "select image" on the screen reveals all of the sprites representing the different image options. This, unfortunately, slows the rendering down because of the number of raster images drawn to the screen and moving around. Clicking on one of these sprites draws its renderer object to the current fighter's renderer object.
- Everything that is clickable has an animation triggered by a mouse over, which serves to tell the user "hey, you can click this and it will do something."
Fighter physics and animations:
- Once again, the p5.play library came to my rescue. All of the "fancy" stuff my game accomplishes is handled by the library. Fighter movement is controlled by manipulating the velocity and rotation of each fighter. Collisions activate the "bounce" method, which triggers a custom bounce callback function I wrote.
- The bounce callback function triggers a camera shake and a particle animation I wrote using, you guessed it, the p5.play library. When the callback is called, the camera is moved into the direction of the net force from the collision (simply the sum of the velocity vector of the two fighters upon collision. It's not really force, but it's convincing enough). Then in the main draw loop I call a function that tries to move the camera back to the origin, over shooting by a decreasing amount to make the camera shake several times before rectifying itself. The particles are simply temporary sprites drawn in a loop, given a random color between yellow and orange-red, a random size between 1 and 10 pixels and a random direction. the number and speed of the pixels are determined by the "force" calculated on collision. The sprites are drawn at the midpoint between the centers of the two fighters.
- The movement of the fighters is intentionally odd for gameplay purposes. If I gave the fighters more traditional controls (independently control speed and rotation), stalemates would occur more often because there's no variation in fighter velocity. The fighters would simply hit and meet at a state of equilibrium. To combat this, I made the fighters constantly turn while moving forward. In order to go faster in a straight line, the player must trigger the two different movements quickly, making the fighter "waddle." This also makes lining up hits a bit more challenging, which leads to more interesting fights.
- On the fight stage, the ring is ALSO A SPRITE. If a fighter no longer overlaps the ring sprite, it is declared the loser, the match ends and the score of the other fighter increments.
- This was by far the trickiest part of the project. I needed a drawing stage, a practice stage, a fight stage, and a result stage. I later added a splash screen to make a spot to select the number of players. The practice stage was intended to be a place for players to "calibrate" their fighters by assigning a number of points to different fighter stats like top speed, acceleration, turn speed, mass, and size. I removed this feature because I didn't have a lot of time to pull it off and also it would overcomplicate the game. I left the stage in to give the players a chance to figure out how the "waddling" movement system works.
- Each stage is a function called by the draw loop depending on what the current stage variable value. My first attempt in stage transitions was to animate the different geometries on and off the screen. This was more work than it was worth and didn't look too good. I then tried a simple screen wipe with a black rectangle. This used my custom animation function used in a previous project. The animation was controlled by the systemclock, which also triggered when the stage variable would increment. This proved overwhelmingly combersome. Turns out my solution was with YET ANOTHER SPRITE. Basically, have a sprite thats just a rectangle the size of the screen. When the next stage is triggered, set the sprite's velocity to make it move right across the screen. when the position of the sprite is at the midpoint, the stage variable would increment, thus triggering the next stage's function. The stage elements would draw under the large sprite when it covers the screen, making a smooth transition between stages.
- I had to adjust the transition sprite's movement speed and make the next stage trigger when the position fell within a certain range to accomodate running the game on different hardware. Fast machines would cause a glitch where the next stage would not trigger. I calibrated this with trial and error until I found a happy medium between working and lokking okay.
- Other triggered events, such as the countdown clock on the fight, kept track of the system clock, which told the functions how to operate. For instance, the fighters cannot move until "FIGHT" prints on the screen, which is triggered after 2250 milliseconds. The different numbers a drawn to the screen every 750 milliseconds.
- Score is kept track between the two players as the fight stage is recalled over and over. when the drawing stage is recalled, these scores are set to zero.
- when permitted, the transition sprite's velocity increases when the A button on the gamepad or the space bar is pressed.
Other fun things:
- Gamepad support is actually accomplished through an API included Chrome and Firefox. Bascially the API generates an array of detected gamepads. Each gamepad is represented by an object, which has an array of buttons and array of axes. Each element in the button array holds a boolean indicating if the buttons is pressed or not. The array of axes holds a value between -1 and 1 for each axis of the gamepad. This means every axis of every joystick has an array element assigned to it. Therefore an XBOX 360 controller has 5 axes: x and y for each joystick and the z produced by the two triggers. The triggers are recognized as both buttons and an axis.
- When gamepads are detected, the program triggers "controller mode," which changes the instructions to regard gamepad controls. If no gamepads are detected, it will switch to "keyboard mode" and changes the instructions accordingly.
- The background images are all done in Photoshop. The draw screen has a moving background by drawing the image to gradually-incrementing coordinates.
- The music is a song I found on Soundcloud offered for free by artist Black Tiger Sex Machine, titled "Numbers." I intended to have other songs but a five-minute demo only warrents one song.
- I intended to have sound effects for collisions, but the only ones I could find for free sounded horrible and took away from the game overall. I did not have the right motivations to expell resources to produce my own sound effects.
- The "A.I." used for single player really ins't an A.I.. It simply moves towards the players current position, and randomly nudges itself right or left or stops. In actuality, the computer controlled fighter actually cheats because it does not have to "waddle" to move. Programming it to waddle effectively and intelligently engage the player would have been more trouble than it's worth.
- When playing against the computer, I randomly assign the player name. This is accomplished with two lists of 50 ish words, one adjectives the other nouns. One word from each list is selected at random and combined to create the name.