Procedural generation is a means for generating data on the fly rather than manually, it gives us the power to create seemingly infinite worlds. You've likely seen procedural generation at play in games such as Diablo where every dungeon you enter is different to the last with monsters and items all computed using procedural generation. No Mans Sky is another brilliant example of procedural generation, every planet, creature, ship is created procedurally, the game is quite literally, endless with approximately 18 quintillion planets to explore.
Creating an infinite universe
In this blog we're going to explore procedural generation and create ourselves a little universe to explore:
Let's code
In my example I'll be using the p5js Editor so I can provide a link to the code if you get stuck at any point. This implementation by no means requires the p5js framework, as I'll be making very little use of its APIs, but just for ease, I'll go with it. If you want to learn more about p5js, check out my introductory blog to p5js.
The following starter code will give us our empty universe:
function setup() {
createCanvas(1280, 720);
}
function draw() {
background(0);
}
Pseudorandomness
Pseudorandomness is the backbone of the universe we're going to create, it essentially allows us to provide some value called a seed which will always return the same random number provided the seed is the same. So given the random value we can use that to generate our universe, or at least, section of our universe. In JavaScript there is no way to provide a seed to the random() function so we're going to have to import a library for this, let's add the following to our index.html
<script src="//cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js">
</script>
So when you provide a seed to:
let rng = new Math.seedrandom(`i am a seed`);
console.log(rng()) // 0.9143626543607534
It will always provide you with the same value, and any subsequent calls will produce a new random value such:
let rng = new Math.seedrandom(`i am a seed`);
console.log(rng()) // 0.9143626543607534
console.log(rng()) // 0.24035517260087458
console.log(rng()) // 0.8950846823124523
We can leverage this to determine certain properties about our universe. The universe comprises of many galaxies, let's create a galaxy class.
Galaxy
Add the following to index.html
<script src="Galaxy.js"></script>
And let's create a new file called Galaxy.js.
class Galaxy {
constructor(x, y) {
this.rng = new Math.seedrandom(`${x} ${y}`);
this.numberOfPlanets = Math.floor(this.rng() * 8);
}
}
Notice how the constructor of the Galaxy class takes an x and y value, this is our seed. And also notice how I've used the generator to determine how many planets are in our galaxy, so in this scenario our galaxy can have a maximum of 7 planets -- small galaxy, I know π should probably have called this SolarSystem.
Let's create the galaxy object passing in the x and y value of our position in the universe in our sketch.js.
let x = 0; // our starting position
let y = 0;
let galaxy;
function setup() {
createCanvas(1280, 720);
galaxy = new Galaxy(x, y);
}
function draw() {
background(0);
}
Creating some planets
Let's create some planets, we will use rng() to generate our seeded random values for the properties of our planets. I've set a this.planets property and added two new methods createPlanets() and draw().
class Galaxy {
constructor(x, y) {
this.rng = new Math.seedrandom(`${x} ${y}`);
this.numberOfPlanets = Math.floor(this.rng() * 8); // max 8 planets
this.planets = this.createPlanets();
}
createPlanets() {
let planets = [];
for (let i = 0; i < this.numberOfPlanets; i++) {
let x = this.rng() * width; // anywhere within the width of the screen
let y = this.rng() * height; // anywhere within the height of the screen
let r = this.rng() * 300; // some arbitrary radius
planets.push({x,y,r});
}
return planets;
}
draw() {
for (let planet of this.planets) {
ellipse(planet.x, planet.y, planet.r, planet.r);
}
}
}
Let's add the draw call in our sketch.js
let x = 0; // our starting position
let y = 0;
let galaxy;
function setup() {
createCanvas(1280, 720);
galaxy = new Galaxy(x, y);
}
function draw() {
background(0);
galaxy.draw(); // add this
}
And there we have our first galaxy
Let's add some code to allow us to navigate around our galaxy, so in this setup we're just going move using the arrow keys, so pressing right will shift you to the galaxy to the right, up will shift you to the galaxy above, etc.
let x = 0; // our starting position
let y = 0;
let galaxy;
function setup() {
createCanvas(1280, 720);
setupGalaxy();
}
function draw() {
background(0);
galaxy.draw();
}
function keyPressed() {
if (keyCode == UP_ARROW) {
y += height;
} else if (keyCode == DOWN_ARROW) {
y -= height;
} else if (keyCode == LEFT_ARROW) {
x -= width;
} else if (keyCode == RIGHT_ARROW) {
x += width;
}
setupGalaxy();
}
function setupGalaxy() {
galaxy = new Galaxy(x, y);
}
So now when you click the right arrow key, you should see the next galaxy:
And when you press the left arrow key, you should see our first galaxy:
Time to make things pretty
Let's add some assets to make this thing really look like a universe:
You can grab the assets from the p5js sketch
Load the images and set an array called assets that we're going to pass into the Galaxy object when we new it up!
let assets = [];
function preload() {
for (let i = 1; i <= 20; i++) {
assets.push(loadImage(`assets/${i}.png`))
}
console.log(assets);
}
...
function setupGalaxy() {
galaxy = new Galaxy(x, y, assets); // add assets as a constructor argument
}
In the Galaxy class let's set the assets to a property and then let's introduce a new type variable for when we create a planet object, this will determine which type of planet - which asset - to choose:
class Galaxy {
constructor(x, y, assets) {
this.assets = assets;
this.rng = new Math.seedrandom(`${x} ${y}`);
this.numberOfPlanets = Math.floor(this.rng() * 8); // max 8 planets
this.planets = this.createPlanets();
}
createPlanets() {
let planets = [];
for (let i = 0; i < this.numberOfPlanets; i++) {
let x = this.rng() * width; // anywhere within the width of the screen
let y = this.rng() * height; // anywhere within the height of the screen
let r = this.rng() * 300; // some arbitrary radius
let type = Math.floor(this.rng() * 20);
console.log(type);
planets.push({x,y,r,type});
}
return planets;
}
draw() {
for (let planet of this.planets) {
image(this.assets[planet.type], planet.x, planet.y, planet.r, planet.r);
}
}
}
woohoo! Now we have a pretty procedurally generated universe!
Conclusion
I hope you've enjoyed this introduction to procedural generation, I've certainly enjoyed learning about it. I hope this has given you the motivation to explore a little deeper and realise the potential of what procedural generation can do. In just our universe there's so much more we can do, here's a few ideas, I'd love to see what you come up with!
- Make the universe navigable with a spaceship, so rather than moving a galaxy at a time make them seemingly merge into one. You can leverage ideas from here to do that!
- Introduce more exciting planets, maybe even stars!
- Give the planets randomly seeded names and make them clickable
- Add movement, maybe even gravitational forces
Thank you, if you like my rambling check out my personal blogging site at https://codeheir.com/
Top comments (0)