Norman's Sky - 2 years later - A joke made in 10 hours that exploded

2 years ago I made Norman's Sky, a terribly made parody of the likenamesake, a spaceship-flying procedural generated infinite universe game, which I made in 10 hours for a jam after an almost mental breakdown. I hated the game at first, but over time my opinion of it drastically changed as it became a joke on it's own. This is the story of how it all happened.

Note: The post has some technical blabber, so for those interested in 'under the hood' this is a great example of how to use the quickest solutions to make a procedurally generated game. But if you are less interested in it, freely glance over the tech bits it and jump to the 'Aftermath'

The fall

On that Sunday, April 17th, A MAZE. 2016 edition was just a day away. I was becoming more anxious about the trip coming up since, of course, I neither packed nor prepared anything for the trip. At the same time, Ludum Dare jam was happening! Just like, not-so-coincidentally, both A MAZE. is coming up next week, and Ludum Dare is going to be this weekend in 2018.

The theme of that Ludum Dare edition was 'Under the Surface'. Of course the first thing popping into everyone's mind was something to do with water, seas, oceans. I immediately wanted to march away from the guns and think about something totally different, something more subtle, some subject that I found more interesting. I quickly started thinking about syringes going 'under the surface'. My first idea was Botox injection, but then I thought liposuction could be a better, more disgusting idea for a game, just thinking about [particles of] fat streaming out of the body! Gonna be so juicy and sounds pretty fun right!?

And not only did I want to do something atypical, I wanted to do it in a way I almost never do. In 2D! With drawn sprites! Uh... So alien! But I have to do it! I need to do something different for once!

Few hours later, this is where I was at


Ahah, so bad. I felt so not at home with any kind of hand drawn sprite art. It's just not my world..

And there, I was just stuck, completely! I did not know how to make any good gameplay, the 'particles of fat' (not shown) looked terrible, I did not know how to proceed in any way. My decision making abilities were gone. Hours passed and I was losing energy just being stressed out trying different things that don't work at all.


Well, it was half past 9 PM and I did nothing.

I quit.


..But then I remembered, wait! Was LOWREZJAM still ongoing? Yes, and it was going on for 10 more hours! I felt fresh air streaming through my hair.


Immediately, I started thinking with totally new energy, as if I didn't spend past 8 hours being under stress and about to break down..

You know what? Scrap this 'I want to do something I never attempted before'-thing. I'm gonna make something that I know how to make!

It was moments later that I just thought of the name Norman's Sky, and thought how ridiculous it would be. I thought, well, it might not be that complicated. You just have like.. infinite stars and planets and a spaceship to travel around.. SIMPLE!

The Universe


I opened a new Unity Project.

Mmmmm.. The smell of a new Unity Project

I started thinking about what the fabric of my universe should be. I needed a 3D space filled with starts, then planets around stars. The initial idea was to have just randomly placed spheres, in a typical fashion:

for (int i = 0; i < 1000, i++)
Instantiate(starPrefab, Random.insideUnitSphere * 10000, Quaternion.identity);

But hey, that's no challenge! I need to make the space infinite!

I decided to split the world into regular voxels, because it would be the easiest to code of course, and I don't really need to care if the universe is going to be completely regular and predictable or not. Albeit, unlike Minecraft's voxels, these would be gigantic voxels containing one star system each. I called these voxels sectors.

Each sector would have a unique coordinate and the sector I am currently in would be considered the 'center'. Then I would create all other sectors around the center one. As the player moves from sector to sector, I would 'move' the entire universe and regenerate all sectors. This was necessary to prevent precision errors at high distances from scene origin and also to make it .. practically infinite.

The world split in sectors, with the current sector in green

To place stars inside the sector, I would pick a random point within. This, is what I suppose is called 'perturbed grid'... but well, in 3D.. So I'll call it a 'perturbed lattice', if I may.

sector.starPostion = new Vector3(Random.value, Random.value, Random.value) * sectorSeparation;

where the sectorSeparation is the distance between the sectors, or lets say, the voxel size.

Now, you may be thinking, if the star position can be anywhere within the sector, that means the star system with planets may be on the edge and 'leak' into the other sectors, heck, two systems could even collide!

Exactly! Why not? And if you encounter a sector like that, it would be just cool, right? I could've limited the range, or used Random.insideUnitSphere, but seriously, I just don't need to..

But of course, the main problem is how to make this position value persistent, meaning that every time you query a coordinate at 1,1,1 it will give you the same star location. Well, the solution is simple, just fix a seed before you generate anything within that voxel! And the seed we are going to use, is going to be the coordinate itself!

Random.seed = x + y + z;
sector.starPostion = new Vector3(Random.value, Random.value, Random.value) * sectorSeparation;

This now insures that every call to Random.value (or any other Random. method) will always output the same values as long as the sequence is the same.

But of course, x + y + z is (intentionally) a bad example, because that would mean that the 1,0,0 will have the same seed as 0,1,0. I had to be 'smarter' than that and I simply jumbled it up by writing it as a string and then getting a hash value out of it.

Random.seed = ("ad" + x + "_" + y + "_" + z).GetHashCode();

This now makes sure that we don't have similar values next to each other... Hopefully. Actually, from a bit of testing, string.GetHashCode() can sometimes give similar values from shorter strings. It depends on the method implementation, but there are much better, more random hash solutions than Unity's built in one. And of course, I could've used another solution to generate a seed from coordinate (if you know any, please do tell), as said, this was simply was the quickest one I knew at the time.

Now I don't remember anymore what 'ad' was supposed to mean in that string, and it's most probably something I randomly typed. You can always add some gibberish to improve the randomness. I wonder what was my mind's seed of that random value!

Stars inside the sectors, scrolling the X value offsets the entire universe. It's hard to see, but if you focus hard on the top row you can spot the stars are indeed 'moving' side to side.. (or just continue to the next gif)

I make this adress-seed solution sound like it was a problem I had to solve, but actually, I already knew it. I used exactly the same method in Library of Blabber to generate pages of text. There, I used the address of the page as a seed, which is page-book-shelf-wall-chamberX-chamberY (with a few random strings added to improve randomness). I have since reused the same technique numerous times and is one of the most useful and quickest ways of getting random but persistent things from addresses. And as you can see with the library, the address doesn't need to be an integer coordinate, you could even use a Vector3 position (tho note that floats are floaty).

The Guts of the Sector

When I got this version working, now I could generate everything within this voxel, with the same sequence of random values. I use the first position for the star, then decide on a normal to represent the axis-normal of the orbit, then, for each planet I can place them on random distances and positions on orbits. Also, I assign them colors. Here is the code snipped of the generator:

s.starPostion = new Vector3(Random.value, Random.value, Random.value) * sectorSeparation;
s.starSize = (value - 0.5f) * 2;
s.starColor = starColorGradient.Evaluate(s.starSize);
s.orbitNormal = Random.insideUnitSphere.normalized;

int planetsNum = Random.Range(0, maxPlanets);
s.planetOrbits = new float[planetsNum];
s.planetPositions = new Vector3[planetsNum];
s.planetColors = new Color[planetsNum];
s.planetRadii = new float[planetsNum];

float orbitRadius = minPlanetRange;

for (int i = 0; i < planetsNum; i++)
{
    orbitRadius += Random.Range(
    minPlanetSeparation,
    minPlanetSeparation * (Mathf.Pow(i + 1, nextPlanetPower)));
    s.planetOrbits[i] = orbitRadius;

    Vector3 pos = RandomPointOnPlane(s.orbitNormal, orbitRadius);

    s.planetPositions[i] = s.starPostion + pos;
    s.planetRadii[i] = Random.Range(minPlanetRadius, maxPlanetRadius);
    s.planetColors[i] = planetColorGradient.Evaluate(Random.value);
}

Look at me, using arrays for every planet property.. *FACEPALM* Ha-ha, I'm embarrased now. At the time it was the.. quickest solution! (I can always use this excuse right? But I'm pretty sure it wasn't) But hey kids, you should not be doing this, you would want to make a class for the planet, and then have a nice readable logical array of planets!

You can spot the line minPlanetSeparation * (Mathf.Pow(i + 1, nextPlanetPower), and the reason why I use power for the planet orbit radius here is to make sure there will be more planets near the star, which is similar to how it is in real life, the farther you go the distances become exponentially larger.

As you can see, for colors of stars and planets, I am evaluating gradients. This is a very handy way to author colors, especially if you need to blend between them. Additionally, if you want to make a certain color more probable, just increase the size of it in the gradient.

Star systems with planets across 4 sectors, and color gradients
But it was not fun to have the entire universe populated with airless moons. I needed to have some atmosphere too! I simply used a bool at the beginning with a random value to decide on the having vs not having the atmosphere.

s.hasAtmosphere = Random.value < 0.5f;

The 'atmosphere' ended up being just a sphere mesh, the same as planet, but with a transparent shader. When it was lit by the light from the sun, it was glowing on the bright side and it was in shadow on the back side, which was exactly the way you want atmospheres to look.


For the graphical effect of the 'sky' when getting deeper into the atmosphere with your ship, I simply increased the fog density depending on the altitude! It was that simple and it was effective. The only bad looking thing was that in case you were looking at the horizon and entering the atmosphere-'sphere', there would be a sharp transition as you clip through that sphere mesh. But hey, it's 64x64 pixels so who cares!?

In the next small update I also added a fresnel shader on the atmosphere to make the surface a bit more visible from space, this is how it ended up looking:

The Void

I didn't want to have all sectors filled with star systems. Initially I accomplished this similarily to the planet atmosphere, just by skipping the creation process if Random.value is below 0.3, but it was not as satisfying as these pockets of void would just be uniformly dispersed everywhere.

I thought, since I have nice clean sector coordinates, I could also 'filter' them using a noise function. This would add some holes that are bigger and more organic. I found a very simple simplex noise generator (which source I don't know and so I'm just sharing it with you), and used the coordinate as the input to the generator.

The seed of the generator, of course, had to be..

simplex = new SimplexNoiseGenerator("42");

Then, I plugged it in the generator, and now it has some nice voids

...
float value = simplex.coherentNoise(x, y, z, octaves, multiplier, amplitude, lacunarity, persistence);
value += 0.5f;

Random.seed = ("ad" + x + "_" + y + "_" + z).GetHashCode();

if (value < systemProbability) // has no solar system
    return;

s.starPostion = new Vector3(Random.value, Random.value, Random.value) * sectorSeparation;
...

The stars and planets are now generated in a much more organic and nice way.

Much easier to see the universe now with voids shifting than the first gif

The Flight Model

Again, the reason why I chose having a realistic gravity model with 6DoF is simply because it was the easiest one to implement. First you need to make the ship be controlled with translation and rotation, meaning, just add forces and torques.

The second is external forces which is that celestial bodies apply gravity force to the ship. Get the normal to the planet and make the force lower by square with the distance. In the 10 hour version, all bodies had the same mass, later I calculated it from sphere volume. In reality of course, masses of planets depend on what they are made of - densities, but it was not of essence in Norman's Sky

public void ApplyGravityForce(Rigidbody rb)
{
    Vector3 direction = transform.position - rb.position;
    float rSqr = direction.sqrMagnitude;

    if (rSqr < maxRangeSqr)
    {
        force = gravityMult * bodyMass / rSqr;
        rb.AddForce(force * direction.normalized * Time.deltaTime, ForceMode.Acceleration);
    }
}

I've added a maxRangeSqr to just simply not apply force from ALL the visible bodies. I don't remember if I actually tested if this was necessary or not, but I just automatically prematurely optimized there...

As a consequence of using realistic gravity calculation allows the player to actually make stable elliptical orbits around the bodies. It is a BIT hard tho, because you have no instruments to help you with that other then the terrible 64x64 visual reference. The only way I managed to do it is to keep looking at a body I am trying to orbit and notice the then push left-right in the direction of the orbital velocity to adjust the speed and attain a (relatively) circular orbit.

The Spacecraft

The spacecraft was a 3d model I quickly made, by just extruding along a simple box mesh's edges. TADA! It was enough.

Overly juicy render, courtesy of Microsoft MX Viewer
The first version of it actually had a pillar in the middle, and it was really annoying, so  I removed it a day later. You can still see the 'mk1' cockpit in the cover gif of the game on itch.io page, I just never bothered to change the image from the page, so now it's basically historic!

Audio

I had to have some sounds, so I just turned to bfxr and made a few samples. For the RCS jets I wanted as close to white noise as possible with a little crackle, just like a real rocket engine, while the main engine wobbly sound was one of the 'randomize' things with a few tweaks to make it as long as possible... I looped the sounds by cutting them 'on waves' in Audacity and they looped pretty well. I spent in total 15 minutes on that.

It's definitely the thing about the game I hate the most, because EVERYONE HATES IT, and I had really no time to improve it, and a lot of people complained about it. Over time, it kind of aged like a fine wi... I mean... More like... aged like The Room. It was so bad that I didn't even wish to ever change.

The Aftermath


So, that was it, the game was done! And..

I published it!

..And went to sleep.

Over night, the tweet blew up! It was my first tweet to get so many likes so quickly.

I was in such a chaos because I was preparing to travel to A MAZE. and it hit me hard! I realized there were a few small bugs and quickly got to fixing them while packing my bags, including removing the center pillar in the cockpit.

Suddenly, simply because of the name, the game became a sort of a precursor to No Man's Sky. RPS did a wonderful article on it, and basically people said of it as 'if you can't wait for THE REAL No Man's Sky so badly, here is the game to soothe your urges'.

Youtube videos started popping out of people playing the game and reading the comments on those videos made me giggle really hard.


Some players sent me messages they enjoyed the game so much they played it literally for hours. Which.. Made me feel sorry for them, because.. there's literally nothing else but seeing green planets with yellow skies..

But actually, I felt really bad about it. It was kind of enjoyable to see it explode, but bitterly enjoyable because it was the WORST game I ever made. It's so bad, it has bugs, I didn't even test it really, there's nothing to do, the sound is just horrendous. I don't want to live my life being remembered by this shit. It was just a small jam game, not intended to be remembered.

The game just kept growing on downloads and hype while I was drinking and hanging out on A MAZE. Afterwards, it coasted for a while, until..

No Man's Sky Comes Out

Well, we knew how that ended up, right?

Unsurprisingly, people started coming back to Norman's Sky.. Comparing it the two games saying..
No Man's Sky: Red planet, green sky.. $59.99
Norman's Sky: Green planet, yellow sky.. Free

As you can see, it was a renaissance for Norman's Sky. Suddenly Norman turned out to be a meta joke. Since I kept saying that "I made it in 10 hours", it was the primary thing that was noted when comparing to a $59.99 'big brother', and how it had 'the same amount of content'.

It was the time I finally found the game in a better light. I still continued complaining how bad Norman was but now it became amusing to me. My opinion changed from
Ah, it's so baaaad, pls don't play it
to
Haha, it's soooo baaad, pls play it!

A Personal Opinion of No Man's Sky

I just said "we all know how that happened". Well... I actually don't personally, because I never actually played No Man's Sky! First, it was just too expensive, then, when it came out I was in Stugan, working on House of Flowers, so I haven't had time to really play games. People told me what it was like, and I watched some gameplay videos. By the time I was back home the game already received a significant backlash. I was just immediately put off by the grinding mechanic, and I saw you can't really fly ships so freely. I'm a fan of flight simulators, so I was hoping for some nice-feeling flight mechanics, at least.

I don't blame the game, I think it's really gorgeous and what they accomplished was fantastic. Where they failed is overhyping and overpromising.

The unfortunate consequence of the game, which indirectly affected my gamedev life, is they kind of made everyone know the buzzword 'procedrual generation' and made people extremely excited about it. And.. after it came out, extremely skeptic about it. I met a few people that just frowned upon hearing the words 'procedural generation', simply because they knew it automatically means dull and boring. I think the word samey was invented at that point in history. But this topic deserves a discussion of it's own, so I'll just stop here.

2 Years Later

For a long time I felt that the game was just really bad and what if it ends up being the only 'good' game I ever made? I will be remembered by the crappiest game ever, that was actually a 'parody'!

But 2 years later, the entire experience was really cool and funny. I still feel a bit like an impostor because the main reason why the game became so popular is because of the name. If it was named anything else it would be completely forgotten and covered by no one.

Meeting people around the world and mentioning Norman's Sky often instils a reaction "OMG, I know it! You made it?? Cooool!", after which, I need to triple check they actually don't mean.. No MAN's Sky.. And none of them so far did, they DID actually know my crappy ripoff!

The game is still being downloaded daily, and is still actually my most popular game.

But this is more of a consequence of my lack of output these days.

I guess the thing I love the most about it is that it has become a sort of a meta joke in itself as the time went by, although I absolutely did not intend it to be. It still gives me the giggles to read comments and hear people praise it :)

And I still haven't played No Man's Sky.

And... Good luck everyone for Ludum Dare 41!

Comments

  1. You should make sequel - Norman seems pretty decent guy and his story deserves to be told :)

    ReplyDelete

Post a Comment