Inheritance in GridEnv

In week3, we introduced the GridEnv via HungryTurtle and more.

Of note, Actor is an abstract class, and Turtle, Fruit, Rock and more all extend this abstract class. Let's think about why we would do this, with a smaller (but similarly graphical) example that we can create from scratch.

Then we'll discuss how GridEnv, Actor, and GridView work together.

What's an abstract class?

An abstract class is an incomplete class, but it's not quite an interface which is just a list of methods a class must implement. For instance, we might have an abstract class to build on our animal examples:

public abstract class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
    public String setName(String name) {
        this.name = name;
    }
    public abstract String says();

    public String toString() {
        return this.name + " says '" + this.says() + "'";
    }
}

This abstract class Animal has a String name field that's accessible via getName()/setName() it also demands that classes wanting to be an Animal implement the method String says().

For instance, a valid class Dog that implements Animal needs to have two things:

  1. a Constructor that provides a String name to Animal.
  2. a method public String says() which was marked abstract in Animal.
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    @Override
    public String says() {
        return "Woof!";
    }
}

We can now toss a main method into either class and create our first objects:

public static void main(String[] args) {
    Dog doug = new Dog("Doug");
    System.out.println(doug);
}

Which gives us the output:

Doug says 'Woof!'

We can use abstract classes to implement most of a class that we plan to have slight variations on.

GridEnv and Actor

The GridEnv class is a representation of a two-dimensional grid. It has three important instance variables:

public class GridEnv {
    public List<Actor> actors;
    public int width;
    public int height;
    // ... more variables & methods ...
}

At its core, GridEnv is a list of Actor objects, that happens to know about a 2d space of size (width, height).

An Actor is deeply connected to a GridEnv.

public abstract class Actor {
    protected int x;
    protected int y;
    protected GridEnv environment;
    // ... a few more variables / methods ... 
    public abstract void act();
}

If we look at GridEnv.insert and GridEnv.remove, where Actor objects get actually put into the world (or taken out), they look something like this (simplified):

// in class GridEnv:
public void insert(Actor actor) {
    this.actors.add(actor);
    actor.environment = this;
}

public void remove(Actor actor) {
    this.actors.remove(actor);
    actor.environment = null;
}

Inserting an actor, we add it to GridEnv's this.actors list, so we can keep track of it and we tell the actor about the current GridEnv. While removing, we remove it from the list and we tell actor that it's environment variable cannot point to us any longer.

This environment connection makes the following method on Actor work:

// in class Actor:
public void remove() {
    if (this.environment != null) {
        this.environment.remove(this);
    }
}

Because (once it's been inserted) an Actor always has this.environment set to the GridEnv that contains it, an Actor can be told to remove itself.

GridEnv.find

We've said that GridEnv is basically a list of actors. But the one method that gets the most use is GridEnv.find(int x, int y).

When we click on the grid, or try to take a step, or determine if there's fruit the turtle should be eating, what we care about is being able to simply find all the Actor objects in a given (x,y) position.

It turns out, it's a fairly basic loop to do so:

// in class GridEnv:
public List<Actor> find(int x, int y) {
    // create an empty list:
    List<Actor> output = new ArrayList<>();

    // loop over all actors
    for (Actor actor : this.actors) {
        // if they're in the spot we're asking about
        if (actor.x == x && actor.y == y) {
            // add them to output
            output.add(actor);
        }
    }
    return output;
}

This allows us to write things like the inspect on click that exists in GridView:

// in class GridView:
public void click(int x, int y) {
    List<Actor> clickedOn = this.grid.find(x, y);
    System.out.println("click: (" + x + "," + y + "): " + clickedOn);
}

Which typically outputs something like the following, if you click on a cell with the Turtle:

click: (3, 6): [Turtle@1]

... or an empty list, if you click on an empty cell in the GridEnv:

click: (1, 1): []

GridView? What's that.

Since GridEnv is basically just the 'data' behind the scenes of our worlds (HungryTurtle/FishRescue/etc.) ... it doesn't actually look like anything.

All the graphics code lives in GridView.java, with such loops as (simplified):

// Draw each actor:
for (Actor actor : this.grid.getActors()) {
    // compute the coordinates of the cell the actor is inside:
    cell.setFrame(actor.x * tileW, actor.y * tileH, tileW, tileH);

    // ... deleted code about scaling actors ...

    if (actor.visual != null) {
        // trust the draw method otherwise.
        actor.visual.draw(g, cell);
    } else {
        System.err.println("Warning: Actor " + actor + " has no drawable.");
    }
}

GridView is a concrete class with the intention that you will override specific methods. Here I'm abstracting or hiding away the number-crunchy details of how to draw things on a grid. It's not terribly difficult but it's just long and involves working out some pictures of things on a whiteboard, while reading about classes like Java's java.awt.geom.Rectangle2D.

The methods that are exciting are the buttons, click, and getHeaderText methods.

// defaults in GridView.java:

// what should we put at the top of the screen?
public String getHeaderText() {
    return this.getClass().getSimpleName();
}

// what should we do when the user presses some Buttons?
public void buttons(Buttons current) {
    System.out.println("Buttons: " + current);
}

// what should we do when the user clicks on a cell?
public void click(int x, int y) {
    List<Actor> clickedOn = this.grid.find(x, y);
    System.out.println("click: (" + x + "," + y + "): " + clickedOn);
}

In both HungryTurtleMain and FishRescueMain we build a game by mostly customizing what happens in the buttons method.

click-to-add Fruit

In HungryTurtleMain, we add a bunch of Fruit to the universe, and the turtle seeks it out to eat it.

We have an @Override for buttons and getHeaderText already, so let's add one for click.

// in HungryTurtleMain.java
@Override
public void click(int x, int y) {
    System.out.println("Nice click.");
}

If you add that definition to your HungryTurtleMain class and run it, instead of getting the list of Actors you've clicked on (and the coordinate), now you'll just see the message 'Nice click.' printed to the terminal.

To actually add a fruit to the game, we'll need a few more lines of code:

// in HungryTurtleMain.java
@Override
public void click(int x, int y) {
    Fruit f = new Fruit();
    f.setPosition(x, y); // move to click position
    this.fruit.add(f); // add to game (so we can collect it!)
    this.grid.insert(f); // add to grid (so it shows up!)
}

If you've modified the Fruit constructor already, you'll have to be a bit more clever.

What does @Override do?

@Override is Java-speak for "this method is the same as one from my parent classes or interfaces" ... which means that Java can double-check to see, if, in fact, it is the same shape & name as one of your parent's methods.

If we take our click-to-add Fruit method from the previous section, and delete the @Override, everything still works.

But, if we managed to spell click wrong, or ask for the wrong parameter types, or return the wrong thing...

// in HungryTurtleMain.java
public void clik(int x, int y) {
    // The code here won't run, no matter what
    System.out.println("Never see this...");
}

We'll never see this print statement, nor will we get an error, because the GridView code expects to call click, and it's fine for us to make up & add new methods, like we already did with eatFruit, or isLegalMove... and so clik just looks like another new method, that we happen to never call.

If we toss an override annotation on that method...

// in HungryTurtleMain.java
@Override
public void clik(int x, int y) {
    System.out.println("Now it's an error!");
}

Java now tells us:

The method clik(int, int) of type HungryTurtleMain must override or implement a supertype method

So we at least know we typed it wrong and aren't bewildered as to why it's not getting called!