In Planet Jumpers' third phase of gameplay, you fly (or drift) through an endless universe. The goal, to get further than anyone else has ever been from the starting point. As of writing, that records belongs to a gas planet roughly 18 million miles away named… (checks notes)… uh… that’s not that important right now… More about the leaderboard in another post. This post is to explain how the universe generation works!
Here’s a zoomed out view of a new round with freshly generated galaxies! That tiny planet in the middle? That’s the starting planet that everyone starts on each time.
So how does the game build this? Well let’s start at the smallest piece, a single planet! Every planet is generated with four primary attributes.
- Gravity
- Radius
- Biome
- Atmosphere Toxicity
These different attributes give the planet a different look in space!
As you can see in the image above, these properties allow planets to have a wide variety of appearances! The player can even actively avoid toxic planets by paying attention to the slight color changes.
All of these properties are generated at random, with position being calculated during galaxy creation. Here’s the code for planet creation.
func generatePlanet():
return {
radius = rand_range(16,64),
gravity = rand_range(20, 80),
biome = PlanetBiome.values()[randi()%PlanetBiome.values().size()],
atmosphereToxicity = rand_range(1,MAX_ATMO_TOXIC)
}
So what is galaxy creation, and how does it work? Well, here is a simple galaxy, freshly generated.
Every galaxy has a star in the center of it. When a galaxy is created, it picks a random number of planets between 1 and 10. The first planet is placed 150 units away from the center, but the direction is picked at random. After it places this first random planet, it then picks a random number between 2 and 5, and multiplies it by the first planet’s radius. This is then added to the first planet’s distance to get the distance for the next planet. The direction in which the planet is placed, is again generated at random.
This is then repeated for each planet that is placed. The planets are limited to be at most 500 units away from center, causing planets to sometimes stack up at the edge (Not intentional on my part, something that I wish I had more time to tune!). Below is a diagram explaining the placements.
Here is the code that generates a galaxy.
func generateGalaxy(x,y,planets = 10, maxPlanetDistance = 500):
var sun = sunScene.instance()
sun.global_position = Vector2(x,y)
sun.planetsOrbiting = planets
$Planets.add_child(sun)
var lastPlanetDistance = 150
for i in range(planets):
var randomDegree = randi()%360
var randomRad = deg2rad(randomDegree)
var planetVector = Vector2(cos(randomRad), sin(randomRad))
var newPlanet = newPlanetNode(x,y)
var planetDistance = lastPlanetDistance + rand_range(2*newPlanet.planetRadius, 5*newPlanet.planetRadius)
if planetDistance > maxPlanetDistance:
planetDistance = maxPlanetDistance
newPlanet.global_position += planetVector.normalized()*planetDistance
$Planets.add_child(newPlanet)
lastPlanetDistance = planetDistance
So now that we see how planets are placed within galaxies, how do galaxies get placed? Well galaxies use a very similar approach. When you first launch, the game generates what I’ve called a “space area”. This area consists of x number of rings of galaxies. Due to performance limitations, the game currently only generates 1 ring of galaxies at a time. These rings are generated with a center on the current player position. Every ring is 1000 units apart. Each ring has a specific number of galaxies within it, the calculation for this is below.
var galaxyCount = 4 + floor(2 * distance / ringDistance)
Since I originally wanted multiple rings to generate at a time, the calculation takes into account the current ring’s distance from the player. Since there is only ever 1 ring at a time, the current version of the game always generates 8 galaxies in one ring.
Once the galaxy counts are calculated, it picks a random degree to start with (0-360). It then takes 360, divides it by the number of galaxies, and that becomes the degree of difference between each galaxy in the ring. This makes it so that the ring has evenly spaced galaxies placed around it.
Here is the code for generating an area of space.
func generateSpace(centralPosition):
var ringsToGenerate = 1
var ringDistance = 1000
var maxGenerationDistance = ringDistance*ringsToGenerate
var distance = ringDistance
while distance <= maxGenerationDistance:
var angleDeg = randi()%360
var galaxyCount = 4 + floor(2 * distance / ringDistance)
for i in range(galaxyCount):
angleDeg += 360/galaxyCount*i
var angleRad = deg2rad(angleDeg)
var position = centralPosition + (Vector2(cos(angleRad), sin(angleRad)) * distance)
generateGalaxy(position.x, position.y, randi()%10+1)
distance += ringDistance
Below is a diagram showing roughly how galaxies are placed around a ring.
The final question is, how does the game handle when you go outside of the galaxy ring? The answer to that is somewhat disappointing I’m afraid. Due to time constraints, I wasn’t able spend time figuring out a way to map areas that have previously had planets generated. I also didn’t want to risk the player getting lag from too many planets. So my solution was to calculate a “play area”.
A “play area” is simply a bounding box, calculated by looking at the furthest planets in each direction.
Here is a quick illustration of what a “play area” would look like.
When the player steps outside of this box (plus some margin), all planets that are currently in play are destroyed and a brand new galaxy ring is generated around the player. This is unfortunately crude, and causes both stutter during the game and noticeable flashes of planets at times. However, it does ultimately give the feeling of a never ending universe.
To sum things up, the random generation is relatively simple! Planets are placed at random distances on a random angle from the center of galaxies, and galaxies are placed along a radius from the player’s position. With this we can get what feels like random planet placement in all directions. Generating a game each time really does feel unique, and despite a few hiccups and shortcuts I am happy with the result!