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 four entities.

    • At least one 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 in the Sheep class called canEnter(). This will manage both checking for valid terrain and see if there is already an entity in a tile.

public boolean 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 cute, but he has actually has murdered hundreds of his friends due to shoddy code. Oops.

Bug Fix - Removing From The Old Tile

Mr. M forgot one thing in his explanation for this section...

  • Sometimes if you run your program long enough, you'll notice the sheep get stuck

  • This is because when we move a sheep, we only did half of the job: The sheep is being assigned to the new tile, but we have to clear it from the old tile. The program actually has invisible, unmoving sheep all across the land! Weird.

  • To do so, we must do two things:

    • In class tile, write a simple method to clear the a tile so that it has no entity in it

public void clearEntity()

{

entity = null;

}

    • Then, we'll add this line four times in the Sheep's move() method each time it would take a step

if(canEnter(destination))

{

tile.clearEntity();

destination.setEntity(this);

}

PART FIVE - FOOD

Hungry Like The Sheep

  • In the Sheep class, we'll add a variable to keep track of how much food the sheep has. Start it at a small number, like 5, just to test out that it works when the sheep run out. Once you know it works, find an appropriate value for your simulation.

  • Next we'll write a hunger() method. You should call this method() in the same way you call move(). Make sure it's only once per frame!

  • Either the sheep eats a food, or it dies of starvation! Remember that if nothing references a Sheep anymore, it is removed from memory.

public void hunger()

{

if(food > 0)

{

food--;

}

else

{

tile.clearEntity();

}

}

Eat Grass

  • In order to make the Sheep eat grass, we first need to allow us to count the grass and remove it.

  • Let's write a few methods in the Grass class:

public float getGrass()

{

return curGrass;

}

public void removeGrass(int amount)
{
curGrass -= amount;
}

  • We'll want to add a constant to sheep to represent how many units of grass it eats per frame. Let's call this EAT_VALUE and set it to 2.

  • Next we write a method called eat(). As usual, call this once per frame after move() and hunger().

public void eat()

{

if(tile.getTerrain() instanceof Grass)

{

// Since we know the Terrain is a grass, we'll cast it to a grass

// This lets us use the handy new grass methods

Grass grassTile = (Grass)(tile.getTerrain());

if(grassTile.getGrass() > EAT_VALUE)

{

grassTile.removeGrass(EAT_VALUE);

food += EAT_VALUE;

}

}

}

Look for small trails in the grass. It may be subtle at first!

Reproduction

  • When a sheep eats enough grass, it splits into two. That's how reproduction works, kids.

  • As usual, we're adding another method to sheep. Make sure to call it after move(), hunger(), and eat()

public void reproduce()

{

int x = tile.getX();

int y = tile.getY();

if(food > 100)

{
if(x > 0)

{

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

babyTile.setEntity(new Sheep());

food -= 50;

}

}

}

  • First, test out this code. Next, you'll want to expand and fix this method. Currently sheep always spawn to their left. It would be better if it randomly placed the baby in an adjcent tile. You can do the same strategy you used in move(). Generate a random value, check the appropriate direction, and if it is available drop the baby there.

  • Finally, you should TWEAK THE VALUES. Consider: grass growth speed, sheep starting food, reproduction threshold and costs, eating value, hunger amount, etc. You want to have a nice boom and bust cycle without the sheep going extinct.

The boom and bust cycle of Sheep populations

PART SIX - WOLF

  • At this point you should be pretty familiar with Worldcraft and the program's syntax. Instead of providing step-by-step code, this section will offer advice on the things you should do. Make sure you understood all the previous steps; your ability to complete the last two sections will be a good assessment of whether you just copied code or really understood it.

  • Step by Step

    • You'll need to write an accessor in class Tile called getEntity()

    • Copy the Sheep class to make a Wolf class.

    • Start by making it a different color and adding some sheep to the world. For now it's okay if it works just like a different color sheep.

    • To do so, you'll add a constant called NUM_WOLVES and expand the code in generateEntities(). It will follow the same pattern as the Sheep did.

    • Now let's change the Wolf class up!

      • You can remove the old eat() method because Wolves do not eat grass

      • They do move and reproduce similarly to Sheep. Cool.

      • When a Wolf would try to move onto a tile with a sheep, it should eat the sheep instead of not moving onto that tile

        • Before checking if a tile is clear....

        • If the destination has an entity and that entity is a Sheep...

          • For the first part, check for null. This isn't optional!

          • For the second part, use instanceof to check the class of the Entity

        • Clear the entity from the destination

        • Add a large amount of food

      • You may also want to have the wolf lose food more slowly or have a larger pool of food. It may have to go a long time between finding sheep. They don't grow on the ground...

PART SEVEN - MORE ENTITIES

Your program should have a total of four different entites. They must meet the following criteria:

  • At least one entity must interact with the terrain (like Sheep)

  • At least one entity must interact with another entity (like Wolf)

  • The remaining two entities must either interact with the terrain, an entity, or both

Some ideas for your own entities:

  • Consider entities that exist in areas outside of grasslands, like fish in the oceans.

  • Sheep now grow wool. Humans harvest wool from sheep (but do not kill them) and hunt wolves for food. Once they have wool + food, they reproduce.

  • Code SmartSheep or SmartWolf. These classes actually look for nearby patches of grass or prey to eat. Each improved class will count as a seperate entity provided you give them a distinct color/appearance and put them in the world. If there are only a few SmartSheep, will they become the dominant species of sheep over time?

  • Fire can start from near a volcano or random lightning strikes. It moves through cells like a creature, destroying grass + other entities. It transforms a Grassland tile into Ash. Over time, the Ash tile fades and eventually turns back into Grass.

EXTRA CREDIT (+10%)

Extra credit can be awarded for a ton of different enhancements. This is a more open-ended challenge. Feel free to come up with your own.

Some suggestions:

  • Especially complex or unique entity behavior, such as the "Fire" example from Part 7

  • Weather system

  • Biomes based on layers of datas. For example, tiles generate based on layered noise maps of elevation vs. temperature vs. precipitation.

  • The program draws real-time graphs to show population data for each entity over time

EXAMPLE: RUNNING PROGRAMS