Coding Task 1 - Physics Engine


Time Estimate: 10 hours

Jump to current week
Requirements

Project Structure


GitHub Repository link: https://github.com/CSE-116/CSE116-GameEngine

  • Clone the starter code from the repository into a new IntelliJ project

Once you have the project opened in IntelliJ, you'll see a src folder which contains another folder called main, which contains another folder called java. Inside this folder is the Java package app which contains all the code for the project. The starter code contains many Java classes needed for the engine to run.

Don't panic about the number of files. For now, you can safely ignore most of them. If you ever have trouble finding a specific file, you can press shift twice to open a convenient search window that'll direct you to the desired class.

For this task, you will be writing code in the following classes:

  • DynamicGameObject, located in the package app.gameengine.model.gameobjects
  • Wall, located in the package app.games.commonobjects
  • PhysicsEngine, located in the package app.gameengine.model.physics
  • TestTask1, located in the package app.tests

You will also need to use the provided Vector2D and Hitbox classes from the app.gameengine.model.physics package for this task. These classes will not be modified, though you should read them to understand how they work.

To submit your project, create a zip file containing your entire project and submit it to Autolab. (Click file -> export -> Project to Zip File, though there may be slight differences to this across OSes and versions). If this option is not available, you may need to install the "Android" plugin through the settings menu



The Game Engine


Throughout this semester, you will be building a game engine along with a game that will utilize this engine. All 6 tasks (excluding task 0) will build on this project and add more features to the engine and game. After cloning the repo, you should run "StartGame". When you do, a new window should open showing a 2d pixel-art top-down view game.

Game image from starter code

As you can see, it's basically a marvel of modern graphics. You probably thought this was photograph of cosplayers at a medieval festival, but it's actually in game graphics from this project.

When you run the game, you'll notice that you can't actually do anything. That's because major components of the game engine are missing. Your job is to complete it. In this task, you will add player movement, collision detection, a few smaller features to the game.


The Coordinate System


The coordinate system use for computer graphics might be different than you used to. We'll use the standard coordinate system that is typically used in video games.

The origin of the system is at the top-left of the screen/window/level with positive x going right and positive y going down. This image shows the same information as the one above, but with the coordinate system labeling all the tiles in the game. You can see that "G" (The goal) is at location (6, 2).

Coordinate system image from level 0

Objects (and hit boxes) in our game have two components: location and dimensions, both represented as 2-dimensional vectors (x, y). All objects/hit boxes will be rectangles and their location is the location of their upper-left corner. Their dimensions are the length and width of the rectangle. For example, the goal has a location of (6, 2) since that's the location of its upper-left corner, and it has dimensions (1, 1) since it's exactly 1 tile in size.

Locations and dimensions do not have to be whole numbers. In the image below, the player character is at [roughly] location (1.5, 1.0) with dimensions (1.0, 1.0).

Floating point location

Specification


Note: All methods are public and non-static, unless otherwise noted, from this point forward in the course. It is recommended you follow the spec in the order it is written in.

  • PhysicsEngine - updateObject
    • In the PhysicsEngine class, complete the method called updateObject that takes in a DynamicGameObject and a double representing the change in time. This method should update the location vector of the DynamicGameObject based on the value of its velocity vector.
      • Note: the change in distance (for both components of the vector) is equal to the velocity (of that direction) multiplied by the change in time.
    • Both location and velocity are represented as Vector2D objects and the DynamicGameObject contains getter methods for both of them. Note that you never have to set the vector objects themselves, and should instead set the x and y components of the existing vectors using the provided setters. This will modify x and y of the Vector2D objects and that change will be made on the heap (If you don't understand what this means, it may help to do a memory trace on your code for this method to better understand what is happening in the stack and heap).
    • Once you complete this method, you should be able to move your player character in the game using either the arrows keys or WASD. You will notice quite a few other issues however (eg. try hitting the space bar).. We'll get there!
  • The DynamicGameObject class
    • Modify the DynamicGameObject constructor to initialize instance variables for the following:
      • An int representing the Max HP of the object which is set the value of the maxHP parameter of the constructor
      • An int representing the current HP of the object. Initially (ie. in the constructor), this should be set to the object's Max HP
      • A Vector2D representing the orientation the object is facing. This should be initialized to x = 0.0, y = 1.0.
      • Note: the given constructor initializes the location using inheritance, with super(location). This must be the first line of the method and all code you add should go below it. More details on this when we cover inheritance in lecture.
    • Implement the following getters and setters for the instance variables following the standard setup for getters and setters covered in lecture:
      • getHP
      • setHP
        • When setHP is called, add some logic that will prevent HP from exceeding the Max HP. If the parameter exceeds the Max HP, HP should be set to the Max HP.
      • getMaxHP
      • getOrientation
      • Note: These methods exist in the handout code, but do not do what they are supposed to do. Your task is update their code to use your instance variables
    • Implement a method named takeDamage that takes in an int and returns void. This method should subtract the current HP instance variable by the amount specified in the parameter.
      • If the parameter is negative, this method should do nothing.
      • The resulting HP is allowed to become negative
  • PhysicsEngine - detectCollision
    • Complete the detectCollision method that takes in two Hitboxes and returns a boolean. The boolean should return true if the two Hitboxes are colliding (ie. overlapping) and false otherwise. Two hitboxes are overlapping if any portion of the boxes overlap
      • Recall from the coordinate system section that hitboxes have both a location (the (x, y) of its upper-left corner) and dimensions (the width and height of the rectangle) both represented as Vector2D objects. To visualize when 2 hitboxes collide, it's recommended that you sketch out hitboxes and their location/dimension vectors and write test cases for a variety of situations before diving deep into the code
      • After implementing this method, collisions will be registered when you play the game. When you walk into an enemy, you will be sent back to the starting location of the level and when you reach the goal you'll advance to the next level. You can even shoot the enemies using the space bar to fire.. however, you can still walk through walls. Let's fix that
      • Note: It is strongly recommended that you write the tests for this method before trying to write the code. Having tests will make it much easier to check if your code is correct, and debug it if it's not. Going through the process of writing tests will also ensure that you fully understand what is considered a collision. You can submit to Autolab to check if your tests are correct before finishing this method for assurance that you have good testing to help you write the code
  • The Wall class
    • Write a method called collideWithDynamicObject that takes in a [reference to a] DynamicGameObject and returns void. This method should modify the state of the DynamicGameObject such that it cannot pass through the wall. This method will be called by your physics engine whenever the player collides with a wall and prevent them from clipping out of bounds
      • This method should not modify the velocity of the intersecting object, only its location
      • To implement this method, change either the x or y location (not both) of the dynamic object so that they no longer collide. That is, the object must be moved in only one direction (up, down, left, or right); this direction must be whichever requires the shortest distance to completely remove the dynamic object from the hitbox of the Wall.
      • Ex: If a wall has position (1,0) and dimensions (1,1), and a DynamicGameObject has position (0.5, 0) and dimensions (1,1), these two objects are colliding. If the object is moved up or down, it would need to be moved a distance of 1.0 to no longer collide with the Wall. If it were moved right, it would need to be moved to (2,0), a distance of 1.5. If it were moved left, it would need to be moved to (0,0), a distance of 0.5. Thus, it should be moved left, to the location (0,0)
      • Simplifying Assumptions: When writing this method, you can assume two objects are currently colliding, that they have the same dimensions, that their hitboxes exactly match their location and dimensions, and all dimensions are (1.0, 1.0). All these assumptions are true in the provided tests and the tests in Autolab
      • You are given tests for this method in TestTask1 which can use to be sure that you're implementing the method according this spec without having to submit to Autolab
      • When this method is complete, you will no longer be able to walk through walls when you play the game


Testing Utilities


For this task, and all future tasks, you will be required to write test cases for some of the functionality. You will also often be asked to write a testing utility method to assist you with writing thorough and clean tests. This utility should be written in the TestTask1 class in the tests package. (Note: Do not add the @Test annotation to any testing utility methods since they are not tests)

  • comparePlayers - Write a method named comparePlayers in the tests.TestTask1 class that:
    • Takes [references to] 2 Player objects as parameters
    • Returns void
    • Checks if the two Player objects contain all the same instance variable values, including location, velocity, orientation, HP, and maxHP. The method fails a JUnit assert if any of these variables do not have the same values. You can call assertTrue, assertEquals, etc. in your comparePlayers method even though it itself is not a test method (ie. This method can call compareEquals on each pair of values that are expected to be the same).
    • Note: For any variables that are stored as Vector2D objects, you must check both their X and Y components

Note: You still have to create every class and method from the specification before getting feedback on your submission in Autolab since the grader will not be able to compile if those classes/methods do not exist. You don't have to implement them yet, and they can all return a default value



Testing Requirements


Write JUnit tests for the following methods from the specification in the TestTask1 class. These tests should be annotated with @Test:

  • DynamicGameObject
    • DynamicGameObject
      • Note: DynamicGameObjects cannot be instantiated directly. When testing these methods you should create and use Player objects, which will have access to these methods due to inheritance. More details about this during inheritance week
      • Test that each of the instance variables is correctly set by the constructor. You can do this by creating an object, then calling all the corresponding getter methods then assert that the getters returned the expected values
      • The values that should be checked are location (x and y), velocity (x and y), orientation (x and y), maxHP, and hp
    • setHP
      • Call this setter, then assert that getHP returns the expected value
    • takeDamage
      • Call this method, then assert that getHP returns the expected value
  • PhysicsEngine
    • To test these methods, you will have to create a PhysicsEngine object in order to call the methods
    • updateObject
      • To test this method: create a Player object, set the x, y of its location and velocity to known values, call updateObject with the player and a dt of your choice, do the math to compute the expected x and y for the player location after the update and assert that the actual x and y match this expectation
    • detectCollision
      • To test this method, you can create multiple hitbox objects with different locations and dimensions and assert that the method properly detects if the rectangle overlap or not
  • Wall
    • You DO NOT need to test the collideWithDynamicObject method. Tests are provided for you in the TestTask1 class, which you may use to help you write the method itself


Programming Requirements


Implement all the methods from the specification.



Autolab Feedback


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

  1. Testing your testing utility methods
    • Your testing utility method will be checked with a variety of test cases to ensure that it makes all the required checks. This phase will ensure that your utility methods are accurate before you start using them in your tests
  2. 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.
  3. 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
  4. Running my tests on your solution
    • Once Autolab is happy with your tests, it will run 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 here, it is an indicator that you should write more tests to help expose your bug.

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