You will continue to add functionality to your existing project from the previous task. There is no new repository to clone.
Video link: https://youtu.be/pdIPmXGCpVU
You get it by now
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
In this task, you will implement the following specs. Once all of these are complete, your enemies will have an AI that allows them to make decisions based on their surrounding environment.
DynamicGameObject
- Invincibility Frames (app.gameengine.model.gameobjects.DynamicGameObject
)
double
to the DynamicGameObject
class
representing the amount of
time remaining on their invincibility frame timer in seconds. This should be initialized to 0.0
since
i-frames will not start until triggered by a game action (ie. The player getting hit by an
enemy). Add getters and setters for this instance variable named
getInvincibilityFrames
and setInvincibilityFrames
update
method that was inherited from the GameObject
class
destroy
method
Projectile
(app.games.topdownobjects.Projectile
)
collideWithDynamicObject
method in
Projectile
to
deal
damage to the other object rather than destroying them. (Replace the call to
otherObject.destroy
with otherObject.takeDamage
.) Use the
projectile's damage instance variable in your call to takeDamage
Enemy
(app.games.topdownobjects.Enemy
)
collideWithDynamicObject
method to deal damage to
the
player rather than destroying them. The amount of damage dealt (By calling the takeDamage
method)
should be equal to the enemy's strength instance variable.
DynamicGameObject
, along with the getter getPath
and setter
setPath
. Since Enemy extends DynamicGameObject, this should not have any effect
on the game because of the power of inheritance.
update
method. Enemies will now use the inherited update method
unaltered. For pathing behavior, we will add decision trees to make enemies do more than always
move towards the player (You should save this code in a safe place as it will be reused in the
decision tree. If you lose your code, you can always download your old submissions from Autolab
to recover it)
Now, you should be able to run into enemies and watch your health go down in the UI without instantly
being reset to the beginning of the level. You should also be able to shoot enemies and have them take
damage rather than dying right away.
Note that the default projectile damage is 5, meaning you can kill
enemies in 2 hits. If you wish to change this, you can edit the actionButtonPressed
method in
app.gameengine.Level
to create projectiles which deal more or less damage. This should not interfere with
testing in Autolab, since this method is only called when the game is running
Now let's start working with trees. For the rest of this task, you will build a decision tree for the AI of the enemies. Each node of the tree will either make a decision (Move to the left or right child), or take an action (when we reach a leaf of the tree, a node with no children). You'll create a variety of new classes to implement this functionality.
Decision
(app.gameengine.model.ai
)
ai
package, create a new class named Decision
getName
and setName
for the name instance
variable.
This name will be used when testing and debugging your decision tree
decide
that returns a boolean and takes in a reference to a
DynamicGameObject
, Level
, and a double
dt (in this
order). This method should be blank initially (just return false). You will be overriding this
method using inheritance
doAction
that returns void and takes in a reference to a
DynamicGameObject
, Level
, and a double
dt (again, in this
order). This method should be blank and will also be overridden using inheritance
DecisionTree
(app.gameengine.model.ai
)
ai
package, create a new class named DecisionTree
BinaryTreeNode<Decision>
and store it in
an
instance variable
getTree
and a setter setTree
for that instance variable
traverse
that returns a reference to a Decision
and
takes
a BinaryTreeNode<Decision>
, DynamicGameObject
,
Level
,
and double
dt as parameters (In this order)
decide
on the value of each tree node. A value of true indicates that you
should "travel" to the right child node, and a value of false means travel to the left
child node. When a leaf node is
reached (A node where both left and right children are null), the Decision
value at that node should be returned. Keep in mind that the node in
the parameter itself might be a leaf node
decide
, pass the required arguments using the parameters of
this method
traverse
method that returns void and takes a
DynamicGameObject
,
Level
, and double
dt as parameters (In that order). This method
will call the traverse method you wrote previously with the instance variable tree as the first
argument and all the parameters of this method as the remaining arguments.
Then call doAction
on the returned Decision
, again using the
parameters of this method as arguments
Decision
is null, do nothing
reverse
that returns void and takes a reference to a
BinaryTreeNode<Decision>
as a parameter
reverse
method that returns void and takes no
parameters. This method
will call the reverse method you wrote previously with the instance variable tree as the
argument
app.gameengine.model.ai
)
ai
package. While the amount of classes
here may seem daunting, each class, ignoring all the imports and class declaration boilerplate,
should have relatively few lines of code; try not to overthink these classes.
LowHP
LowHP
class that extends Decision
. Add a
constructor that takes in a String for the name (so that the super constructor
may be
called) and an int representing the HP threshold the enemy should have before
it's considered
"low" on HP. You should make an instance variable to store this value
decide
and return true if the
DynamicGameObject's
health is less than or equal to the Decision's low HP threshold
NearPlayer
NearPlayer
class that extends Decision
. Add
a
constructor that takes in a String for the name (so that the super constructor
may be
called) and a double representing the acceptable distance that this node should
use. You
should make an instance variable to store this distance
decide
method and return true if the
distance
between the DynamicGameObject and the player is less than or equal to the
acceptable
distance instance variable and false otherwise. You can access the player
through the level object (Hint: Use the Pythagorean
theorem/Euclidean Distance to determine distance!)
TargetingPlayer
TargetingPlayer
class that extends Decision
.
Add a
constructor that takes in a String for the name (so that the super constructor
may be
called) and a double representing the distance the player can stray from where
the
DynamicGameObject is targeting. You should make an instance variable to store
this distance.
decide
method and will return true if the
distance from the DynamicGameObject's target destination (the tail node of their
path) to the player's current location is less than or equal to the distance
instance variable and false otherwise (as previously, you should being using the
Euclidean Distance in your calculation). Also return false if the path is empty
MoveTowardsPlayer
MoveTowardsPlayer
class that extends
Decision
. Add a
constructor that takes in a string for the name and call the super constructor;
there are no additional parameters or instance variables for this class
doAction
method and make the
DynamicGameObject move towards the player. (Hint: you should move all your
update
code in Enemy from task 2 to this method. This includes
all 3 cases that we had to either calculate a path, remove the head of the path,
or travel to the head of the path)
Heal
Heal
class that extends Decision
. Add a
constructor
that takes in a String for the name (so that the super constructor may be
called), an
int representing the health to heal, and a double representing the cooldown
time before
healing. You should make 2 instance variables to store these values. You will
also want a 3rd instance variable storing the amount of time spent on this node
doAction
method to heal the
DynamicGameObject once the cooldown expires. When this method is called, use the
dt parameter to add the total time that has elapsed across all calls of this
method (ie. by adding this time to your 3rd instance variable). If the value of
this 3rd variable (The total time spent on this node) is greater than or equal
to the cooldown time (The 3rd constructor parameter), heal the DynamicGameObject
(Using
get/set hp) by the amount given by the 2nd constructor parameter. When the
object heals, reset your 3rd instance variable so they have to wait for the
cooldown again before healing
doAction
method is called, both x and y velocity should be set to 0
app.gameengine.controller.WASDMovement
and
app.gameengine.model.gameobjects.Player
RecalculatePath
RecalculatePath
class that extends Decision
.
Add a
constructor that takes in a string for the name and call the super constructor;
there is no additional instance variables you will need.
doAction
method and should set the
DynamicGameObject's velocity to 0 and recalculate the DynamicGameObject's path
with the destination at the player just like in MoveTowardsPlayer
(Set their path to the result of Pathfinding.findPath). You do not have to
handle all 3 cases like before. Always set their path when this method is called
TargetingPlayer
will allow you to build trees
that will update the path as the player moves so enemies are no longer moving to
your old position
RunAway
RunAway
class that extends Decision
. Add a
constructor that takes in a string for the name and call the super constructor;
there is no additional instance variables you will need.
doAction
method and will do the same as what
MoveTowardsPlayer
does, but the velocity should be reversed
(multiplied by
-1)
DynamicGameObject
- DecisionTree implementation
DecisionTree
instance variable, along with a getter
getDecisionTree
and a setter setDecisionTree
. Modify the
update
method to call traverse
on the DynamicGameObject's instance
variable if it is not null (the DynamicGameObject
parameter in traverse should be this
and the remaining arguments come from the parameters of the update call). In the
Enemy
constructor, you should assign the DecisionTree
to have one
node containing a MoveTowardsPlayer
decision. This is important so that the default
behavior of an enemy remains unchanged, and your Task 2 tests still pass
Enemy
class. You should add the decision
tree in the constructor which takes two parameters, as the other constructor simply calls this
one
SampleTopDownGame
class
for each enemy that you wish to enhance.
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.
Write a public static method named compareBinaryTreesOfDecisions
in the
app.tests.TestTask4
class that returns void and takes two BinaryTreeNode
of
Decision
s as parameters. This method should fail a junit assert if the two
trees are not equal and do nothing if they are equal. You should compare the inner Decision
value's getName
method to check for equality. Two nodes are considered equal, according to this
method, as long as their names are the same. The trees must additionally have identical structures
Testing your traverse method using the Decision
child classes you created previously would be
quite difficult and tedious. You would need to create a game, level, player, and enemy with specific
characteristics (eg. location, health, etc) so that the tree traversal happens in exactly the way you want.
Because of this, it is highly recommended that you create a new class in your tests
package
that extends Decision
. The name of this class is not important as long as it is in the tests
package and is only used inside of the tests package. If either of these rules is not upheld, autolab
will not be able to compile your code
The purpose of this testing class is to give you greater control over how the tree traversal occurs, without
relying on complicated state of the game, level, player, and enemy. One possible way to achieve this control
would be to make the constructor of this class take in a boolean, and have the decide
method
use this boolean to determine the direction to traverse.
You must also test that the doAction
method is properly called when a leaf node is reached.
To check this, your test decision class should also override the doAction
to have some obvious
and testable side effect on either the enemy or level object passed in to the method. The specifics of this
side effect are unimportant, as long as you detect it properly in your tests.
Note: since this specific type of decision will never be used in game, it's okay that it doesn't make sense
in the context of the game. For example, in this specific case, it is okay to override both the
decide
and doAction
methods, even though this is prohibited for other types of
decisions.
This class is not required and will not be tested on autolab, but it is recommended that you do this as it will make your testing significantly easier.
Create a class named TestTask4
in the tests
package.
You will write tests for the following functionality from the specification:
traverse
& reverse
methods in the
DecisionTree
class
reverse
method, you should test the version that takes no
parameters. When testing the traverse
method, you must test the version that
returns void. It may also be helpful to test the other version. The reason for this is that the
other methods are helper methods, so testing the base methods will test both
While you are not required to use your testing utility and test object, it is recommended. They will make your tests considerably easier to write.
Implement all the functionality from the specification.
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 provided.
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.