I’ve spoken about the “Big Bang” system I have concocted for generating my game universe, but I thought I’d go a little more in-depth this time on account of having more or less translated it from C# to Javascript to GDScript on Sunday evening.
The Big Bang (or just BB, if you please) does five things: generate the solar systems, generates planets within those solar systems, adds stations around planets, adds jump gates to all systems, and determines which solar systems are “major hubs”. It also saves it all to disk. There will be more, but this is the minimum amount of work I need to get done in order to run a test with saving and loading data, using it to populate a scene, and to build the ability to move between systems.
All Systems Go
Building solar systems is relatively simple. Using a minX, minY, maxX, and maxY I build a grid. Each system has an [x,y] coordinate and is placed into an array which is then shuffled. Looping through, I assign IDs and placeholder names using sequential integers.
# Generate solar systems.
func GenSystems():
# tempSystems holds the in-order Vector2 coordinates (0,0), (0,1), (0,2)
# Loop through maxX and maxY to add the V2s to the tempSystems array.
var id = 0
var tempSystems = []
for x in range(0, maxX):
for y in range(0, maxY):
# Create a new SolarSystemObject
var _sso = {
"id": null,
"name": null,
"x": x,
"y": y,
"is_hub": false,
"star": GenStar()
}
tempSystems.append(_sso)
tempSystems.shuffle()
for s in tempSystems:
s.id = "system_" + str(id)
s.name="System " + str(id)
id += 1
# systems is a class-scoped array
systems.append(s)
Hub Systems
I have long had an idea of “major and minor systems” in the universe. These would be random percentages of existing systems which would have proven to be very hospitable for human life (major), or just pretty good for human life (minor). As the theory goes, major systems would be guaranteed to have verdant planets, which automatically grants them a space station. They would also get the most jump gates. Minor systems would have an increased chance at a verdant planet and increased chances at space stations, and fewer jump gates than major systems, but more than everyone else. My existing code for calculating this didn’t translate well at all to GDScript because of some major shortcomings with the language that I hope will be shored up in Godot 4.0, so I have opted to feature “hub systems” only. These act as major systems, guaranteed to have a nice planet and orbital shopping center with plenty of toll booths in and out.
The real kicker is that I did in only 4 lines what I did in Javascript in about 12, so at least I have that going for me.
# Select a random X of items from systems and mark them as hub systems
func GenHubs():
systems.shuffle()
for i in range(0, hubCount):
systems[i].is_hub = true
systems.sort_custom(self, "sort_ascending")
Intergalactic Planetary…
As of right now, planets are being created, but only in about 1/4 of their final form. Turns out placing planets is a tricky business.
I’m not claiming that my universe is scientifically accurate, but I did do some research into stars and solar system composition and found a few statistics about “habitable zones” in relation to advanced stellar classes. Eventually I want to include this in the planet placement scheme, but right now it’s pretty dumb.
For every system in the…system…if it’s a hub then we automatically give it a “TT” world in the third orbital slot. “TT” is the type (Terrestrial) and subtype (Temperate) designating a planet in the “Goldilocks Zone”. For everyone else, I calculate a number of other planets between 0 and 8. Rolling through that range, I flip a coin: if the result is > 50 then a planet is placed in that orbit (unless we have an orbit 3 “TT” planet already, at which point we skip the general population for that slot). Other planets include Rocky, Metallic, Gas, and Ice, and most have sub-types.
I still need to figure out population as well. Population will play into production later on, so I have to suss out that entire thing to a point before I can start generating people.
# Generate planets in each system
# Based ON the systems array, we loop through. If a system is a HUB,
# then we need to guarantee a TT planet within the habitable zone
# as determined by the system's star type.
# All other systems get random chances at planets.
# Certain orbits have an increased chance of habitable worlds, based
# on the star type. If a TT planet has already been assigned to an
# orbit because it's a HUB, skip it.
func GenPlanets():
var id = 0
for s in systems:
var _sysPlanets = []
var _has3 = false
if s.is_hub:
var _po = {
"id": "planet_" + str(id),
"system_id": s.id,
"name": "Planet " + str(id),
"orbit": 3, # Need to calc based on star type
"type": "T", # Change to enum
"sub_type": "T", # Change to enum
"is_habitaible": true if rng.randi_range(1,100) > 70 else false
}
_has3 = true
_sysPlanets.append(_po)
id += 1
# How many OTHER planets does this sector have?
# Between 0 and 8
var _pCnt = rng.randi_range(0,8)
# Loop through the _pCnt, adding a planet to that orbit count.
# If _has3 = true, skip adding a planet to orbit 3
if _pCnt > 0:
for _o in range(1, _pCnt):
# If this is orbit 3 and we don't have a planet at 3, or
# if this is NOT orbit 3, calculate the probability
# of having a planet.
if (_o == 3 and not _has3) or _o != 3:
# Right now, 50% chance there's a planet at this orbit
var _flip = rng.randi_range(1,100)
if _flip > 50:
var _pType
var _pSub
match(_o):
1:
_pType = "T"
_pSub = "R" # Rocky
2:
_pType = "T"
_pSub = "M" # Metallic
3, 4:
_pType = "T"
match(rng.randi_range(1,3)):
1:
_pSub = "R"
2:
_pSub = "M"
3:
_pSub = "T"
5, 6:
_pType = "G"
7, 8:
_pType = "I"
_:
pass
var _po = {
"id": "planet_" + str(id),
"system_id": s.id,
"name": "Planet " + str(id),
"orbit": _o, # Need to calc based on star type
"type": _pType, # Change to enum
"sub_type": _pSub, # Change to enum
"is_habitaible": false
}
_sysPlanets.append(_po)
id += 1
if _sysPlanets:
planets.append_array(_sysPlanets)
GenStations(_sysPlanets)
_sysPlanets.resize(0)
planets.sort_custom(self, "sort_ascending")
I apologize for the crappy formatting; as GDScript is Python-like, it relies on tabs, which doesn’t translate well to a fixed and formatted block like the one above.
On Station
Stations are the bread and butter of the game, as they are where players will dock to buy and sell goods. Right now, I just need them to exist as a concept, and later on they will get menus and all of the visual support.
“TT” planets in hub systems will get stations. For everyone else in the system, if there’s already a station at the “TT” planet, we don’t add another; if not, then as soon as we do add a station in that system, we stop adding them. I only want one station per system, if any at all.
Of note: station generation happens as part of the planet placement function. This way, I can act on placing stations within the current system, with only the planets that were generated in that system, so I don’t have to crawl the entire planet array and figure out which planets are within which system (to limit their presence-per-system to one).
# Generate stations around select planets. Major systems are
# guaranteed to have a station; all other systems are a crapshoot.
func GenStations(sysPlanets):
var id = 0
if sysPlanets.size() > 0:
var _hasStation = false
for p in sysPlanets:
if p.type == "T" and p.sub_type == "T":
_hasStation = true
var _so = {
"id": "station_" + str(id),
"name": "Station " + str(id),
"planet_id": p.id,
"system_id": p.system_id
}
stations.append(_so)
id += 1
if not _hasStation:
var _pctChance = rng.randi_range(1,100)
if _pctChance < 20:
_hasStation = true
var _so = {
"id": "station_" + str(id),
"name": "Station " + str(id),
"planet_id": p.id,
"system_id": p.system_id
}
stations.append(_so)
id += 1
Jump Gates
I hate jump gates. Scratch that: I hate that I don’t want every system to access every [x,y] around it, because that would make things a lot easier. I blame Trade Wars for having a variable number of pathways.
To generate gates, I do start by creating gates for all 3, 5, or 8 directions out of a particular grid square. A record is created and put into a temp array. I then choose a random number representing the number of gates in a system, between 2 and 6. I pick a random gate from the current system for each gate and, assuming I don’t already have a gate that’s coming in from the opposite direction, add it to the overall jump gate collection, along with the inverse. I am expecting that when checking for exits from a system, I will pull the collection of gates in that system and will need their destination ID values. Likewise, a player should expect to be able to return through a gate she just passed through to get back to the system she just left. This has proven to be a pain in the ass in GDScript; it wasn’t easy initially in Javascript either, but I got in working in both cases, though in both cases this is the part in the Bang that takes the longest (~ 1 second in GDScript).
# Connect systems. Major systems should have ??? exits; minor
# systems should have ??? exits. All other systems should have
# at least one exit.
func GenJumpgates():
var id = 0
for s in systems:
var sX = s.x
var sY = s.y
var _exits = []
if sY - 1 >= minY:
var _sys = filter_array(systems, ["x", "y"], [sX, sY-1])[0]
var _jg = {
"id": "jumpgate_" + str(id),
"sys_from": s.id,
"sys_to": _sys.id,
"toll": 0,
"fuel": 0
}
_exits.append(_jg)
id += 1
if sY - 1 >= minY and sX + 1 < maxX:
var _sys = filter_array(systems, ["x", "y"], [sX+1, sY-1])[0]
var _jg = {
"id": "jumpgate_" + str(id),
"sys_from": s.id,
"sys_to": _sys.id,
"toll": 0,
"fuel": 0
}
_exits.append(_jg)
id += 1
if sX + 1 < maxX:
var _sys = filter_array(systems, ["x", "y"], [sX+1, sY])[0]
var _jg = {
"id": "jumpgate_" + str(id),
"sys_from": s.id,
"sys_to": _sys.id,
"toll": 0,
"fuel": 0
}
_exits.append(_jg)
id += 1
if sX + 1 < maxX and sY + 1 < maxY:
var _sys = filter_array(systems, ["x", "y"], [sX+1, sY+1])[0]
var _jg = {
"id": "jumpgate_" + str(id),
"sys_from": s.id,
"sys_to": _sys.id,
"toll": 0,
"fuel": 0
}
_exits.append(_jg)
id += 1
if sY + 1 < maxY:
var _sys = filter_array(systems, ["x", "y"], [sX, sY+1])[0]
var _jg = {
"id": "jumpgate_" + str(id),
"sys_from": s.id,
"sys_to": _sys.id,
"toll": 0,
"fuel": 0
}
_exits.append(_jg)
id += 1
if sX - 1 >= minX and sY + 1 < maxY:
var _sys = filter_array(systems, ["x", "y"], [sX-1, sY+1])[0]
var _jg = {
"id": "jumpgate_" + str(id),
"sys_from": s.id,
"sys_to": _sys.id,
"toll": 0,
"fuel": 0
}
_exits.append(_jg)
id += 1
if sX - 1 >= minX:
var _sys = filter_array(systems, ["x", "y"], [sX-1, sY])[0]
var _jg = {
"id": "jumpgate_" + str(id),
"sys_from": s.id,
"sys_to": _sys.id,
"toll": 0,
"fuel": 0
}
_exits.append(_jg)
id += 1
if sX - 1 >= minX and sY- 1 >= minY:
var _sys = filter_array(systems, ["x", "y"], [sX-1, sY-1])[0]
var _jg = {
"id": "jumpgate_" + str(id),
"sys_from": s.id,
"sys_to": _sys.id,
"toll": 0,
"fuel": 0
}
_exits.append(_jg)
id += 1
# Choose between 2 and 6 exits.
var _exitCnt = rng.randi_range(2, 6 if _exits.size() >=6 else _exits.size() -1)
for x in range(0, _exitCnt):
# Pick a random index
var _rngExit = rng.randi_range(0, _exits.size() -1)
# Splice that object out
var _keep = _exits.pop_at(_rngExit)
# Check this INVERSE of this gate in jumpgates, and if we don't have it, add it
# We will also add the inverse
var _exists = filter_array(jumpgates, ["sys_from", "sys_to"], [_keep.sys_to, _keep.sys_from])
if _exists.size() == 0:
id += 1
var _rev = {
"id": "jumpgate_" + str(id),
"sys_from": _keep.sys_to,
"sys_to": _keep.sys_from,
"toll": 0,
"fuel": 0
}
jumpgates.append(_keep)
jumpgates.append(_rev)
Helper and Debug Methods
I have several methods that support these operations:
- Exit/EntryTest: This checks that all systems have at least one gate to another system, and one gate from another system that can reach it.
- GenStar: This uses a researched “probability matrix” for the 7 major star classes (O, B, A, F, G, K, and M) to assign a star to a system during system generation.
- ArrayShuffle: This was copied from Javascript, before I realized that GDScript has a shuffle() function for arrays which is completely fucking awesome.
- PercentChance. I got this function from the Internet; it uses two arrays to calculate the percent chance any item will be chosen and chooses it. It’s used for star generation, and eventually for habitable planet placement using stats I pulled off the Internet.
- SortAscending: Another provision from the Internet; this one sorts my arrays. Since I do rely on a lot of shuffle(), I use this function to re-order data before saving it, so I can eyeball it without having to search through the whole thing.
- Save/LoadJSON: These two functions do as the name implies: saves and loads JSON, which is how I am storing all of this data to disk.
- FilterArray: I am kind of proud of this one as I wrote it myself, but also kind of irritated that I had to. I use a lot of Array.map() in Javascript, and it’s one of the best functions ever invented. GDScript does not have anything like map(), so I had to write my own. It can do “all” filtering with multiple criteria, and I might update it to also do “some”, but I am hoping for a proper map() function in 4.0.
Stats
Here’s some stats from a recent test run (originally posted to Mastodon):

Using a 10×10 “grid”, I ended up with 100 systems. There are 182 planets across those systems, and 42 of them have stations in orbit. Not sure how I feel about that, as it means that 58 systems are basically just pass-through. Something will have to be present in those systems, so players don’t get bored. There are a total of 530 jump gates, which is a lot, but there is no data in either _badEntry or _badExits, so all sectors are connected both in and out.
With this done, I can generate “a universe” well enough to test with. I have these files that I will need to move over to my “movement test” project, where I’ll attempt to load them into a singleton via AutoLoad so I can see how they perform, and then work with them to get system generation and passage from system to system working.