The purpose of this homework is to do a deeper dive into object-oriented programming.
At the end of the assignment, you will have produced a game called Irate Avians (our take on Angry Birds). Irate Avians has moving targets, targets with multiple hit points, and a limited number of bird launches. We have provided an example video of how your game might look when it is complete here. Each class acts as the blueprint for distinct, crucial parts of our overall game. As you are implementing this assignment, keep in mind that we are having objects interact with each other; they do not exist in isolation. Your bird object will interact with your target objects by flying into and hitting them, and your bird and target objects operate within the arena object which controls the game.
You can find the base code files here which are also available through Codio. Read through the classes and function stubs provided, along with the comments. You will be using several different classes to implement the game. Namely:
Bird.java
represents the bird projectile you launch in the gameTarget.java
represents a single target to be hit by the bird in the game.Arena.java
represents the domain in which the game takes place. This class handles the bulk of the game logic.IrateAvians.java
represents the overall game. We have provided all the code that goes in this class, but you should understand what every function call does. Do NOT modify anything in this class, just read through it.Work on the program in the order of the directions. Do not try to do things out of order, as that will likely add to your confusion. You will start with game elements like the bird and work backwards to the drawing and the loop.
Compile, run, and debug frequently; make one small change at a time, ensuring that your program works as you expect it to after each change. We will describe checkpoints you should test as you code the program.
We will begin the Irate Avians assignment by implementing the constructor for each of our game components - Bird.java
, Target.java
, and Arena.java
. This will allow us to properly instantiate them by parsing a level-description text file as in previous assignments like NBody
.
The constructor for the Bird
class should initialize all of the bird’s instance variables. Its position, radius, and number of throws (how many times the bird can be launched before the game ends) are passed to the constructor. The bird’s velocity components should both be set to zero.
The constructor for the Target
class takes in the canvas’s width and height, and the target’s position, radius, velocity components, and number of hit points. These attributes should be initialized accordingly. The Target
s in this version of the game move across the screen, and take more than one hit to completely destroy, which is why we will need velocity and hit point variables. We will discuss how to use these variables in later sections of the writeup, just make sure you initialize them in this constructor. Also, initialize hitThisShot
to false
.
The Arena
class’s constructor is where you will parse the level description file passed as the first argument to IrateAvians.java
’s main function. As the comments in the base code note, you will read several pieces of information about the game’s initial state from the input file. The data in the file will be ordered as such, where target[0]
is the first target, target[1]
is the second, and so on:
numTargets width height
bird.numThrowsRemaining
targets[0].xPos targets[0].yPos targets[0].radius targets[0].xVel targets[0].yVel targets[0].hitPoints
targets[1].xPos targets[1].yPos targets[1].radius targets[1].xVel targets[1].yVel targets[1].hitPoints
The first line of information contains data about the Arena
. Make sure to call PennDraw.setXscale()
and PennDraw.setYscale()
with the width and height in the file (from 0 to the specified width or height), in addition to setting Arena’s member variables. The Arena constructor should initialize its bird to the position (1,1), with a radius of 0.25, and with a number of throws equal to the amount indicated in the file. The number given for numTargets
will tell you how many lines there will be with target data in the file; you should use this when allocating and instantiating the values of your Target
ArrayList. Note that the .close()
function in the In
class closes the input stream for your text file. As well, make sure to set mouseListeningMode
and mouseWasPressedLastUpdate
to the correct boolean value in your constructor.
Next in this project we will implement the draw()
functions of the Bird
, Target
, and Arena
classes. This will let us “visually debug” our constructors to ensure our objects are initialized with the right properties.
You should draw the bird so that its body should appear as a circle centered at the bird’s position, with a radius equal to the bird’s radius. Additionally, draw a triangular beak and two circular eyes somewhere on the bird’s body to give it more visual detail. You may add more body parts and details as you wish, but they won’t be worth any additional credit. You should also draw the bird’s remaining throws as text at a position 0.1 units above the bird’s body (not overlapping it).
In addition to drawing the bird, you will implement the bird’s drawVelocity()
function to pre-visualize the bird’s launch velocity when the player clicks and drags (to be implemented in a future section). All drawVelocity()
needs to do is draw a line from the bird’s position to a point equivalent to the bird’s center after a timestep of 1. Since the new bird’s position will change after one timestep, you can represent the new position as $p_{new} = p_{old} + velocity$.
Each target should be drawn as a circle centered at the target’s position, with a radius equal to the target’s radius. The target’s current hit points should be drawn as text centered at the target’s position. A target’s draw function should only draw anything if the target’s hit points are above zero.
The Arena
’s drawing function will be a little more involved than the Bird
’s or Target
’s. The arena is the object in the game engine that stores the bird and targets, so it will invoke their respective draw functions to draw them. Thus, Arena’s draw()
function will perform the following operations:
PennDraw.clear()
Target
s and draw each oneBird
Arena
’s mouseWasPressedLastUpdate
variable has a value of true
and mouseListeningMode
is true
, draw the bird’s velocityPennDraw.advance()
to draw everything to the screenRun IrateAvians.java
and give it the command-line argument targetFile1.txt
. At this point, you should see the bird and targets being drawn on your screen, but nothing will move and nothing will respond to mouse input (yet).
This should produce the following drawing:
If your drawing does not look like this, try to debug your draw()
functions.
The first thing we’re going to do in this section is actually add a little code to Arena
’s update function. Iterate over the ArrayList of Target
s and call update()
on each Target
. You should pass timeStep
to each invocation of target.update()
. We will implement the rest of arena.update()
later in this section. We do this so that our code actually calls target.update()
somewhere so we can test whether our implementation of it works after this step.
Now, we will actually implement target.update()
. This function should do the following:
Arena
’s width, accounting for the target’s radius in this calculation. The target should be completely offscreen to the left or right for this check to evaluate to true; you should not just check the target’s center. If the target is offscreen horizontally, then you should set the target’s xPos
such that it wraps around to the other side of the screen, also accounting for the target’s radius. For example, if the target’s radius were 0.5 and its x-position were 10.5, it would be offscreen to the right. So, that target’s position would be set to -0.5 in order to wrap it around to the left side of the screen, while making it just offscreen so it doesn’t visually teleport, but smoothly move offscreen then onscreen again. Refer to the video provided in the base code for a visualization of this occurring.Arena
’s height, accounting for radius. Then, perform the same behavior as you did for the x axis check above, but for the y axis.Run IrateAvians.java
from the terminal, and give it the command-line argument targetFile1.txt
. If you’ve correctly implemented target.update()
, you will see the targets move like they do in the example video of Irate Avians.
Implementing bird.update()
will be fairly brief. This function should simply increment the bird’s x and y position by its x or y velocity multiplied by the input time step. Then, it should update the bird’s y velocity by subtracting from it the value 0.25 * timeStep
to represent gravity dragging the bird down over time. We won’t be able to test this function until we’ve completed arena.update()
, however.
arena.update()
is a slightly more involved function than the other two updates. You’ve already implemented the portion that updates each Target
. Now you will implement the portion that handles mouse-listening mode, and the portion that handles bird-flight mode. We have provided comments inside Arena.update()
that outline what to do, but we will further explain the process here. When the game is in mouse-listening mode, it has to handle two possible states. The first is when the player is currently pressing a mouse button, indicated by PennDraw.mousePressed()
returning true
. In this state, the Arena should set mouseWasPressedLastUpdate
to true
, and invoke bird.setVelocityFromMousePos()
. We will implement that function in the next section. The second state is when the player has just released the mouse button, indicated by PennDraw.mousePressed()
returning false
and mouseWasPressedLastUpdate
being true. In this state, mouseWasPressedLastUpdate
should be set to false
, mouseListeningMode
should be set to false
, and the bird should decrement its throw counter. To decrement the bird’s throws, you should implement bird.decrementThrows()
.
We will take a detour in our code for a moment in order to implement bird.setVelocityFromMousePos()
, which will allow us to update the bird’s initial velocity in mouse-listening mode. This function should get the mouse x-position and y-position, and compute the vector from that position to the bird’s current position. Then, it should set the bird’s current velocity to equal that vector (remember, a vector has an x and y component).
xVel <- bird's x-position - mouse's x-position
yVel <- bird's y-position - mouse's y-position
If you run your program with the same command-line argument as before, you should now be able to click and drag, and a line representing your bird’s initial velocity should be drawn when you do so. Once you release the mouse, the game will become non-interactive as you have not yet implemented bird-flight mode in arena.update()
. For this checkpoint to work, make sure that mouseListeningMode
is set to true
in the Arena
constructor.
The comments in arena.update()
indicate where you should handle bird-flight mode: in the else statement provided in the base code of arena.update()
. This portion of the function should do three things (some of which are functions we have not yet implemented):
Target
s and call bird.testAndHandleCollision()
on each Target
. The eventual result of this will be to have the bird check if it currently overlaps each target, and decrease that target’s hit points by 1 if it does overlap. The target’s hit point should decrease after the bird is offscreen and not at the moment where it hits the target.arena.birdIsOffscreen()
, and if that evaluates to true
, iterate over each Target
, and if the target is hit this round, decrease its health and reset its hit value to false for the next round. Finally, you should be calling bird.reset()
and setting mouseListeningMode
to true
. The eventual result of this will be to reset the game back to mouse-listening mode when the bird flies offscreen.First, we will implement bird.distance()
, which is a helper function that computes the distance between two points (x1, y1) and (x2, y2). You will use the Pythagorean Theorem to implement this function to find the distance between two points. This function will be called from bird.testAndHandleCollision()
.
We will now implement bird.testAndHandleCollision()
. The first thing this function should do is check if the input Target
has more than zero hit points. If it does, then the bird is allowed to test for intersection against it, as the target still exists in the playing arena. The bird should now check if the distance between its center and the target’s center is less than the sum of its radius and the target’s radius. If this is true, then you should set the target’s hitThisShot
value by calling the setter in the Target
class. Note that you will code the getter and setter for hitThisShot
in section 3K.
To implement arena.birdIsOffscreen()
, you’ll need to perform three checks.
We do not check if the bird is above the top of the screen since the bird is subject to gravity and will fall back down eventually, so the bird is allowed to continue to travel (and not reset) if it goes above the top of the screen.
If any of the three conditions described above are true, then this function should return true
. If they are all false, then it should return false
. Notably, you should be able to evaluate your function’s return value without using any if
statements. If you use any if
statements in this function, you will lose points.
If you run your game with targetFile1.txt
as input, you should be able to launch your bird by clicking and dragging, and have it decrease the number of throws remaining. However, targets don’t lose any hit points yet even when your bird intersects with them.
isHit
booleanImplement target.isHit()
and target.setHitThisShot()
now. These are basic getters and setters for the isHit
field.
We will now update bird.testAndHandleCollision
so that it only modifies the input Target
if the Bird has not yet collided with it in this bird-launch. To do this, we will make use of the hitThisShot
boolean for the Target
. The bird should set that Target
’s boolean value to true
by calling target.setHitThisShot()
inside the if
statement in your testAndHandleCollisions
function.
When you run your game, you should now be able to properly decrease a Target
’s hit points by at most 1 per bird launch. You’re almost done with the project at this point! All you have to do is check whether or not the game should end, which you’ll do in the next section.
If you examine IrateAvians.java, you will see that its while
loop continues as long as the Arena
’s gameOver()
function returns false
. You will now implement this function so that your game can end in one of two ways: the player can win, or they can lose.
Now that you’ve handled what happens when the Bird
hits a Target
, you should modify arena.update()
so that it removes any Target
objects from the ArrayList that have zero hitpoints remaining. This will simplify the process of checking whether or not the player has won the game.
Victory occurs when all targets are broken. Implement arena.didPlayerWin()
. This is a helper function that determines if the state of the game’s variables are such that the player has won. This function will return true
if and only if there are no remaining Target
objects in the ArrayList targets
. In all other scenarios, this function will return false
.
You will also have to check if the player has lost the game, which occurs when they are out of bird throws. Implement arena.didPlayerLose()
. This is a helper function that returns true
if the game is in mouse-listening mode and the bird’s remaining throw count has reached 0. We add the condition that the game must be in mouse-listening mode in order to allow the player to win on their very last throw; if the player lost immediately when the throw count reached 0, they’d never be able to launch their bird for their very last time as the game would immediately end without giving the bird a chance to collide with any remaining targets.
Now that you’ve implemented the two helper functions, go ahead and implement arena.gameOver()
. This function should return true
when either the win or loss condition is true
.
Now you must also implement arena.drawGameCompleteScreen()
. This will clear the screen and draw a message that depends on whether the player has won or lost. If the player has won the game, it should draw text on screen that says “You Win!”. If the player has lost, it should draw text that says “You have lost…”.
Run your game as you have before. If you can reach both the win screen and the lose screen by playing, then congratulations! You have finished coding Irate Avians!
Implement any of these features to earn extra points!
Draw the targets with more detail than just a plain PennDraw circle, using additional PennDraw shapes. Using images alone in place of circles will not count for extra credit.
Have the bird-ball bounce off of targets rather than moving through them. This will require you to reflect the bird’s velocity about the vector between the target’s center and the bird’s center. Feel free to look up how to reflect one vector about another. For an additional point, have a target change its velocity direction to match that of the bird’s bounce velocity, but in the opposite direction.
Using PennDraw’s keyboard input capabilities, allow the user to hold the I or O key while clicking to place a force field that either pulls the bird-ball in (I) or pushes it out (O). The bird’s initial velocity should only be updated while the mouse is held when no keyboard key is pressed. Likewise, the user should be able to press the R key to reset the game to mouseListeningMode
and reset the bird, should it get caught in the force fields. You should represent force fields as small PennDraw circles; orange for pull-fields and purple for push-fields.
As a target’s HP decreases, it should change appearance. For example, you might change its color, decrease its radius, or have more and more cracks appear on its surface.
Make sure to fill out the readme provided with the base code before you submit!
You need to upload Arena.java
, Bird.java
, Target.java
, IrateAvians.java
, and readme_avians.txt
. Remember that your filenames should match the required names exactly, including capitalization. Make sure you submit the .java files, NOT the .class files.
If you used images for any of your drawing, you’ll need to submit those image files as well in order for your program to run correctly.