Create a program that simulates a world
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
For this section, we're going to work with the Core package.
The main engine of your program. You won't need to modify this.
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.
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.
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
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.
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
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.
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:
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.
Update and Render
In the update() method, we'll increase curGrass by the growthRate as long as it hasn't exceeded maxGrass
In the render method, we'll set the colors based on the percentage of grass on the tile.
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.
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:
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).
Other Terrain Types
You need to add at least three more terrain types to your program for a total of six.
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
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:
You can apply this to other terrain. For instance, your sand might have more randomness to look rougher.
Making Gradiant More Dramatic
PART FOUR - SHEEP
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.
Next we can add an accessor method that allows you get a tile at a given coordinate. We're still keeping the array private!
Finally, we'll make the methods getTileHorizontal() and getTileVertical() static. You'll need these later in this section!
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()
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.
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
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.
Next, we'll visit the Entity class. We will add a protected variable called lastUpdateTime.
Finally, in Sheep's update method we'll check that the lastUpdateTime isn't the current time. After we update, we'll change lastUpdateTime.
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!
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()
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:
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.
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.
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.
Finally, we can go back and fix up the move method using canEnter.
Replace the four places you used to check for terrain with...
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
Then, we'll add this line four times in the Sheep's move() method each time it would take a step
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.
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:
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().
Look for small trails in the grass. It may be subtle at first!
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()
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.
Especially complex or unique entity behavior, such as the "Fire" example from Part 7
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