Game Features 2

Linked Lists and Inheritance


Overview

Coding Task 2 Overview


Game Features 2 assesses Linked Lists and Inheritance. You will continue to build functionality for the Game Engine in this component of the task. This will be done on top of the code written for the previous tasks; you should not reclone or remove said code for this task or any future Game Features tasks.

There are three sections listed in the handout for convenience. Future Game Features tasks may expect some or all of these sections to be fully completed.

We have provided some tests for the PhysicsEngineWithGravity and LevelParser classes. They can be accessed at this link: TestTask2.java. You can open this link in a new tab to copy and paste the content into your project, or right click and select "save link as", then download the file to the correct location within the project. The correct location is in the same directory as the previous two given test files, the test/java/tests/ package.

LinearGame

Overview


For this portion, you will be writing code in the app.gameengine.LinearGame class, which already exists.



Specification


Many of the methods in this task will make use of the LinkedListNode class, found at app.gameengine.model.datastructures. You are required to use this class. It is identical to the LinkedListNode class used in the Problem Set.

For this portion of the task, you will implement the following functionality:

  • Implement the following methods in the LinearGame class. This class represents a Game with a linear sequence of levels that are typically only traveled through in order from start to finish. For example, the MarioGame class extends the LinearGame class, and uses these methods.
    • getLevelList
      • Create a method named getLevelList which takes no parameters and returns a LinkedListNode of Levels. It should return the head of the game's list of levels.
      • If no levels have been added, this method should return null.
    • setLevelList
      • Create a method named setLevelList which takes a LinkedListNode of Levels as an argument and returns void. It should replace this game's list of levels with the input value.
    • addLevel
      • Create a method named addLevel which takes a Level object as an argument and returns void. This method should append the input Level to the end of this game's list of levels.
    • Note: all together, getLevelList, setLevelList, and addLevel are worth 10 points.
    • advanceLevel (10 points)
      • Create a method named advanceLevel which takes no parameters and returns void. When this method is called, it should find the current level within the list of levels, and call this.loadLevel on the next level in the list.
      • You should use the getCurrentLevel method to access the current level. If the current level is not in the level list, including when the list is empty, this method should not do anything.
      • You should use the getName method on each level object to determine if it is the current level (ie. if the names are equal). This method may assume that there will not be multiple levels with the same name in the list.
      • If the game is already on the last level, this method should do nothing. You should never call loadLevel with an argument of null.
    • removeLevelByName (10 points)
      • Create a method named removeLevelByName which takes a String and returns void. It should remove the first level in the list which has the same name as the input String.
      • If multiple Levels in the list exist with this name, only the first should be removed.
      • You should use the getName method on each level object to determine if its name is the same as the input String.
    • Optional - resetGame
      • This method is entirely optional. Although it improves the quality of the game somewhat, it will not be tested, and you do not need to complete it if you do not wish to. Whether you complete it or not will not affect your submission or grading, as long as your code still compiles.
      • There are several actions which can be taken within the game that reset a game to its initial state, such as pressing "F2". To fully reset the game, each level in the linked list of levels must be reset, and the current level must be set to the head of the list.
      • To reset a level, you can call the reset method on it.
      • To change the current level to the head of the list, you can access the instance variable this.currentLevel directly, and set it to the desired value. Note that this instance variable is of type Level, not LinkedListNode.
      • The head of the linked list should be the last to have reset called on it. Otherwise, the player will start at the wrong location.


Testing


You are not given any tests for this portion. You are encouraged to write your own and test in game. Once you've completed the PhysicsEngineWithGravity portion of this assignment, you'll be able to verify if some of these methods are functioning properly by playing Mario.



Programming Requirements


Implement the methods from the specification. You are encouraged to write tests before you begin your implementation and run them throughout your implementation.



Autolab Feedback


Autolab will display the results of several features for this portion, and the number of points earned for each feature. Some methods may be split into multiple features; these methods have partial credit. Successful completion of this portion will earn you 30 points.

PhysicsEngineWithGravity and Mario

Overview


For this portion, you will be writing code in the following classes which already exist:

  • PhysicsEngineWithGravity, located in the app.gameengine.model.physics package.
  • LevelParser, located in the app.gameengine package.


Specification


You should read through all the following sections before you begin to implement the methods from the specification. You must at least create every class/method from this specification to receive feedback on this section. 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.

Many of the methods in this task will make use of the LinkedListNode class, found at app.gameengine.model.datastructures. You are required to use this class. It is identical to the LinkedListNode class used in the Problem Set.

For this portion of the Game Features task, you will implement the following functionality:

  • Implement the following methods in the PhysicsEngineWithGravity class. Notice that this class extends the PhysicsEngine class, and as such has access to all of the public methods from that class, including those that you wrote in the previous task.
    • The PhysicsEngine methods are all tested together and worth 25 points with no partial credit.
    • Constructor
      • You do not need to create a new constructor, but you should modify the existing constructor which takes a single double as a parameter and represents the amount of gravity. You should create an instance variable to store the gravity, and assign it in the constructor.
    • getGravity
      • Write a method named getGravity that takes no parameters and returns a double. This method should return the gravity of this physics engine. Note that this is different from the static variable "DEFAULT_GRAVITY". You should return the instance variable that was described above.
    • setGravity
      • Write a method named setGravity that takes a double and returns void. This method should set the gravity instance variable to the input value.
    • updateObject
      • Write a method named updateObject which takes a double and a DynamicGameObject and returns void. This method should override the method of the same name in the PhysicsEngine class.
      • This method should apply gravity to the passed in object, according to the change in time passed in. You should 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) (note also that the positive y direction is down).
      • Call the super class' updateObject method and pass in the parameters. Be sure to call this after applying gravity, to make sure that velocity and position are appropriately adjusted.
      • This method should only apply gravity to objects that are in the air. This is because when an object is on the ground, applying gravity would only cause it to clip into the ground, and make movement more difficult. To determine if an object is in the air or on the ground, you can use the isOnGround method on the DynamciGameObject from the parameter, which returns true if the object is on the ground, and false otherwise.
      • This method should not apply gravity to the object if that object is the player. This is because the player's gravity is handled separately, within the controls, and we do not want to apply it twice. To determine if an object is the player, you can use the isPlayer method on the DynamicGameObject from the parameter, which returns true if the object is the player, and false otherwise.
    • With these few modifications, and the power of inheritance, you have just created gravity. Any game that uses a PhysicsEngineWithGravity object instead of a basic PhysicsEngine will now have gravity applied automatically. You can play Mario, as described above, to see the effects (as long as you have also made the required changes to the LevelParser).
  • Implement the following updates to the LevelParser class. These changes will allow the game engine to parse levels which contain objects for Mario.
    • The updates to LevelParser are worth 5 points all together with no partial credit.
    • readDynamicObject
      • The existing structure of this method uses a switch-case statement. A switch-case statement is much like an if statement, where it checks if the expression after the keyword "switch" is equal to each value after "case". The first case that is equal is entered, and the code inside is performed. You may modify this method to use a series of if-else statements if you prefer, but it will likely be easier to extend the existing switch-case structure.
      • Add a case for Goomba objects. If the string being checked is exactly "Goomba", a new Goomba object should be returned. The Goomba constructor takes only two doubles, which should be the variables x and y which already exist in this method.
      • Add a case for Koopa objects. If the string being checked is exactly "Koopa", a new Koopa object should be returned. The Koopa constructor takes only two doubles, which should be the variables x and y which already exist in this method.
    • readStaticObject
      • As with readDynamicObject, this method uses a switch-case statement.
      • Add a case for Block objects. If the string being checked is "Block", "Bricks", or "Ground", a new Block object should be returned. The Block constructor takes two doubles and a String. The doubles should be the variables x and y which already exist in this method. The String should be the same as the String being checked. For example, if the String is "Bricks", "Bricks" should be passed into the constructor.
      • Add a case each of the following objects: QuestionBlock, HiddenBlock, PipeEnd, PipeStem. For each of these objects, the String should exactly match the class name (eg. "QuestionBlock" or "PipeStem"), and the only constructor parameters are two doubles, which should be the variables x and y which already exist in this method.
      • Add a case for Flag objects. If the string being checked is exactly "Flag", a new Flag object should be returned. The Flag constructor takes two doubles and a Game, which should be the variables x, y, and game which already exist in this method.
    • parseLevel
      • Modify this method to return either a TopDownLevel or a MarioLevel depending on the content of the csv file being read.
      • The level type can be determined from the first field in the first line of the level file. If that field is exactly "TopDownLevel", then the level you create, modify, and return should be a TopDownLevel object. If that field is "MarioLevel", it should be a MarioLevel object instead.
    • Your LevelParser should still be capable of reading and parsing all previously assessed levels. However, now it can also create levels for Mario with the appropriate objects.


Testing


You are given tests for this portion. See the "Overview" section for more details on how to get these tests.



Programming Requirements


Implement the methods from the specification. You are encouraged to run the provided tests and game throughout your implementation.



Autolab Feedback


Autolab will display the results of several features for this portion, and the number of points earned for each feature. Some methods may be split into multiple features; these methods have partial credit. Successful completion of this portion will earn you 30 points.

Pathfinding

Overview


For this portion of this task, you will be implementing functionality in the Agent class (from the app.gameengine.model.gameobjects package) and the PathfindingUtils class (that you should create in the app.gameengine.utils package) such that enemies have somewhat functional pathfinding. This will be most obvious in the Sample Game, which you can access by changing the GAME constant in app.Configuration to "Sample Game".

Each of the methods you implement will have an associated point count that you will earn upon passing our tests for that method. Partially implemented methods may result in some partial credit being earned, depending on the method.


Programming Requirements


  • findPath (20 points)
    • Create a class in the app.gameengine.utils package named PathfindingUtils. In this class, implement the following static method:
    • In the PathfindingUtils class, write a static method named findPath that takes in two Vector2D objects as parameters and returns a LinkedListNode of Vector2Ds.
    • This method should return a shortest valid path that travels from the tile containing the first Vector2D object to the tile containing the second Vector2D object.
      • To compute the tile containing a vector, you can take the floor of each coordinate in the vector. Recall that locations for rectangles are the location of their upper-left corner.
      • The vector (3.9, 4.6) is contained on the tile at location (3.0, 4.0)
      • The input Vector2Ds themselves should not be modified. You should instead create copies of them to modify. There are several ways to do this, such as: create a new Vector2D using the constructor, with the desired x and y components; use the Vector2D.copy method, and modify the returned vector; use the static Vector2D.floor method, which returns a new vector with floored components.
    • A path is valid if each Vector2D object after the head of the list is one tile above, below, to the left, or to the right of the previous one AND if each Vector2D object in the list is aligned to a tile (i.e. no decimals). Considered the following paths attempting to travel from eg. (3.9, 4.6) to (5.2, 5.999)
      • [(3.0, 4.0), (3.0, 5.0), (4.0, 5.0), (5.0, 5.0)] is valid.
      • [(3.0, 4.0), (3.0, 5.0), (5.0, 5.0)] is not valid as the third Vector2D is two tiles right of the second.
      • [(3.0, 4.0), (4.0, 5.0), (5.0, 5.0)] is not valid as the second Vector2D is diagonal to the first one.
      • [(3.9, 4.6), (3.1, 5.9), (4.6, 5.5), (5.2, 5.999)] is not valid as it is not aligned to a tile.
    • You may find it helpful to review the overview of the coordinate system in the task 1 handout.
    • Note that there are many correct solutions for this method since there are multiple valid paths that all of minimal length (unless the start and end share a coordinate). For example, traveling from (0, 0) to (1, 1) both [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)] and [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)] are correct solutions. Any valid path will be accepted by our test cases.
  • followPath (20 points)
    • In the Agent class, there exists a method named followPath that takes a double and returns void. The input double represents the change in time since the last time this method was called. Complete this method so that the agent along its path if it has one.
    • This class already contains an instance variable representing this object's path, called path, as well as a getter and setter for that instance variable, named getPath and setPath respectively. You will need to use these to complete this method.
    • In each update call, only one of these three things can happen. If one of these conditions is met, in this order, it should be done, and then nothing else should be done.
      1. Idle: If the Agent's path instance variable is null, their velocity in the x and y directions should be set to 0, and nothing else should be done.
      2. Advance to the next node in the path: If the Agent is directly on the tile at the path's head, the Agent's position is manually set to the location of that tile. The head of the list is then removed from the list so the agent will travel to the next node the next time this method is called.
        • An enemy is considered to be "directly on" a tile if its distance from the tile is less than its movement speed times the change in time since the last update. The movement speed can be accessed by calling this.getMovementSpeed, or with the movementSpeed instance variable, both of which already exist in this class. The change in time is the parameter dt.
        • Use Euclidean distance for this calculation, which is calculated as the square root of the sum of the squares of differences in each dimension. It is calculated as √((x1-x2)2 + (y1-y2)2). You may use the Vector2D.euclideanDistance method, which calculates this for you.
        • You must also set the agent's velocity in the x and y directions to 0.
        • These steps are necessary to prevent enemies from getting caught at the edges of hitboxes.
      3. Travel along the path: If neither of the 2 previous conditions apply, the Agent's velocity should be set to move towards the tile at the head of the Agent's path.
        • The Agent's velocity must have a magnitude (speed) equal to its movement speed. The movement speed can be accessed by calling this.getMovementSpeed, or with the movementSpeed instance variable, both of which already exist in this class.
        • The Agent's orientation should be set in the same direction as its velocity, but always with a magnitude of 1.0.
        • Since the Agent can only move in four directions, only one component (x or y) of a moving Agent's velocity or orientation will be non-zero at any point. This means there are only 4 possible values for the Agent's orientation, (1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), or (0.0, -1.0). Velocity is much the same, except that the magnitude will depend on the movement speed.
        • For example, if an Agent is located at (2.5, 2), has a movement speed of 4, and its path is [(3, 2), (4, 2), (4, 3)], it must first target the point (3, 2). To reach that point, it must travel in the positive x direction, so its velocity would be set to (4, 0), and its orientation would be set to (1, 0).
  • These two methods comprise this portion of this assignment. However, you will not yet be able to see these changes in-game. This is intentional, as the next two tasks add a modular behavior system that can make use of this pathfinding. However, if you want to see this behavior in game early, you can do the following:
    1. In the Agent.update method, add code that checks if the path instance variable is null. If so, call the findPath method, where the start is the location of the Agent, and the end is the location of the Player (obtained with level.getPlayer().getLocation()), and call setPath with the returned path.
    2. In the Agent.update method, if the path is not null, call the followPath method, passing in dt as the change in time.
    3. When completing Task 3, you will have to modify the Agent.update method in a different way. Those changes should replace these ones.
  • Additionally, you can just include the first part which sets a path obtained from calling findPath, and press F5 in game to see the path on screen. There are a variety of other debug controls you may find helpful which can be seen by pressing "Q." More information on these can be found in the readme.


Testing


You are not given tests for these methods. You are encouraged to use one of the previously discussed ways to show paths in-game, as well as writing tests and running the debugger, to verify the correctness of your solution.



Autolab Feedback


Autolab will display the results of several features for this portion, and the number of points earned for each feature. Some methods may be split into multiple features; these methods have partial credit. Successful completion of this portion will earn you 40 points.