Coding Task 3 - Inheritance


Time Estimate: 9 hours

Jump to current week
Requirements

Project Structure


You will continue to add functionality to your existing project from the previous task. There is no new repository to clone.


Overview Video


Video link: https://youtu.be/GSQh2a7hF30

Another video for Task 3! Yippee!

As before, watching this video is optional, and all of the information you need for this task is contained within this page. However, you may find it helpful for getting a better understanding of the task, and what is expected of you


Using the Graphics Engine - Sprites


Rather than completing functionality of the game engine as was the focus of the previous tasks, this task will mainly be focused on building new features. In order to do so, we'll need to tell the game engine what sprites to load for the new objects. While you don't need to understand how the game engine does this, we will briefly explain how to assign new sprites.

Everything in game that can have a sprite (e.g. GameObjects, DynamicGameObjects) has instance variables called spriteSheetFilename and defaultSpriteLocation. In most 2D games, rather than storing each individual sprite as its own file, we store groups of sprites together in a single file called a spritesheet, then tell the engine which sprite on the sheet to load. spriteSheetFilename is a String that represents the filename containing the spritesheet with the desired sprite in it. defaultSpriteLocation takes in a SpriteLocation object. For the sake of this task, all you need to know about the SpriteLocation class is that it has a constructor taking in the row and column of the desired sprite in the spritesheet (feel free to look through the data directory to understand what these files look like).

So, to add a sprite to an object, we just need to set values to these instance variables in the constructor. For example, this is how the Wall class sets the sprites:

this.spriteSheetFilename = "Ground/Cliff.png";
this.defaultSpriteLocation = new SpriteLocation(3, 0);

Any time anything you add needs a sprite, the handout will specify what the filename and location should be to load a good sprite for the object. We will not test which sprites you use, so if you wish you may use different sprites. All of the suggested sprites already exist in the project, but the sprite sheet linked below has some additional sprites that you may wish to use; if you choose to, you should save this image into the data directory of your project, by right clicking and pressing "Save Image as..."

Task 3 Textures

You may also want to visit the link in the "data/sprites.txt" file to find the source of all sprites used in this project and download any sprites that interest you.



Specification


In this task, you will implement the following specs. Once all of these are complete, you will have created a platformer game using the same game engine we built for the top-down game. In this new game, your character will be able to jump around a level with a side-view while avoiding spikes.

While implementing the functionality in this task, you will use much of the existing inheritance structure of the project. You should take some time to look through the code and explore all the inheritance usage to give more context for the new classes/methods you'll create.

  • Potion (app.games.commonobjects.Potion)
    • In the commonobjects package, create a new class called Potion that extends DynamicGameObject.
    • Create a constructor that takes in two parameters: a Vector2D representing location and an int representing the amount to heal, which should be stored as an instance variable
      • When you call the super constructor, give the Potion a max HP of 10.
      • Graphics (See "Using the Graphics Engine" for more detail):
        • You should initialize spriteSheetFilename to "User Interface/Icons-Essentials.png"
        • If the amount to heal is non-negative (This includes 0), the defaultSpriteLocation should be a SpriteLocation at column 0, row 1 (see the "Using the Graphics Engine" section of the handout)
        • If the amount to heal is negative, the defaultSpriteLocation should be a SpriteLocation at column 1, row 1. This turns the potion into a poison
        • Note: Both spriteSheetFilename and defaultSpriteLocation are protected variables meaning you can access them using this. even though they are not defined in this class
    • Override the collideWithDynamicObject method. If the object being collided with is a Player, you should add the amount to heal (from the constructor) to the Player's HP, then call this.destroy() to remove the Potion from the level. If the other object is not a Player, this method does nothing
      • You can check if a DynamicGameObject is a player by calling the isPlayer method on it.
    • Optional: To see your Potion in game, you can choose one of the methods creating a level (levelZero, levelOne, levelTwo) in the SampleTopDownGame class and add a new Potion object to the Level's DynamicGameObject ArrayList (follow the process the given code uses to add an Enemy to the level, but with a Potion constructor instead.)

The remainder of this task will be implementing a new physics engine. While the physics engine you wrote in task 1 was designed for top-down levels where you freely move around a 2D space, this new physics engine will be designed for 2D platformer levels where your movement is constrained to moving left, right, and jumping.

  • DynamicGameObject - Platformer Preparation (app.gameengine.model.gameobjects.DynamicGameObject)
    • Add an instance variable of type boolean representing if the given DynamicGameObject is on the ground. This should be initialized to false
    • Add a method called isOnGround that takes no parameters and returns a boolean. Add a method called setOnGround that takes in a boolean and returns void. These should be the getter and setter for the previously described instance variable
  • PhysicsEngineWithGravity (app.gameengine.model.physics.PhysicsEngineWithGravity)
    • Create a new class called PhysicsEngineWithGravity in the app.gameengine.model.physics package that extends PhysicsEngine. We'll see the power of inheritance where we will only have to add gravity while reusing all of your code from PhysicsEngine to still handle collisions and movement. This will allow us to make games that have gravity applied to dynamic objects
    • Write a constructor that takes in a single double representing the acceleration from gravity
    • Override the updateObject method to have the following behavior:
      • Add the change in velocity from gravity to the DynamicGameObject's y-velocity (note that change in velocity is equal to acceleration from gravity times the change in time). This will decrease the objects y-velocity on every frame. Since the game runs at 60 fps, this linear change every frame will appear as a parabola when playing the game
      • Call the super class' updateObject method and pass in the parameters. Be sure to call this after applying gravity or the movement will be a little jenky
      • Keep in mind that the positive y direction is downward in the world of computers. The acceleration from gravity is directionless (only magnitude) so it will always be positive (Yes this annoys physicists, but Earths gravity would be positive 9.81 [with no units] in our game)
    • And that's it. By leveraging inheritance, you should have just created gravity with about 20 lines of code (including package and imports). You have the power of a god!
  • PlatformerWall (app.games.platformerobjects.PlatformerWall)
    • Create the app.games.platformerobjects package and add a class named PlatformerWall in it that extends Wall
    • Write a constructor that takes in two ints representing the x and y location of the PlatformerWall. These should be passed into the super constructor.
      • The suggested spriteSheetFilename for this class is "Ground/Cliff.png", and the suggested defaultSpriteLocation is column 4, row 0.
    • Override the collideWithDynamicObject method to have the following behavior:
      • Firstly, resolve the collision exactly as the base Wall class does. You can do this by calling super.collideWithDynamicObject
      • Since we have to consider the cases where the object lands on top or bumps on the bottom, add the following logic after resolving the collision:
        • If the DynamicGameObject is overlapping in the x-dimension with the Wall, set the y-velocity to 0.0. This makes it so the object stops falling when it lands on top of the wall or start falling if it hits the bottom.
          • When determining if the overlap is occurring, you should not include the edges (if the object's left == the wall's right or the object's right == the wall's left, it's not overlapping)
        • Additionally, if that condition occurs and the object is above the wall (the case where it lands on top), you should also mark that the object is on the ground using the previously written setter.
        • Note that we are using what we know about the objects in this method. Since the method was called, we know that these objects collided. Since we called the Wall's collideWithDynamicObject method (That you wrote in task 1), we know that they are no longer colliding. After this, if they still overlap in the x direction we know that the Wall's method moved the dynamic object either up or down (It resolved the collision by removing the y overlap). This means we either landed on the top of the wall, or hit our head on the ceiling. In either case, our y velocity should be set to 0. When landing, we want to report to the object that it is now standing on ground which we will use later to know that we're allowed to jump
  • Spike (app.games.platformerobjects.Spike)
    • Create a class called Spike that extends StaticGameObject
    • Write a constructor that takes in two ints representing the x and y location of the Spike. These should be passed into the super constructor
      • The suggested spriteSheetFilename for this class is "User Interface/UiIcons.png", and the suggested defaultSpriteLocation is column 2, row 10
    • Override the collideWithDynamicObject method to have the following behavior:
      • If the colliding object is a Player, call the destroy method on it. Otherwise, do nothing
    • This class does not change any velocity or location of the other object. Since it's being destroyed, we don't care about how it's moving
  • PlatformerLevel (app.games.platformerobjects.PlatformerLevel)
    • Create a class called PlatformerLevel that extends Level.
    • Override the wallOffBoundary method. This should have the exact same behavior as wallOffBoundary in app.gameengine.Level, but with PlatformerWalls rather than Walls.
    • Create a constructor that takes in a Game, an int for width, an int for height, and a String for name in this order. This constructor should:
      • Call the super constructor with the Game, a PhysicsEngineWithGravity object, the width, the height, and the name in this order. When creating the PhysicsEngineWithGravity object, use a gravity of 25.0
      • Assign the inherited instance variable gameControls with a new PlatformerMovement object.
        • The PlatformerMovement is already written in the handout. You should pass in the game as the first parameter and the default movement speed as the second parameter
        • The default movement speed should be 5.0
      • Call the wallOffBoundary method.
    • Override the inherited jumpButtonPressed method from the Level class. This will be called whenever your character jumps (press up or w) in a PlatformerLevel
      • This method should check if the player is on the ground. If so, it should set the player's y velocity to -14.0, and set the onGround flag to false. If the player is not on the ground, it should do nothing

Finally, you will create a new Game subclass that will be used to play Levels using these new features. You're welcome to create your own Levels, but we will also be providing Levels via the course Piazza.

  • Game (app.gameengine.Game)
    • Move the Level functionality added in the previous task in SampleTopDownGame to the base game class
    • This will include the instance variable, addLevel, getLevelList, setLevelList, advanceLevel, and removeLevelByName (And remove the existing advanceLevel stubbed method). Make your instance variable protected if you use in the init method in SampleTopDownGame. Remove the init() call from the SampleTopDownGame constructor since it will be called by the super constructor
    • Add a method that returns void and takes no parameters called init. This method should do nothing and only exists to be inherited and overridden
    • Modify the Game constructor to call the init method
  • PlatformerGame (app.games.PlatformerGame)
    • Make a new class called PlatformerGame that extends the Game class.
    • Override the init method to add levels to the game and load your first level. This can be done using the inherited addLevel method
      • You have the freedom to add whatever levels you'd like to this game. The actual content of the Level list will not be considered except that it exists
    • Note that if you don't write any constructor, you will still get the correct behavior since we only need the default constructor (no params, empty body). We could also remove the SampleTopDownGame constructor and still play it normally

To actually play your game, update the switch statement in GameFactory (in the app.games package) to have a case for "platformer game". In this case, it should assign game to a new PlatformerGame object, then break. You can then replace the game String in Configuration in the app package with "platformer game". After that, running StartGame will load up your new game.

Note: You can see feedback in Autolab for your tests without completing the programming portion of this task, but you must at least create every class/method from this specification. You can "stub out" these methods by having them always return a fixed value, but they must exist so the grading code, and your tests, can compile and run.



Testing Utilities


There is no testing utility for this task.



Testing Requirements


Create a class named TestTask3 in the tests package.

You will write tests for the following functionality from the specification:

  • PlatformerWall
    • collideWithDynamicObject. You do not need to test that the position is being updated correctly (as you know the base Wall class is already functional). You only need to test that the velocity and isOnGround is getting set correctly.


Programming Requirements


Implement all the functionality from the specification.



Autolab Feedback


The feedback in Autolab will be given in 3 phases. If you don't complete a phase, then feedback for the following phase(s) will not be provided.

  1. Running your tests on a correct solution
    • Your tests will be run against a solution that is known to be correct. If your tests do not pass this correct solution, there is an error somewhere in your tests that must be fixed before you can move on with the assignment. If your tests don't get past this check, you should re-read this document and make sure you implemented your tests and code according the specification. You should also make sure that if there are multiple correct outputs to the input in your tests cases that you accept any of the outputs as correct
  2. Checking your tests for feature coverage
    • The next phase is to check if your tests check for a variety of features defined by different inputs. You should write at least one test case for each feature to pass this phase
    • Passing this phase does not necessarily mean that your testing is completely thorough. Satisfying Autolab is the bare minimum testing requirement. Not all possible inputs are checked, and it is sometimes possible to pass this phase with weak testing. If you are struggling to earn credit for code that you believe is correct, you should write more than the required tests
  3. Running my tests on your solution
    • Once Autolab is happy with your tests, it will run my tests against your code to check it for correctness. If your testing is thorough, and your code passes your tests, then you should pass this phase. If you pass your tests, but fail one of mine, it is an indicator that you should write more tests to help expose your bug

Once you complete all 3 phases, you will have completed this Task and Autolab will confirm this with a score of 1.0 for complete.