Noise in Video Games
At some point everyone encounters all-too-irritating noise when using a television. Noise is a good visual representation of using a random number generator (a.k.a RNG) in programming. Complete random numbers is ideal in many situations but when making programs to generate random worlds having a complete RNG is not particularly practical. In this scenario “coherent noise” (i.e Perlin Noise or Simplex Noise) can be much more useful. Coherent noise can be considered “pseudorandom noise” meaning that it is not absolutely random. The reason coherent noise cannot be considered completely random is that rather than each number having absolutely no relation the the last when using coherent noise each number does have a correlation to the last. This results in a smoother, more usable noise function.
Coherent noise is very common in video games, especially among the survival genre. Games like Minecraft, Starbound, No Man’s Sky and many more use coherent noise for many things. Minecraft and Starbound use coherent noise for generating their worlds, much like the demo program above although with more complex algorithms. No Man’s Sky is special in the sense that it uses coherent noise for much more than just terrain generation, almost everything in No Man’s Sky is randomly generated including the animals, the plants, the rocks and more.
The screenshots from the demo above shows the clear benefits of using coherent noise in contrast to completely random noise in the sense of generating a fluid, more navigable world. On the programming side of things coherent noise also gives the person writing the code much more control over the generation. This control is given by many factors, such as the ability to adjust the amplitude. In this scenario amplitude would be the maximum height value allowed. You can see the code down below for the terrain generation function used in this example.
void terrainGeneration() {
float xoff = 0;
float STEP_VALUE = 0.05; //The larger this value the rougher the noise will be.
for (int x=-1; x<WORLD_WIDTH; x++) { //Loops through x-axis
noiseDetail(5, 0.4);
float noiseY = noise(xoff); //Generates y-value of tile based on noise, noise() returns value between 0-1
int layer = int(map(noiseY, 0, 1, 0, WORLD_HEIGHT)); //Maps y-noise value to more usable value
int depth = 0; //Tracks how deep into the ground the current tile is
for (int i=layer; i<int(height / TILE_SIZE) + 1; i++) {
int[] tempColours = new int[3]; //Used to store colours (to create grass and stone)
if (depth < 1) { if (random(1) > 0.94) {
addTree(x, layer - 1); //Adds trees occasionally
}
}
if (depth < 2) {
tempColours[0] = 34;
tempColours[1] = int(random(130,140)); //Grass colours
tempColours[2] = 39;
} else {
int tone = int(random(100,110)); //Stone Colours
tempColours[0] = tone;
tempColours[1] = tone;
tempColours[2] = tone;
}
tileList.add(new tile(x * TILE_SIZE, i * TILE_SIZE, tempColours)); //Creates the tile
depth++;
}
xoff += STEP_VALUE; //Increases the xoff variable by the step value, otherwise world would be flat
}
}
You can play the example project down below along with the source code or view it at:
https://www.ktbyte.com/projects/project/71871. You can move around with WASD to see what worlds made with coherent noise would look like.
[raw]
[/raw]
This particular demo contains multiple classes. There are separate classes for the tiles, the decoration tiles, and the player, a simpler way to understand classes when you’re just starting to learn about them is thinking of them as different objects. The tile and decoration tile classes are relatively simple, they contain the x and y coordinates of the tiles and just display them where they should be. The player class is relatively more complex as it contains the position of the player, but also contains the code which detects collisions between the players and tiles. The source code can be found below.
int TILE_SIZE = 16;
int WORLD_HEIGHT, WORLD_WIDTH;
float GRAVITY = 0.1;
float COLLISION_BUFFER = TILE_SIZE * 0.3;
player p;
float xOffset = 0;
ArrayList tileList;
ArrayList decoTileList;
void setup() {
size(800, 600);
frameRate(60);
noStroke();
rectMode(CENTER);
ellipseMode(CENTER);
WORLD_HEIGHT = 40;
WORLD_WIDTH = int(width * 3 / TILE_SIZE);
tileList = new ArrayList();
decoTileList = new ArrayList();
p = new player(width / 2, 0);
terrainGeneration();
}
void terrainGeneration() {
float xoff = 0;
float STEP_VALUE = 0.05; //The larger this value the rougher the noise will be.
for (int x=-1; x<WORLD_WIDTH; x++) { //Loops through x-axis
noiseDetail(5, 0.4);
float noiseY = noise(xoff); //Generates y-value of tile based on noise, noise() returns value between 0-1
int layer = int(map(noiseY, 0, 1, 0, WORLD_HEIGHT)); //Maps y-noise value to more usable value
int depth = 0; //Tracks how deep into the ground the current tile is
for (int i=layer; i<int(height / TILE_SIZE) + 1; i++) {
int[] tempColours = new int[3]; //Used to store colours (to create grass and stone)
if (depth < 1) { if (random(1) > 0.94) {
addTree(x, layer - 1); //Adds trees occasionally
}
}
if (depth < 2) { tempColours[0] = 34; tempColours[1] = int(random(130,140)); //Grass colours tempColours[2] = 39; } else { int tone = int(random(100,110)); //Stone Colours tempColours[0] = tone; tempColours[1] = tone; tempColours[2] = tone; } tileList.add(new tile(x * TILE_SIZE, i * TILE_SIZE, tempColours)); //Creates the tile depth++; } xoff += STEP_VALUE; //Increases the xoff variable by the step value, otherwise world would be flat } } void addTree(float x, float y) { int[] tempColours = new int[3]; int treeHeight = int(random(3,6)); for (float i=y;i>y-treeHeight;i--) {
tempColours[0] = 120;
tempColours[1] = 80;
tempColours[2] = 10;
decoTileList.add(new decoTile(x * TILE_SIZE, i * TILE_SIZE, tempColours));
}
for (float i=x-2;i<x+3;i++) {
tempColours[0] = 80;
tempColours[1] = 150;
tempColours[2] = 10;
decoTileList.add(new decoTile(i * TILE_SIZE, (y-treeHeight) * TILE_SIZE, tempColours));
decoTileList.add(new decoTile(i * TILE_SIZE, (y-treeHeight-1) * TILE_SIZE, tempColours));
decoTileList.add(new decoTile(i * TILE_SIZE, (y-treeHeight-2) * TILE_SIZE, tempColours));
}
}
void draw() {
background(137, 207, 240);
for (int i=0; i<tileList.size(); i++) { if (tileList.get(i).xpos + xOffset > - TILE_SIZE &&
tileList.get(i).xpos + xOffset < width + TILE_SIZE) {
tileList.get(i).draw();
}
tileList.get(i).wrapAround();
}
for (int i=0; i<decoTileList.size(); i++) { decoTileList.get(i).draw(); } p.draw(); } void keyPressed() { if (key == 'w') { p.jump(); } if (key == 'a') { p.moveLeft(); } if (key == 'd') { p.moveRight(); } } void keyReleased() { if (key == 'a' && p.xvel > 0) {
p.xvel = 0;
}
if (key == 'd' &&
p.xvel < 0) {
p.xvel = 0;
}
}
class player {
float xpos, ypos;
float xvel = 0, yvel = 0;
int speed = 2;
float top, bottom, left, right; //hitbox sides
boolean topC, bottomC, leftC, rightC; //collision sides
player(float x, float y) {
xpos = x;
ypos = y;
}
void updateHitbox() {
top = ypos - (TILE_SIZE / 2);
bottom = ypos + (TILE_SIZE / 2);
left = xpos - (TILE_SIZE / 2);
right = xpos + (TILE_SIZE / 2);
}
void updatePosition() {
xOffset += int(xvel);
if ((xvel < 0.1 && xvel > 0) || (xvel > -0.1 && xvel < 0)) { xvel = 0; } if (!bottomC) { yvel += GRAVITY; if (yvel > 2) {
yvel = 2;
}
} else {
yvel = 0;
}
ypos += yvel;
}
void collisionCheck() {
rightC = false;
leftC = false;
bottomC = false;
for (int i=0; i<tileList.size();i++) {
if (dist(xpos, ypos, tileList.get(i).xpos + xOffset, tileList.get(i).ypos) < TILE_SIZE * 2) { if (right >= tileList.get(i).left &&
right < tileList.get(i).right - COLLISION_BUFFER && bottom > tileList.get(i).top + COLLISION_BUFFER) {
rightC = true;
if (right > tileList.get(i).left) {
xOffset += right - tileList.get(i).left;
}
}
if (left <= tileList.get(i).right && left > tileList.get(i).left - COLLISION_BUFFER &&
bottom > tileList.get(i).top + COLLISION_BUFFER) {
leftC = true;
if (left < tileList.get(i).right) { xOffset += left - tileList.get(i).right; } } if (bottom >= tileList.get(i).top &&
bottom < tileList.get(i).top + COLLISION_BUFFER &&
left < tileList.get(i).right && right > tileList.get(i).left) {
bottomC = true;
if (bottom > tileList.get(i).top) {
ypos -= bottom - tileList.get(i).top;
}
}
}
}
}
void jump() {
if (bottomC) {
ypos -= 2;
yvel = - (TILE_SIZE / 8);
}
}
void moveRight() {
if (rightC == false) {
xvel = - speed;
} else {
xvel = 0;
}
}
void moveLeft() {
if (leftC == false) {
xvel = speed;
} else {
xvel = 0;
}
}
void draw() {
updateHitbox();
collisionCheck();
updatePosition();
fill(0, 0, 255);
rect(xpos, ypos, TILE_SIZE, TILE_SIZE);
}
}
class tile {
float xpos, ypos;
int[] colours = new int[3];
float top, bottom, left, right;
tile(float x, float y, int rgb[]) {
xpos = x;
ypos = y;
for (int i=0; i<colours.length; i++) {
colours[i] = rgb[i];
}
}
void updateHitbox() {
top = ypos - (TILE_SIZE / 2);
bottom = ypos + (TILE_SIZE / 2);
left = xpos - (TILE_SIZE / 2) + xOffset;
right = xpos + (TILE_SIZE / 2) + xOffset;
}
void draw() {
updateHitbox();
fill(colours[0], colours[1], colours[2]);
rect(xpos + xOffset, ypos, TILE_SIZE, TILE_SIZE);
}
void wrapAround() {
if (xpos + xOffset <= -TILE_SIZE * 2) { xpos = (WORLD_WIDTH * TILE_SIZE) - TILE_SIZE - xOffset; } if (xpos + xOffset >= WORLD_WIDTH * TILE_SIZE) {
xpos = - xOffset - TILE_SIZE;
}
}
}
class decoTile {
float xpos, ypos;
int[] colours = new int[3];
float top, bottom, left, right;
decoTile(float x, float y, int rgb[]) {
xpos = x;
ypos = y;
for (int i=0; i<colours.length; i++) {
colours[i] = rgb[i];
}
}
void draw() {
fill(colours[0], colours[1], colours[2]);
rect(xpos + xOffset, ypos, TILE_SIZE, TILE_SIZE);
if (xpos + xOffset <= -TILE_SIZE * 2) { xpos = (WORLD_WIDTH * TILE_SIZE) - TILE_SIZE - xOffset; } if (xpos + xOffset >= WORLD_WIDTH * TILE_SIZE) {
xpos = - xOffset - TILE_SIZE;
}
}
}