/* * IDMT Mini-Assignment 2 * * Tank Wars: a simple game of throwing projectiles * This is a skeleton with a few basic pieces filled * in. You will need to fill in the rest to make the * game work. */ // Basic information on the terrain and the tanks float[] groundLevel; // Y coordinate of the ground for each X coordinate float tank1X, tank1Y, tank2X, tank2Y; // Positions of the two tanks float tankDiameter = 30; // Diameter of the tanks float cannonLength = 25; // How long the cannon on each tank extends float tank1CannonX1, tank1CannonX2, tank1CannonY1, tank1CannonY2; float tank2CannonX1, tank2CannonX2, tank2CannonY1, tank2CannonY2; float gravity = 0.05; // Strength of gravity // Store velocity of projectile float velocity; // Store value for if projectile has collided with the ground or a tank boolean contactMade = false; // Store the diameter of the projectile int projectileSize = 8; // Current state of the game int playerHasWon = 0; // 1 if player 1 wins, 2 if player 2 wins, 0 if game in progress boolean player1Turn = true; // true if it's player 1's turn; false otherwise float tank1CannonAngle = PI/2, tank2CannonAngle = PI/2; // Direction the tank cannons are pointing float tank1CannonStrength = 3, tank2CannonStrength = 3; // Strength of intended projectile launch // Location of the projectile // Store the state of the projectile boolean projectileInMotion = false; // Store projectile position and velocity values on screen float projectilePositionX, projectilePositionY; float projectileVelocityX, projectileVelocityY; // Image object to encapsulate the background image PImage bg; // Terrain generation function adapted from: http://www.redblobgames.com/articles/noise/introduction.html // Generate varying hills and valleys for terrain. void GenerateTerain() { // Store amplitudes (randomly generated within arbritrary limits) for each // sine wave float amp[] = {random(1.5,2.5), random(0.8,1.4), random(0.1, 0.3), random(0.0001, 0.002), 0.01, 0.01}; // Store frequencies (Hz) for each sine wave int frequency[] = {1, 2, 4, 8, 16, 32}; // generate a random phase for sine waves float phase = random(0, 2*PI); // Initialize all values of groundLevel to 0 for(int i = 0; i < width; i++) { groundLevel[i] = 0; } // for every ground level array element for(int i = 0; i < width; i++) { // for all sine components to be accumulated for(int j = 0; j < frequency.length; j++) { // generate the current sample value for the current sine wave groundLevel[i] += (sin(2*PI * frequency[j]*i/width + phase)*amp[j])*30; } } // Apply an arbitrary offset to place terrain at appropriate level on // screen for(int i = 0; i < width; i++) { groundLevel[i] += height - (height / 3.5); } } void setup() { size(960, 480); // Set the screen size // Background image taken from https://www.behance.net/gallery/31516629/CARTOON-BACKGROUNDS // Load the background image into object bg = loadImage("background.png"); // Initialize the ground level groundLevel = new float[width]; // Fill groundLevel array with values GenerateTerain(); // set the height of the tanks relative to the generated ground level float player1Height = groundLevel[int(width*0.1)]; float player2Height = groundLevel[int(width*0.9)]; // Set the location of the two tanks so they rest on the ground at opposite // sides tank1X = width * 0.1; tank1Y = player1Height; tank2X = width * 0.9; tank2Y = player2Height; } void draw() { // Main draw loop. Farm out the individual tasks to other functions for // clarity (though it could be equivalently implemented entirely in this // function.) // Set the background to the picture provided background(bg); // run draw functions for respective elements of the game drawTanks(); drawGround(); drawProjectile(); drawStatus(); // draw projectiles at new positions and run collision detection for // projectiles updateProjectilePositionAndCheckCollision(); } // Draw the terrain under the tanks void drawGround() { /* TO IMPLEMENT IN STEP 1 */ strokeWeight(5); // Draw ground using a single custom shape using vertex functions beginShape(); int idx; // for each groundLevel array element for(idx = 0; idx < groundLevel.length; idx++){ // create a vertex point at the position on the screen determined by the // array index position at the height of the element in the array vertex(idx, groundLevel[idx]); } // Apply two final vertices to fill space below generated vertices vertex(idx, height); vertex(0, height); // Colour ground fill(250, 236, 157); endShape(); // Set future shapes to not have a border noStroke(); // create layered ground with different colour and dimensions for aesthetic // effect // Logic is essentially the same as above beginShape(); for(idx = 0; idx < groundLevel.length; idx++){ vertex(idx, (groundLevel[idx] * 0.85)+70); } vertex(idx, height); vertex(0, height); fill(235, 222, 137); endShape(); strokeWeight(5); // See the groundLevel[] variable to know where to draw the ground // Ground should be drawn in a dark grey } // Draw the two tanks (including cannons) void drawTanks() { /* TO IMPLEMENT IN STEP 1 */ // Draw the two tanks as semicircles using the positions and sizes at the top // of the file // Tanks should be different colours // Also be sure to draw the cannons, using the angles given at the top of the // file // Draw tank 1 // Set size of border to 10 pixels strokeWeight(10); // set variable for placing tank 1 cannon at the centre of the tank tank1CannonX1 = tank1X; // set variable for placing the tip of the cannon at the angle and length // specified tank1CannonX2 = tank1X + (cannonLength * cos(tank1CannonAngle)); // similar to above... tank1CannonY1 = tank1Y; tank1CannonY2 = tank1Y - (cannonLength * sin(tank1CannonAngle)); // draw tank cannon at position specified line(tank1CannonX1, tank1CannonY1, tank1CannonX2, tank1CannonY2); // Make cannon square strokeCap(SQUARE); // Set global border size to 5 pixels strokeWeight(5); fill(255, 0, 0); // draw 1 tank body // arc was replaced with ellipse to account for more detailed varying ground // level ellipse(tank1X, tank1Y, tankDiameter, tankDiameter); // Draw tank 2 // Exactly the same as tank 1 strokeWeight(10); tank2CannonX1 = tank2X; tank2CannonX2 = tank2X + (cannonLength * cos(tank2CannonAngle)); tank2CannonY1 = tank2Y; tank2CannonY2 = tank2Y - (cannonLength * sin(tank2CannonAngle)); line(tank2CannonX1, tank2CannonY1, tank2CannonX2, tank2CannonY2); strokeWeight(5); fill(0, 0, 255); ellipse(tank2X, tank2Y, tankDiameter, tankDiameter); } // Draw the projectile, if one is currently in motion void drawProjectile() { // Don't draw anything if there's no projectile in motion and the projectile // isn't exploding if(!projectileInMotion && !contactMade) return; // if projectile has exploded enough then stop else if(contactMade && projectileSize > 32) { // Set projectile state so that it no longer expands contactMade = false; // toggle to next player's turn nextPlayersTurn(); } // Save the current global stroke colour // (Hacky workaround due to everything being in the global namespace) int strokeColourVar = g.strokeColor; noStroke(); // Set projectile colour to yellow fill(255, 255, 0); // Set the position and size of projectile ellipse(projectilePositionX, projectilePositionY, projectileSize, projectileSize); // Recover previously set stroke colour stroke(strokeColourVar); } // Draw the status text on the top of the screen void drawStatus() { // Temporarily save global variables locally float strokeWeightVar = g.strokeWeight; int strokeColourVar = g.strokeColor; // Set shape border size to 2 pixels strokeWeight(2); // Set stroke colour RGB stroke(230, 249, 211); // Set shape fill colour RGB fill(201, 246, 203); // Draw a background rectangular box to separate information bar from // background rect(0, 0, width, 45); // reset global values to their originals, avoiding unexpected behaviour in // other functions strokeWeight(strokeWeightVar); stroke(strokeColourVar); // Set text size and alignment parameters for info bar textSize(24); textAlign(LEFT); fill(0); // Based on current state of game, display relevant information in // information bar if(playerHasWon == 1) text("Player 1 Wins!", 10, 30); else if(playerHasWon == 2) text("Player 2 Wins!", 10, 30); else if(player1Turn) { // player1Turn == true means it's player 1's turn text("Player 1's turn |", 10, 30); textAlign(RIGHT); text("| Angle: " + tank1CannonAngle + " | Strength: " + tank1CannonStrength, width - 10, 30); } else { // player1Turn == false text("Player 2's turn |", 10, 30); textAlign(RIGHT); text("| Angle: " + tank2CannonAngle + " | Strength: " + tank2CannonStrength, width - 10, 30); } } // Move the projectile and check for a collision void updateProjectilePositionAndCheckCollision() { // If projectile has finished all actions (moving and exploding) if(!projectileInMotion && !contactMade) { projectileSize = 8; return; } // Check for player's turn // This implementation could be vastly improved by creating a generic // function that applies operations to either player 1 or 2 based on this // boolean. An object oriented approach or a way of passing variables by // reference to a generic function would achieve this. if(player1Turn) { /* TO IMPLEMENT IN STEP 3: UPDATE POSITION */ // If the projectile is in motion... if(!contactMade) { // Tasks: increment the position according to the velocity // For later: the velocity according to gravity (and optionally wind) projectilePositionX += tank1CannonStrength * cos(tank1CannonAngle); projectilePositionY += (velocity * -sin(tank1CannonAngle)); } else { // If the projectile has collided with an object, increment it's size projectileSize += 1; } /* TO IMPLEMENT IN STEP 4: GRAVITY */ // Update the velocity of the projectile according to the value of gravity // at the top of the file velocity -= gravity; /* TO IMPLEMENT IN STEP 5: COLLISION DETECTION */ // Compare the location of the projectile to the ground and to the two // tanks // (Conditions ordered to avoid indexing error in final condition) // When the projectile hits the ground, it's the next player's turn if(round(projectilePositionX) >= width || round(projectilePositionX) <= 0) { // When the projectile hits something, it stops moving (change // projectileInMotion) // As projectile has finished moving and exploding, set all relevant // booleans to false, ready for next player's turn projectileInMotion = false; contactMade = false; nextPlayersTurn(); } // If projectile has collided with an object but hasn't exploded... else if (projectilePositionY > groundLevel[round(projectilePositionX)]) { // Set boolean values accordingly projectileInMotion = false; contactMade = true; } } // Same as for player 1 else { /* TO IMPLEMENT IN STEP 3: UPDATE POSITION */ // Tasks: increment the position according to the velocity // For later: the velocity according to gravity (and optionally wind) if(!contactMade) { // Tasks: increment the position according to the velocity // For later: the velocity according to gravity (and optionally wind) projectilePositionX += tank2CannonStrength * cos(tank2CannonAngle); projectilePositionY += (velocity * -sin(tank2CannonAngle)); } else { projectileSize += 1; } velocity -= gravity; /* TO IMPLEMENT IN STEP 5: COLLISION DETECTION */ // Compare the location of the projectile to the ground and to the two // tanks // When the projectile hits the ground, it's the next player's turn if(projectilePositionX >= width || projectilePositionX <= 0) { // When the projectile hits something, it stops moving (change // projectileInMotion) projectileInMotion = false; contactMade = false; nextPlayersTurn(); } else if (projectilePositionY > groundLevel[round(projectilePositionX)]) { projectileInMotion = false; contactMade = true; } } // When the projectile hits a tank, the other player wins if(dist(projectilePositionX, projectilePositionY, tank2X, tank2Y) < (tankDiameter/2.0) + (projectileSize / 2)) { projectileInMotion = false; contactMade = true; // If projectile shape comes into contact with player 2's tank, player 1 // wins playerHasWon = 1; } else if(dist(projectilePositionX, projectilePositionY, tank1X, tank1Y) < (tankDiameter/2.0) + (projectileSize / 2)) { projectileInMotion = false; contactMade = true; // Vice versa playerHasWon = 2; } } // Advance the turn to the next player void nextPlayersTurn() { player1Turn = !player1Turn; } // Check angle is limited from 9 o'clock to 3 o'clock float validateAngle(float angle) { if(angle > PI) { angle = PI; } else if(angle < 0) { angle = 0.0; } // return angle within limited range return angle; } // Limit strength to within reasonable values float validateStrength(float strength) { if(strength > 10.0) { strength = 10.0; } else if(strength < 1.0) { strength = 1.0; } return strength; } // Handle a key press: update the status of the current player's tank void keyPressed() { if(playerHasWon != 0) // Stop the game when someone has won return; if(projectileInMotion) // No keys respond while the projectile is firing return; /* TO IMPLEMENT IN STEP 2 */ // Use the key variable to check which key was pressed. // Arrow keys don't have a printable character, so they show up as CODED // Use the left and right arrows to adjust the angle, the up and down arrows // to adjust strength. // Specify cases based on the key that has been pressed switch(key) { // If key is a special character case CODED: if(player1Turn) { // Specify actions for up, down left and right switch(keyCode) { // Left and right increment/decrement cannon angle by 1 radian case LEFT: tank1CannonAngle += PI/180.0; break; case RIGHT: tank1CannonAngle -= PI/180.0; break; // Up and down increment/decrement cannon strength by 1.0 case UP: tank1CannonStrength += 1; break; case DOWN: tank1CannonStrength -= 1; break; } // Ensure updated values are within preset ranges tank1CannonAngle = validateAngle(tank1CannonAngle); tank1CannonStrength = validateStrength(tank1CannonStrength); } else { switch(keyCode) { case LEFT: tank2CannonAngle += PI/180.0; break; case RIGHT: tank2CannonAngle -= PI/180.0; break; case UP: tank2CannonStrength += 1; break; case DOWN: tank2CannonStrength -= 1; break; } tank2CannonAngle = validateAngle(tank2CannonAngle); tank2CannonStrength = validateStrength(tank2CannonStrength); } break; // Space bar fires the projectile. Initially in step 2, just have it switch // to the next player. case ' ': // Set boolean variables and projectile positions relative to the current // player's tank. if(player1Turn) { projectileInMotion = true; projectilePositionX = tank1CannonX2; projectilePositionY = tank1CannonY2; velocity = tank1CannonStrength; // Generate projectile based on global variables drawProjectile(); } else { projectileInMotion = true; projectilePositionX = tank2CannonX2; projectilePositionY = tank2CannonY2; velocity = tank2CannonStrength; drawProjectile(); } break; } }