Skip to content
Jim edited this page Oct 7, 2024 · 26 revisions

Enter the Gungeon

README

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.

⚠️ This HW may take you a while (there's a lot to wrap your head around); I recommend starting it early 🙂👍

Math

Direction

A direction can be represented by a unit vector (vector with length $1$).

The (direction) unit vector pointing from point $\mathbf{a}$ to point $\mathbf{b}$ is given by $\mathbf{d} = \left(\frac{\Delta_x}{|\Delta|}, \frac{\Delta_y}{|\Delta|}\right)$, where:

  • $\Delta_x = (b_x - a_x)$
  • $\Delta_y = (b_y - a_y)$
  • $|\Delta| = \sqrt{{\Delta_x}^2 + {\Delta_y}^2}$

A velocity $\mathbf{v}$ is a scalar speed $s$ times a direction $\mathbf{d}$.

$\mathbf{v} = s\mathbf{d} = (s d_x, s d_y)$

Programming

Sum Type (ish)

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.

Pool

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 flag inUse = true; ("activates it")
  • 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
  • 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;
        }
    
        ...
    }

Java

class

  • 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])) {
        ...
    }

this

  • 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);
        }
    
        ...
    }

Reference

HW03A

TODO

  • A-

    • Update Cow
    • Implement Thing's draw instance method using a single call to drawCircle(...)
      • 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()
    • 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 in main()
    • Make it so bullets move with a constant velocity
      • NOTE: Implement this inside of the bullet-specific update in main()
    • 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;
  • 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
    • 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
    • Make it so the player's bullets kill the enemies after three hits
      • NOTE: Implement the Thing static method collisionCheck(...) 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; to Thing
      • HINT: You will need to use a for loop (with a few continue's or if's inside)
    • 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), where thing 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
  • 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
      • 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

Starter Code

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);
			}

		}

	}

}
Clone this wiki locally