-
Notifications
You must be signed in to change notification settings - Fork 0
HW03
Enter the Gungeon
In this HW, you will fill in the missing functions for a simple 2D game engine, and then use it to write your own game.
A direction can be represented by a unit vector (vector with length
The (direction) unit vector pointing from point
$\Delta_x = (b_x - a_x)$ $\Delta_y = (b_y - a_y)$ $|\Delta| = \sqrt{{\Delta_x}^2 + {\Delta_y}^2}$
A velocity
This homework will use a single class called Thing
to represent a bunch of different kinds of things (players, enemyies, bullets, ...).
We will do this in a way similar to the concept of a sum type (tagged union), which in Java just means something like this:
class Thing {
// This is the Thing's "type"
int type;
// These are the different types of Things
// NOTE: These are just constants.
static final int TYPE_NONE = 0;
static final int TYPE_PLAYER = 1;
static final int TYPE_BULLET = 2;
static final int TYPE_ENEMY = 3;
...
}
Since each Thing
in our game has a type, we can do stuff like this:
if (thing.type == Thing.TYPE_PLAYER) {
// update for player
...
} else if (thing.type == Thing.TYPE_BULLET) {
// update for bullets
...
} else if (thing.type == Thing.TYPE_ENEMY) {
// update for enemies
...
}
One last note is that some Thing
instance variables will not be used by all types Thing
's. For example, a bullet needs to store its velocity, but the player actually does not. For our purposes, this is totally fine.
This HW uses an advanced technique called a pool. Our pool is a big block (array) of game objects that we create before the game actually starts looping. NOTE: Everything is stored in this pool (the player, every enemy, every bullet, etc.)
- Each "thing" (game object, entity, "slot") in the pool has a flag
boolean inUse;
which says whether the thing is currently being used by the game- Things that are not being used are "inactive" or "dead," and are just waiting around to be used for something ("activated")
- To get a reference to an unused thing, call the function
Thing acquireThing() { ... };
-
NOTE: Before returning a reference to a
Thing
, this function clears it to zero, and sets its flaginUse = true;
("activates it")
-
NOTE: Before returning a reference to a
- To mark an object as no longer in use, set
inUse = false
.- Forget to do this, and the pool will eventually "get full" of "stale" entities; calling
acquireThing()
with a full pool will crash the game
- Forget to do this, and the pool will eventually "get full" of "stale" entities; calling
- To iterate over only the objects currently in use (the "live objects"), use something like:
for (int i = 0; i < pool.length; ++i) { if (!pool[i].inUse) { continue; } ... }
-
A class is a (blueprint for) a chunk of data (group of variables) and associated functions
class Thing { // instance variables boolean inUse; int type; int age; double x; double y; double radius; Color color; // static variables (these happen to all be constants) static final int TYPE_NONE = 0; static final int TYPE_PLAYER = 1; static final int TYPE_BULLET = 2; static final int TYPE_ENEMY = 3; // instance methods void draw() { ... } // static methods static boolean collisionCheck(Thing a, Thing b) { ... } }
-
New instances of a class (objects) can be created (instantiated) with the
new
keyword (the same is true of arrays)- NOTE: In Java, the default constructor zero-initializes objects (clears all the variables to zero)
// // Set up the Thing pool // Make a new array of 128 Thing references // All are initially null Thing[] pool = new Thing[128]; // Actually hook up the references to new, zero-initialized Thing objects for (int i = 0; i < pool.length; ++i) { // // Thing() is a "default constructor" // pool[i] will refer to a Thing instance (initially) with... // - inUse = false; // - type = Thing.TYPE_NONE; // - age = 0; // - x = 0.0 // - y = 0.0 // - radius = 0.0 // - color = BLACK (color.r = 0.0, color.g = 0.0, color.b = 0.0) pool[i] = new Thing(); }
-
IMPORTANT: In Java (and Python), the programmer only ever touches references to objects
-
NOTE: In Java, a reference being
null
(zero) means that it refers to nothing// thing refers to nothing (it is a null reference) Thing thing = null;
// thing refers to the same object that pool[i] does Thing thing = pool[i];
-
You can access an object's instance variables (fields) using a reference and the dot operator
if (thing.health < 0) { ... }
-
You can call an object's instance methods (functions) using a reference and the dot operator
thing.draw();
-
A static variable (class variable) "lives on its class"; there is only one persistent copy of it no matter how many times the class is instantiated
-
NOTE: We've actually been using these the entire semester; For example, we never instantiated the
HW01
"wrapper class"; all its variables were static// The TYPE_BULLET constant lives on the Thing class // We don't need to have a Thing instance to access it pool[i].type = Thing.TYPE_BULLET;
class HW03 { // This array full of the Thing references "lives on the HW03 class"; there is only one of these arrays static Thing[] pool; ... }
-
A static method is similar; you don't need an instance of the class to call it
if (Thing.collisionCheck(pool[i], pool[j])) { ... }
- In an instance method, the
this
keyword refers to the instance of the class that called the method-
NOTE: If there is no ambiguity, using
this
is technically optional; i recommend using it regardless
class Thing { void draw () { drawCircle(this.x, this.y, this.radius); } ... }
-
NOTE: If there is no ambiguity, using
- YouTube Link (A): https://youtube.com/shorts/T-Rm7Oswjsg
- YouTube Link (A+): https://youtu.be/s2PlR7f_06w
-
A-
- Update Cow
- Implement Thing's
draw
instance method using a single call todrawCircle(...)
- NOTE: After implementing this, the player should show up as a red circle
- Make it so the player can move using WASD (
keyHeld(...)
)-
NOTE: Implement this inside of the player-specific update in
main()
-
NOTE: Implement this inside of the player-specific update in
- Make it so the player can fire a bullet by clicking the mouse (
mousePressed
)-
NOTE: Implement Thing's
fireBullet(...)
instance method as described in the code -
NOTE: Call
fireBullet(...)
inside of the player-specific update inmain()
-
NOTE: Implement Thing's
- Make it so bullets move with a constant velocity
-
NOTE: Implement this inside of the bullet-specific update in
main()
-
NOTE: Implement this inside of the bullet-specific update in
- Make it so bullets "die" after being alive for 128 frames
-
NOTE: Implement this inside of the bullet-specific update in
main()
-
HINT: Thing has an instance variable
int age;
-
NOTE: Implement this inside of the bullet-specific update in
-
A
- Add two large, blue enemies to the game
- Make the enemies slowly chase the player
-
HINT: There is a helpful
player
reference that you can use
-
HINT: There is a helpful
- Make the enemies occasionally fire larger, slower-moving bullets at the player
-
HINT: You'll need some sort of counter for this;
age
will work
-
HINT: You'll need some sort of counter for this;
- Make it so the player's bullets kill the enemies after three hits
-
NOTE: Implement the
Thing
static methodcollisionCheck(...)
and use it - NOTE: A bullet dies whenever it hits an enemy
- NOTE: Bullets do not hit other bullets
-
HINT: You can add an instance variable
int numTimesHit;
toThing
- HINT: You will need to use a for loop (with a few continue's or if's inside)
-
NOTE: Implement the
- Make it so the enemies's bullets kill the player after one hit
-
NOTE: You will need to have some way to tell which bullets hit which things; otherwise the player will get hit by their own bullets
-
HINT: The easiest way is probably to test
(bullet.color == thing.color)
, wherething
is a candidate for getting hit by bullet; This is sloppy but should work just fine assuming you don't ever deep copy the colors
-
HINT: The easiest way is probably to test
-
NOTE: You will need to have some way to tell which bullets hit which things; otherwise the player will get hit by their own bullets
-
A+
- Make the game good; inspo:
- Upgrade
draw
to include vapor trails for bullet-type Things based on their velocity - Upgrade
draw
to include hit animations for enemy-type Things-
HINT: Add an
int framesSinceHit;
instance variable
-
HINT: Add an
- Add some cool particle effects (Google this)
- Create a new
THING_TYPE_COIN
; put some coins in the world (and add more when an enemy is killed); allow the user pick them up - Make the game reset after the player dies
- HINT: LERP
- Upgrade
- Make the game good; inspo:
class HW03 extends Cow {
static class Thing {
boolean inUse;
int type;
int age; // how long the Thing has been in use
double x; // positionX
double y; // positionY
double radius;
Color color;
double velocityX;
double velocityY;
static final int TYPE_NONE = 0;
static final int TYPE_PLAYER = 1;
static final int TYPE_BULLET = 2;
static final int TYPE_ENEMY = 3;
void draw() {
// TODO
}
// Fires a bullet by...
// 1) acquiring a Thing (this is done for you)
// 2) setting its fields:
// - type should be Thing.TYPE_BULLET
// - radius should be 1/4 the radius of the Thing that fired it
// - color should be color of the Thing that fired it
// - position should be the position of the Thing that fired it
// - velocity should be a length speed vector pointing from source to target
// HINT: this.x is the x position of the Thing instance that called this function
void fireBullet(double targetX, double targetY, double speed) {
Thing bullet = acquireThing();
// TODO
}
// returns whether two Thing's collide (overlap)
static boolean collisionCheck(Thing a, Thing b) {
// TODO
return false;
}
}
// The Thing pool (a big array of Thing object references)
static Thing[] pool;
// Acquires a thing by...
// 1) finding the first unused Thing in the pool,
// 2) clearing it to zero, and
// 3) returning a reference to it.
// NOTE: Crashes the program if thing pool completely full
static Thing acquireThing() {
for (int i = 0; i < pool.length; ++i) {
if (!pool[i].inUse) {
// FORNOW: using new Thing() instead of pool[i].reset())
// so students don't have to maintain a reset() method
pool[i] = new Thing();
pool[i].inUse = true;
return pool[i];
}
}
PRINT("Error: Thing pool is completely full.");
ASSERT(false);
return null;
}
// Convenient references into the pool
static Thing player;
public static void main(String[] arguments) {
// Set up the pool
pool = new Thing[128];
for (int i = 0; i < pool.length; ++i) {
pool[i] = new Thing();
}
// Set up the player
player = acquireThing();
player.type = Thing.TYPE_PLAYER;
player.radius = 32.0;
player.color = RED;
while (beginFrame()) {
// Update all live things
for (int i = 0; i < pool.length; ++i) {
if (!pool[i].inUse) {
continue;
}
// Common updates
pool[i].age++;
// Specific updates
if (pool[i].type == Thing.TYPE_PLAYER) {
if (keyHeld('W')) {
pool[i].y += 5.0;
}
} else if (pool[i].type == Thing.TYPE_BULLET) {
// TODO
}
}
// Draw all live things
for (int i = 0; i < pool.length; ++i) {
if (!pool[i].inUse) {
continue;
}
pool[i].draw();
}
{ // IGNORE_ME: Draw debug info
int numThingsInUse; {
numThingsInUse = 0;
for (int i = 0; i < pool.length; ++i) {
if (pool[i].inUse) {
++numThingsInUse;
}
}
}
double eps = 16.0;
drawString("pool is " + numThingsInUse + "/" + pool.length + " full", -256.0 + eps, -256.0 + eps, BLACK, 16);
}
}
}
}