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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
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; } } } |