There are two main components to this Game Features task. In the first part, you will be implementing various physics-related functionality to the game engine. In the second part, you'll be implementing functionality that will make the Snake game functional.
This document is split into multiple parts for organization - you do not necessarily need to fully complete the Physics Engine functionality before you begin Snake. However, future tasks will likely build extensively off the physics engine, so it's highly suggested you implement this first.
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 task; you should not reclone or remove said code for this task or any future Game Features components.
For this portion, you will be writing code in the following classes which already exist:
PhysicsEngine, located in the app.gameengine.model.physics package.
Wall, located in the app.games.commonobjects package.
In this task, you will continue to add feature to the game engine project that you started in the previous task.
The coordinate system use for computer graphics might be different than you're used to. We'll use the standard coordinate system that is typically used in video games (and anything involving rendering to a screen).
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).
Each object within the game has several important properties to consider, namely a location and a hitbox, which has its own location and dimensions. These quantities are each represented as 2-dimensional vectors (x, y). All objects and hitboxes are rectangles, and their location is the location of their upper left corner. Their dimensions are the width and height of the rectangle. For example, the goal (G on the grid above) 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.
Object locations and dimensions also do not necessarily have to be aligned with the visuals of that object. In the image above, the green dot represents the location of that object, the blue outline represents the size of the sprite image that is being rendered for that object, and the red outline represents the size of its hitbox. Having a hitbox smaller than the graphical sprite allows for more precise physics and collision, but means that extra care has to be taken when dealing with those properties. Pressing F4 in game shows overlays of each object's hitbox, which can be helpful for debugging.
You should read through all the following sections before you begin to implement the methods from the specification. Note that you will not receive feedback on the correctness of these methods until you've completed the testing component of this task (see the "Autolab Feedback" section for more details). However, you must at least create every class/method from this specification to receive feedback. 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.
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.
For this portion of the Game Features component for this task, you will implement the following functionality:
PhysicsEngine class:
updateObject (10 points)
updateObject method. This method should update the location of
the DynamicGameObject based on the velocity and amount of elapsed time.
DynamicGameObject class to
achieve this functionality, namely getVelocity, getLocation, and
setLocation. These methods are already provided for you.
getX and getY methods on the
Vector2D object returned by getVelocity.
getOverlap (10 points)
getOverlap method. This method should return a
double representing the minimum overlapping distance of the two
Hitbox objects from the parameters.
Wall.collideWithDynamicObject method, so referring to those
descriptions and images may be helpful.
Hitbox, note the following:
Hitbox using the getLocation method, which returns a
Vector2D
containing the x and y coordinate of said location.
Hitbox using the
getDimensions method, which returns a Vector2D containing the
width (in the x-component) and height (in the y-component) of the Hitbox.
Hitbox
objects. This means you must calculate the amount the two hitboxes are overlapping in both the x
and y direction, then return the smaller of the two.
getOverlap
on these Walls' hitboxes would return 0.25.
detectCollision (5 points)
detectCollision method. This method should return a
boolean
that is true if the two Hitboxes are colliding and false otherwise.
getOverlap applies here as well. In the same
example, calling detectCollision on those two Walls would return true, as they are
colliding by 0.25 which is greater than 0.
Wall class:
collideWithDynamicObject (25 points)
collideWithDynamicObject method. This method should adjust the
location of the DynamicGameObject passed in the parameter to ensure that they are
no longer overlapping, and zero the velocity in that direction.
DynamicGameObject. All state in the Wall should stay the same.
PhysicsEngine.getOverlap method. Thus, you should calculate the overlap based on
the Wall's and DynamicGameObject's hitboxes, which can be obtained
with the getHitbox method. You should be able to reuse much of the same logic for
this method.
setLocation with
otherObject.getLocation().getX() - 0.5 rather than
otherObject.getHitbox().getLocation().getX() - 0.5. Note that this is only for the
position update, and you should still use the hitbox's position to calculate the amount to
move).
You are given tests for the PhysicsEngine.getOverlap and Wall.collideWithDynamicObject methods.
You should run and debug using these tests as you implement these methods to guide you.
You are not given tests for the remaining methods. You are encouraging to write more tests using the same format as these given tests.
For this section, your code will be run against our tests for correctness. For each feature you successfully pass, you will earn some number of points. Completing all the methods in this section will earn you 50 points.
For this portion of this task, you will be implementing functionality in the
SnakeLevel class (from the app.games.snake package) such that the Snake game is fully
functional. You can access the Snake game by changing the GAME constant in
app.Configuration to "Snake".
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.
You don't need to understand all of the internal Snake code; however, you should expect to read and understand some of the internal Javadoc comments while completing this task. Not all background information will be provided in this document.
SnakeLevel class, implement the following methods:
wallOffBoundary (10 points)
SnakeWall objects.
SnakeWalls. Adding a
SnakeWall
to the level can be done by calling the addStaticObject method.
spawnFood (10 points)
SnakeFood should be added to the level, as
well as the ArrayList named food in the SnakeLevel class. As with the
previous method, you can add a SnakeFood to the level by calling addStaticObject.
SnakeFood should be at a random tile-aligned coordinate, with the
following restrictions:
SnakeBody object from the tail
ArrayList is currently occupying.
SnakeFood object from the food
ArrayList is currently occupying.
Randomizer class from the app.gameengine.utils package.
randomIntVector2D
method that takes in two parameters.
advanceLevel method on the Game object stored internally in this class (this
object can be accessed with this.game) and return immediately, doing nothing else.
lengthenSnake (10 points)
SnakeBody objects will be added to the tail
ArrayList, as well as the level via addStaticObject. The number of
SnakeBodys to be added to the list will be dependent on the value of the
lengthIncrease instance variable.
SnakeBody object, the location of this object should be decided as follows:
tail ArrayList is empty, the new SnakeBody object should be
positioned such that it is directly behind the head (which is returned by
this.getPlayer()). This can be determined by looking at the head's location and
orientation vectors.
tail ArrayList is not empty, the new SnakeBody object should be
positioned such that it shares the location with the first SnakeBody object in the
tail ArrayList (the object at index 0).
SnakeBody is added to the tail ArrayList in this
method, it should be added to the front of the ArrayList (prepended) rather than the end. Recall that
calling add() with only one parameter defaults to adding to the end of the list
(appending).
spawnSnake (10 points)
SnakeBody objects to the level in the same manner as
lengthenSnake, but the number to be added should be based off of the value of the
startingLength instance variable instead. Note that you should add new
SnakeBody objects to both the tail ArrayList and to the level via
addStaticObject.
startingLength is based off of the total length of the snake including the
head, rather than just the length of the tail. Adding startingLength new
SnakeBody elements will result in an incorrect length.
tail ArrayList is empty initially when this method is
called.
moveSnake (10 points)
SnakeBody object at index 0 is now at the location that the
SnakeBody object at index 1 previously occupied and so on. The SnakeBody at
the last index of the tail ArrayList should have what was previously the location of the
Player.
SnakeBody object stays in place except for the
last. If you choose, you can only modify the location of that final segment to the proper place directly
behind the player.
tail ArrayList, the Player's location should then be
incremented by the Player's orientation. Remember that the Player can be accessed with
this.getPlayer().
Vector2D class
that you may find helpful for your implementation of any methods that require arithmetic
operations on Vector2D objects. However, using these methods is not necessary.
tail contains
SnakeBodys with locations [(0,0), (1, 0), (2, 0), (3, 0)], the Player's location would be
set to (4, 1) and tail will contain SnakeBodys with locations [(1, 0), (2, 0),
(3, 0), (4, 0)]. Repeating this (assuming orientation is unchanged) would result in the Player's
location being set to (4, 2) and
tail
containing SnakeBodys with locations [(2, 0), (3, 0), (4, 0), (4, 1)].
SnakeBody, ensure
that added objects are added to the level via addStaticObject as well as the
tail ArrayList. Removed objects must be removed from the tail ArrayList and
have the destroy() method called on them. This method can (and probably should) be
implemented without adding or removing SnakeBody objects.
You are not provided tests for any of these methods. You are encouraged to write your own if need be. You are also strongly encouraged to run the game to check your functionality.
Note: If you cloned the Game Engine repo early, you may have a test named testGetOverlapWithNoOverlap.
If so, you should remove the test as it tests undefined behavior in the getOverlap method. Additionally,
you may find that the game doesn't end when the snake collides with itself. If so, you should either pull from
the GameEngine repo to update your project, or replace the file src/main/java/app/games/snake/SnakeGame.java
with
this newer version.
For this section, your code will be run against our tests for correctness. For each feature you successfully pass, you will earn some number of points. Completing all the methods in this section will earn you 50 points.