Worldcraft
Create a program that simulates a world

Click here to download the starting code for Worldcraft

  • You'll need to unzip this project and import it into Eclipse.

  • If you get an error about the LWJGL, look at the bottom of the setup instructions for how to fix it.

REQUIREMENTS

  • You will generate a program that generates a world consisting of at least five types of terrain and three entities.

    • At least two of your terrain types must change as your program runs (ex: grass growing, fires burn forests)

    • All entities must interact with the terrain, other entities, or both (ex: eating grass, eating sheep)

    • All entities must be able to reproduce when they have enough food

  • Your program must use the provided noise algorithm to make the environment look natural

  • Your program must use inheritance to keep classes organized

PART ONE - CORE

Make sure you read the Coder's Handbook - Perlin Noise before attempting this section

For this section, we're going to work with the Core package.


  • Main

    • The main engine of your program. You won't need to modify this.

  • Game

    • The simulation's only game state. You won't modify this for now. You'll notice most of the work is being done by the World class.

  • Tile

    • Your world is built of a number of tiles.

    • Each tile consists of a Terrain (ex: grass, water) and an Entity (ex: wolf, sheep)

    • All tiles have a terrain, but they won't always have an entity.

    • Each time it updates, it tells its terrain and entities to update themselves

    • Each time it renders, it tells its terrain and entity to render themselves.

  • World

    • This is a big class that organizes your tiles in a 2D Array

    • Take your time to read and understand this code. Follow through it like a computer.

    • When the constructor is called, it generates the world

      • It starts by generating the terrain

        • First, run the program with "Basic" generation.

          • Try changing the chance of grass vs. water. How does the result change?

        • Then, change the program to "Noise" generation.

          • To do so, comment out generateTileBasic and uncomment generateTileNoise

          • This uses a method called Perlin Noise.

          • How is this mode different from the previous version?

          • What happens when you increase the SCALE?

          • What happens when you decrease the SCALE?

          • What happens when you change the 0.6 to a different value?

          • Find a set of values you like. You can always change them later.

      • For now, don't worry about entities

    • Each frame it tells each tile to update and render itself

Basic Tile Generation randomly decides if a tile is grass or water.

Using perlin noise we can make it have a natural pattern.

PART TWO - GROWTH

Make sure you read the Coder's Handbook - Inheritance before attempting this section

For this section, we're going to work with the Terrain package. Make sure you have read the Coder's Handbook Entry on Inheritance first.

Terrain

  • The Terrain class has two subclasses: Grass and Water.

  • You may notice that it has the keyword "abstract" in it, and a few abstract methods. We'll come back this once we've learned about abstract classes. For now, just know that Terrain defines the basic attributes shared by all terrain objects.

  • Importantly, it has a reference back to the Tile that contains this Terrain object.

    • This lets the Terrain know its own x and y position through the Tile's accessor methods

    • This is a protected variable, meaning it can be used by subclasses

Grass and Water

  • These subclasses are really simple right now!

    • Update doesn't do anything

    • Render simply draws a colored square at the correct x and y coordinate, scaling it by TILE SIZE.

Making the Grass Grow

Adding Data

  • In the Grass class, we'll add two constants and three variables:


final private float HIGHEST_GRASS = 100; // How many units of grass the grassiest tile can have

final private float HIGHEST_GROWTH_RATE = .05f; // How fast the grassiest grass grows

private float curGrass; // How many units of grass THIS tile has

private float maxGrass; // How many units of grass THIS tile can have at most

private float growthRate; // How fast THIS tile grows grass


Updating the Constructor

  • At this point, we have a choice. How do we determine how much grass grows on a tile? How fast it grows? Are they all the same? Is it random?

  • To make this look really organic, we're going to use the Perlin Noise information to help set how grassy a tile is. At a higher noise value, we'll say this tile is very grassy. At a lower one, it's mostly dirt. So the next step is to modify the constructor to take the noise as an input. We'll give it a better name - fertility. We know this input will be between 0 and 1, so we can use it to scale other values.

  • We'll use that input to set the maximum grass and growth rate. Then we'll randomly assign the starting grass . We can change all of these values later on, but this should make it feel random but still organic.

public Grass(float fertility)

{

maxGrass = (float) (HIGHEST_GRASS * fertility);

growthRate = (float) (HIGHEST_GROWTH_RATE * fertility);

curGrass = (float) (maxGrass * Math.random());

}

  • We will need to go back and update our code in World. Make two changes:

    • You can delete generateTileBasic() from your program. We won't need that anymore!

    • In generateTileNoise(), you'll want to put noise into a variable on its own line, then add it as a parameter like this:

public void generateTileNoise(int x, int y)

{

float SCALE = .01f;

float noise = (float) PerlinNoise.noise(x * SCALE, y * SCALE);

if(noise < .6)

{

tiles[x][y].setTerrain(new Grass(noise));

}

else

{

tiles[x][y].setTerrain(new Water());

}

}


Percent Grass

  • Soon we're going to want to change the color based on how grassy a tile is. A good way to represent this is to figure out how this tile's grass compares to the highest possible amount of grass. Let's add a handy accessor.

public float percentGrass()

{

return curGrass / HIGHEST_GRASS;

}


Update and Render

  • In the update() method, we'll increase curGrass by the growthRate as long as it hasn't exceeded maxGrass

public void update()

{

if(curGrass < maxGrass)

{
curGrass += growthRate;

}

}


  • In the render method, we'll set the colors based on the percentage of grass on the tile.

public void render(Graphics g)

{

int red = (int) (50 * (1 - percentGrass()));

int green = (int) (150 * percentGrass()) + 50;

int blue = 0;

g.setColor(new Color(red, green, blue));

g.fillRect(tile.getX() * World.TILE_SIZE, tile.getY() * World.TILE_SIZE, World.TILE_SIZE, World.TILE_SIZE);

}


Running it

When this runs, you'll see a somewhat scattered mix of dirt and grass. Over time, it will shift toward more green colors. But some areas will always be less green, since they are less fertile regions. In a few steps, we'll use this grass as food for our sheep - and they'll change our world's terrain!

PART THREE - MORE TERRAIN

For this section, we're going to work with the Terrain package.

Mountains

The Mountain Class


  • Copy the Water class and call it Mountain.

  • Add a constructor to Mountain so that it takes a float called elevation as a parameter.

    • Don't forget to store elevation as a class level variable.

  • In render, set a color based on elevation. For example:

int r = (int) (elevation * 225);

int gr = (int) (elevation * 150);

int b = (int) (elevation * 100);

g.setColor(new Color(r, gr, 50));


World Generation

  • Let's go back to generateTileNoise() and reorganize our code and add in Mountains. We'll have a low value be water (low elevation), a medium value be grass (medium elevation) and a high value be mountains (high elevation).

if(noise < .45)

{

tiles[x][y].setTerrain(new Water());

}

else if(noise < .67)

{

tiles[x][y].setTerrain(new Grass(noise));

}

else

{

tiles[x][y].setTerrain(new Mountain(noise));

}

Other Terrain Types

  • You need to add at least three more terrain types to your program for a total of six.

  • Some ideas:

    • Mountain Peak - Highest elevation, snow capped

    • Deep Water - Lowest elevation, dark blue

    • Beach - Between water and grass, yellow. Make it a very narrow range.

    • Forest - Between grass and mountains, dark green

Cleaning Things Up

  • At this point I took some time to mess around with my scale value and tile size. Find a level that makes things look good for you.

  • I also recommend making your curGrass start at MaxGrass. It won't look like it's growing now, but during our next step - adding in Sheep - we'll have something to deplete it.

Optional: Fancy Math Tricks

More Dramatic Gradiant

  • One problem we run into is that our mountains look pretty flat! That's because there really isn't that much variation in the actual elevation. For example, a mountain at 0.6 isn't much different from one at 0.65.

  • You can use some math to make the differences more dramatic.

    • Consider using Math.pow() to raise that to an exponent, then multiply by a larger scalar.

    • This will create a bigger variance within the category

Adding Imperfections

  • Do things look too perfect and smooth?

  • You can always add a tiny bit of pure randomness in your constructor.

  • For example, my mountain adds a very small factor of random on top of the elevation:

public Mountain(float elevation)

{

this.elevation = (float) (elevation + Math.random() * .01f);

}

  • You can apply this to other terrain. For instance, your sand might have more randomness to look rougher.

Making Gradiant More Dramatic

Adding Imperfections

PART FOUR - SHEEP

Make sure you read the Coder's Handbook - Abstract Classes before attempting this section

Accessing The World

Static Methods and the Singleton Pattern

  • The way the project is set up right now, we have a small problem: the Sheep can't see anything beyond their current tile!

  • To solve this we will make a static accessor method that returns a Tile

    • Reminder: static methods are part of the class itself, rather than the object.

    • The only reason it's okay to do this here is because we know there is only one World.

    • This is using a strategy called the Singleton Pattern.

    • Be careful about making things static unless you have a good reason to do so.

Aladdin shows Jasmine the benefits of writing accessor methods with the Singleton Pattern (1992)

Modifying The World Class

  • First we need to make the tile array static. This means it's owned by the class, rather than an object of the class.

private static Tile[][] tiles;

  • Next we can add an accessor method that allows you get a tile at a given coordinate. We're still keeping the array private!

public static Tile getTile(int x, int y)

{

return tiles[x][y];

}

  • Finally, we'll make the methods getTileHorizontal() and getTileVertical() static. You'll need these later in this section!

public static int getTilesHorizontal()

public static int getTilesVertical()

Movement

Moving West

  • Let's visit the Sheep class in the package Entity

  • The update() method is already being called each frame based on the code in Tile's update() method.

  • We write a method called move() then call it in update()

public void update()

{

move();

}

  • Let's make our sheep move randomly!

    • Every Entity knows what Tile it lives on. We can use this to find our x and y positions.

    • We'll generate a random number and use that to determine which way we move

    • We only can move in a direction if it is a valid position on the map (watch out for ArrayIndexOutOfBounds!)

    • We can get our new tile using the getTile() method from the World class.

      • Notice how we're accessing World in a static way. We never make a World object here!

      • Tip: When you access a method in a static way using Eclipse, the text is shown in italics.

public void move()

{

int x = tile.getX();

int y = tile.getY();

double r = Math.random();


// Move West

if(r < .25 && x > 0)

{

World.getTile(x-1, y).setEntity(this);

}

}

      • The setEntity method is defined in Tile and makes the relationship go both ways.

        • The Tile has its Entity set to this Sheep

        • The Sheep has its Tile set to the new location.

Framerate and Concurrency

Controlling the Simulation's Speed

  • You may be running into one of two problems:

    • Your program is running at a smooth 60 FPS, and the sheep go wizzing by like crazy

    • Your program is lagging a bunch. Running at 8 FPS is no way to live.

  • Let's modify the World class to only update once per "tick" - an arbitrary time frame we define

    • Add a constant called TICK_FREQUENCY and set it to a value of your choice (try 5)

    • Add a variable called time. Start it at zero, and increase it every frame in update.

    • Only loop through the tiles and update them if you're on a multiple of TICK_FREQUENCY

public void update()

{

time++;

if(time % TICK_FREQUENCY == 0)

{

for(int i = 0; i < getTilesHorizontal(); i++)

{

for(int j = 0; j < getTilesVertical(); j++)

{

tiles[i][j].update();

}

}

}

}

    • If your program is still slowly, you may need to reduce your map size or number of sheep.

    • This demonstrates the importance of splitting our update() and render() methods.

      • We can always change the speed at which the simulation runs

      • However, this doesn't make us have a clunky, unresponsive user interface

Renegade Sheep and Concurrency

  • We have a problem that will only become occur once you add more directions: sometimes your sheep will move multiple times per tick. Uh oh. Let's solve it before it happens.

  • Imagine this: We're moving through the rows and columns like a typewriter: left to right, then top to bottom. If a sheep moves down a row, they get an "extra turn" because they activate again as we loop through the tiles.

  • There are two main ways to solve this problem. The first is that we could store our planned actions, then execute them in a second phase. But we'll opt for the second option: keeping track of timestamps of updates, and only allowing an update if the time stamp does not match the current frame.

  • First, we'll need to go to the World class. We want to make time static, and write an accessor to get it.

public static int getTime()

{

return time;
}

  • Next, we'll visit the Entity class. We will add a protected variable called lastUpdateTime.

protected int lastUpdateTime = 0;

  • Finally, in Sheep's update method we'll check that the lastUpdateTime isn't the current time. After we update, we'll change lastUpdateTime.

public void update()

{

if(lastUpdateTime != World.getTime())

{

move();

}

lastUpdateTime = World.getTime();

}


There are an entire set of problems in Computer Science called Finite State Automata that deal with simulating self-directed cells in 2D arrays.

The most famous of these, Conway's Game of Life, encounters a similar problem to this one!

Other Directions

  • So far we our sheep have a 25% chance of going west, and a 75% chance of staying put

  • Expand your program so that sheep can move north, east, and south as well.

  • Experiment and decide how often you want your sheep to move.

    • Do they always move?

    • If not, how often should they stay in place?

Checking Terrain Types

Using Instanceof To Check Terrain Types

  • Right now our sheep are spawning on all kinds of crazy terrain: beaches, mountains, and the sea.

  • Sheep are simple creatures: they should only be able to spawn and move to Grass tiles.

  • Let's start by writing a method in the Sheep class called isValidTerrain()

public boolean isValidTerrain(Terrain t)

{

if(t instanceof Grass)

{

return true;

}

else

{

return false;

}

}

  • This uses a new keyword called instanceof. This checks if an object belongs to a specified class.



Alex Lee provides a short explanaton of how to use the instanceof keyword.

Getting Terrain From A Tile

  • Right now we don't have a way to get Terrain from the Tile class - only set it.

  • Write an accessor method in the Tile class that returns the tile's terrain.

    • This method should be called getTerrain()

    • We'll use it in the next section

Only Moving To Valid Terrain

  • We can rework the movement code to only allow movement if the destination is valid.

  • Notice in the code below that I've rewritten some lines to avoid writing World.getTile(x - 1, y) twice.

    • Lines like that are very prone to errors when duplicated!

    • Sometimes it's a good idea to make a local variable to avoid errors and increase readability.

  • Revise the move() method as follows. Start with one direction, but eventually apply it to all of them:

double r = Math.random();

int x = tile.getX();

int y = tile.getY();

if(r < .25 && x > 0)

{

Tile destination = World.getTile(x-1, y);

if(isValidTerrain(destination.getTerrain()))

{

destination.setEntity(this);

}

}
Run your program! You'll notice Sheep get "stuck" on invalid Terrain now.

Only Spawning On Valid Terrain

  • Let's visit the World class

  • Look at the addEntityRandomly() method, which is used to spawn Sheep.

  • We'll modify a single line of code as follows.... but you're going to get an error:

if(tiles[rX][rY].hasEntity() || !e.isValidTerrain(tiles[rX][rY].getTerrain()))

  • Eclipse is unhappy because we defined this method for Sheep, but not for Entity. This method has to work for all entities!

  • We can solve this in one of two ways, and this problem leads to an important concept using inheritance.

Option #1 - Default Behavior

  • Create a method with a "default behavior" in the Entity class, then override it in the Sheep class.

    • Let's simply say that a generic Entity is at home in any Terrain unless we specify otherwise.

    • So add isValidTerrain() to Entity and now the Sheep version is overriding this new method.

public boolean isValidTerrain(Terrain t)

{

return true;

}

Tip: In Eclipse, any method that overrides something from a superclass is marked with an upward green arrow.

Option #2 - Abstract Methods

  • Make a promise that all subclasses of Entity will define this method in their own way.

    • Since Entity is an abstract class, we can simply write an abstract method.

    • You'll notice this way the solution for update() and render() already.

    • So we just add isValidTerrain() to Entity and now the Sheep version is overriding this new method.

abstract public boolean isValidTerrain(Terrain t);

With left-only movement, you can see Sheep getting stuck on mountains once this is added.

Stop Sheep Stacking

Obliterated By A Wooly Friend

  • There's one final problem with our code: sheep can walk on top of each other.

  • When a sheep takes another sheep's tile, it simple replaces it. The old sheep is gone forever.

  • We need to make sure our sheep check that there isn't another Entity in a tile if we want to walk into it.

  • Let's make a new method called canEnter(). This will manage both checking for valid terrain and see if there is already an entity in a tile.

public void canEnter(Tile t)

{

return !t.hasEntity() && isValidTerrain(t.getTerrain())

}

  • Finally, we can go back and fix up the move method using canEnter.

  • Replace the four places you used to check for terrain with...

if(canEnter(destination))

{

destination.setEntity(this);

}

This sheep may look cure, but he has actually has murdered hundreds of his friends due to shoddy code. Oops.

PART FIVE - FOOD

Sheep Eating, Hunger, and Reproduction

PART SIX - WOLF

PART SEVEN - MORE ENTITIES

EXTRA CREDIT (+10%)

EXAMPLE: RUNNING PROGRAMS