You can probably tell I don’t update this anymore. I might in the future. I just have so much else going on. Anyway, you can keep up with my endeavors over at JDH Creates now!
It’s been ages since I’ve done anything here. I’ve been busy with other things. That’s no excuse, though, is it?
As of this post, I’ve migrated this site to the same server where I host everything else of mine, which will let me save a little money. Going to install a project management plugin and start recording progress here. Evaluating LOVE as a new engine to migrate my existing work to and move forward, as I’ve reached the limits of my frustration with Pygame.
I’m not promising a whole lot right now. But I plan to take a step back from my other blog in a couple months in order to focus more on this. I may bring on some more social engagement features and things like that. Honestly, I don’t care too much about those, as long as people are able to find this blog easily enough.
In any case, look forward to some more news over the next few months.
It’s been a long time since my last update. Rest assured, I’m not dead, I’ve just been busy with that other game we all love known as “real life.” That said, it’s not that I’ve gotten nothing done on SagaSim. I’ve done very little programming but I’ve been brainstorming other aspects. In particular, I’ve developed the species system in more detail, specifically dealing with traits. In the previous entry, I laid out how the DNA system works. Now, it’s time to define just what all the various DNA-defined traits mean in terms of simulation behavior.
I have the following list of traits so far:
plant animal generalist specialist spore-bearing seed-bearing invertebrate vertebrate fabric producer endothermic ectothermic large-bodied small-bodied aquatic terrestrial scales feathers fur skin exoskeleton woody wings herbivore carnivore algae fern shrub tree burrower ground-dweller tree-dweller deep sea dweller reef dweller conifer fruit-bearing egg-laying live birther predator scavenger nocturnal diurnal baleenic domesticable farmable omnivore flying flightless migratory non-migratory parasitic venomous hive solitary colony pack flocking schooling large brain pollinating grasping appendages opposable digits bipedal sapient
I will no doubt add more and tweak the ones I have. Currently, there is some inconsistency between ones used as nouns and others used as verbs. I will likely adjust those once I decide how best to code them. In any case, I’ve come up with several game elements as a result of these. The following concepts are now part of the species simulation:
- Mobility — Animals can move, plants can’t. This literally just affects whether a species can move from one location to another as part of normal simulation activity. Immobile species can only “move” by reproducing into an adjacent tile.
- Sustenance — Plants need sunlight to live. Animals must eat to live.
- Threat — A value indicating how severe a threat this species poses to others. Think of this like an “attack” statistic from role-playing games.
- Vulnerability — Sort of the inverse of Threat: how easily this species can be harmed by others. This is akin to a “defense” statistic from role-playing games.
- Range — Specialists can live only in the biome type from which they originate. Generalists can live anywhere (so long as their other trait-implied needs are met.)
- Size — Determined by various traits. Size values are exponential: a size of 2 is ten times size 1, size 3 is ten times size 2, etc. This is useful when determining which animals can eat which, as generally larger animals eat smaller ones.
- Resources — Some species produce resources usable by civilizations, such as fabric (think silk.) I will probably come up with more of these, e.g. leather, fur, etc.
- Migration — Some species migrate, which means they can relocate from one tile to another based on availability of food resources. Now, any animal species will migrate in an effort to find food, if food becomes genuinely scarce where they are, but a migratory species will migrate either randomly or just if there is a more optimal source/quantity of food nearby.
- Herding — Animal species can be solitary, or they can operate in packs, or colonies/hives. Animals that operate in herds receive a vulnerability bonus based on their local population. Herding behavior also determines the maximum population for this species on a given tile.
There are some other attributes I’m still roughing out. The next step will be to create the basic species simulation system that simulates populations on a given tile. That one is going to require quite a bit of programming and I might write a blog post about it next, as I work out the particulars.
Most of the brainstorming I’ve done over the past several months was related to the civilization system, so admittedly I’ve neglected the species system in that time, hence the rather sparse update this time around. I’m hoping to get another update done (with actual development progress!) in the next week or two.
We still aren’t simulating anything yet, but we’re getting there, slowly but surely.
When I started this project, I knew simulating life forms would be an essential part of it–not just the intelligent ones that would give rise to civilizations, but the plants and animals that form the critical ecosystems and food chains to support those civilizations. I got suck for quite a while on exactly how to accomplish it. Early on, I decided I would want to use some kind of DNA-like system, in which a string of data represented everything SagaSim needed to know about the species. It took me a long time to figure out how best to implement this, but ultimately I ended up devising a rather simple system for it.
I decided to use a string of bits–1s and 0s–to define my species’ DNA. Then I needed to figure out how many to use. I ended up going with 24, since it provides over 16 million possibilities but is not so many bits as to be unmanageable. Then, I had to determine the best way to describe the bits themselves. I created what is, for lack of a better term, a “DNA specification.” Through the use of a rather simple text file, I can create all the DNA bits and describe what specific patterns mean. The file format is rather simple:
[bit number],[bit type] [0 value,1 value] OR [plain text description, if bit patterns are defined] [bit pattern],[implied trait] [bit pattern],[implied trait] ...
So, the first line is the bit number (literally, the position of that bit in the DNA string), followed by a comma, then the “bit type.” There are two bit types:
- Generalization — A bit that will assign a particular class/type to this species.
- Association — A bit that will assign a particular attribute/property to this species.
The second line can be formatted one of two ways: it can specify the meaning of the 0 value and the 1 value, separated by a comma; or, it can be a plain text description of this bit’s purpose, if we’re going to use bit masks. After the second line, there can be any number of lines of bit masks. Each bit mask is a sequence of 0s, 1s, and question marks, followed by a comma, then the generalization/association implied by that pattern. Any 1s and 0s in the mask must match exactly in order to apply, question marks indicate that that bit isn’t important for the purposes of the given trait.
Examples are helpful, so here are a few:
3,generalization spore/seed,vertebrate/invertebrate 0?0,spore-bearing 0?1,seed-bearing 1?0,invertebrate 1?1,vertebrate ???,[none]
4,generalization thornless/thorny,endothermic/ectothermic,small/large 0??1,thorny 1?11,endothermic 1?10,ectothermic 1?00,small bodied 1?01,large bodied ????,[none]
Pretty simple, right? They go all the way up to bit 24:
24,generalization non-sapient/sapient 101?1???1?0????????11111,sapient ????????????????????????,[none]
You’ll notice that at the end of each list of patterns is an all-wildcard pattern followed by [none]. This is just our “catch-all” pattern to indicate that, if no other matching pattern is found, this bit serves no purpose for this species.
One thing I like about my implementation of the DNA system is that the specification is kept in a text file, and then imported into game at runtime, so it is inherently customizable and easy to update. I considered using XML or some other established, structured format, but I opted against adding such complexity and overhead. I may implement an intermediate XML format for SagaSim’s data files at some point in the future, though, for compatibility with other tools as well as general ease-of-use.
Once a species has been created, all of its traits are kept as part of a species object. This is really just a placeholder until the full species object is created, which will contain all the actions and attributes endowed to that species. What it can do at this point is spit out a description of itself. Here is one for a randomly-generated species:
Species sagasimus testus Generalizations: animal generalist vertebrate endothermic aquatic carnivore egg-laying scavenger diurnal domesticable omnivore non-migratory venomous schooling Associations: fur
The DNA string for it is:
And while I admit this is a feature that’s probably not worth much except the amusement factor, I also wrote a mechanism to render a given DNA string as a graphic. Here is the (greatly enlarged) sprite of the DNA string above:
The algorithm for this is actually really simple. I create an 8×8 image (64 pixels), use the 24 bit DNA string to generate the color (8 bits for red, 8 for green, 8 for blue), then start on the second row and draw four pixels per line, starting from the left. After four pixels are drawn, I go to the next line and keep going, until I have rendered all 24 bits/pixels. I also “mirror” the pixels on the right side of the image, so the sprite is symmetrical. This is very, very simplistic (even silly) but it provides a unique sprite for every species in SagaSim. I plan to use them, or more likely a modified version of them, in the actual simulation.
Just for fun, here’s a 600×600 randomized set of species:
I still have a couple of issues left to iron out, such as resource contention over the DNA database, but what I have so far seems adequate enough to proceed.
It may be a while until the next update, as I need to write the actual class/type system to go along with the DNA system. The good news is, this means I will actually have some simulation code in place! I will most likely break the coding of the species simulation into multiple blog entries, as I expect it to be somewhat complicated and I’d like to talk about each ability and attribute of the species.
This may not be a super exciting entry, as I will be carving up SagaSim into a bunch of pieces to determine what I need to tackle individually. This list will probably change over time, and once I settle on a good project management system, I’ll turn all of them into tasks/projects in it. Gotta have something to motivate and organize here. With my blather out of the way, on to the actual breakdown.
I envision SagaSim as being made up of multiple high-level modules, and then submodules. There are two overarching components to the entire project: the world generation, from the terrain all the way down to individual characters; and the simulation, which controls the interactions of everything that’s been generated.
- World Generator
- Noise Generator
- Noise Normalizer
- Terrain Generator
- Climate Generator
- River Generator
- Biome Generator
- Resource Generator
- Species Generator
- DNA Model
- Generalization Classes
- Species Metaprogram
- Species Placement Module
- Civilization Generator
- Technology Model
- Sociology Model
- Religion Model
- Economy Model
- Policy Model
- Civilization Placement Module
- Character Generator
- Personality Generator
- Trait Model
- Family Model
- Simulation Engine
- Species Simulator
- Civilization Simulator
- Sociology Simulator
- Attitude Simulator
- Economy Simulator
- Resource Simulator
- Labor Simulator
- Commerce Simulator
- Political Simulator
- War Simulator
- Policy Simulator
- Religion Simulator
- Technology Simulator
- Character Simulator
- Interpersonal Simulator
- Family Simulator
- Sociology Simulator
- World Simulator
- Disaster Simulator
- Disease Simulator
Water is obviously an important chemical for any world to sustain life as we know it. It’s why so much of SagaSim’s world generation revolves around the distribution of water. Up to this point, we have oceans and inland lakes, but we’ve not discussed rivers, nor assignment of biomes. Both of these are influenced by climate, and biomes are also influenced by rivers. I will discuss how each is generated.
Let’s start by generating another new world, because that’s just so much fun. (You’ve probably gotten the impression by now that the world generation process is fairly disposable and I can make new ones quickly and easily. You’d be right!) Let’s start with the terrain and climate maps, since those will determine the behavior of the river generator.
After the river generation process is run, the river map is stored as a single image, where blue pixels signify the presence of a river and black pixels signify… well, nothing. They aren’t important to the river map. When overlaid the terrain and climate maps, you can see the rivers pretty easily and the ways they flow should make logical sense:
You can see how they flow down from high ground to low ground, either working toward the coast or toward the lowest point they can reach. Sometimes, multiple rivers will meet at the same low point and join together. Sometimes, a river may flow around a low level long enough to create a lake. So, how are they actually generated?
It begins by selecting a pixel–any pixel–from the climate map. Each climate is either considered rainy or not. A river can only begin on a rainy climate, so the randomly-chosen pixel is checked, and if it falls on a rainy climate, then it is considered a valid starting point for our river. Then, the river generation process begins. The pixels surrounding our chosen starting point are examined. If any of the surrounding pixels are lower than the current one, they are added to the list of possible successor pixels. Out of that list, the generator always picks the lowest one available. If two or more pixels are tied for the lowest, one is chosen at random. Then, the process is run again from the next pixel. This is done a set number of times, based on the following formula:
river length < (width * height * 0.005)
Given our existing map dimensions (600×465), each river will run for up to 1395 pixels. That may sound like a lot, but bear in mind that once a river reaches the ocean, it has nowhere else to go, and once it reaches its lowest point, it will simply pool around that. Rivers can never climb uphill. The length limiter is admittedly arbitrary. One could generate much longer rivers by making that number larger. I’ve found that 0.001 makes them too short, but 0.005 produces lengthy yet not overwhelming rivers.
This process is run to generate a set number of rivers. I use 100 as a default, but it’s a user-defined option so you can have as many or as few as you like.
Intro to Biomatics
Biomes are easily the most complex part of the world generation, because they bring together all the other elements we’ve created so far: terrain, climate, and rivers. The available biome types are:
- Montane Grass/Shrubland
- Temperate Coniferous Forest
- Temperate Broadleaf Forest
- Tropical Coniferous Forest
- Mixed Forest
- Coastal Forest
- Tropical Broadleaf Forest
- Temperate Grassland/Savanna/Shrubland
- Tropical Grassland/Savanna/Shrubland
- Flooded Grassland/Savanna
Each biome type is associated with one or more climate types and one or more terrain types. For a given land pixel, the possible biome types are determined based on the terrain, climate, and whether any adjacent pixels are river or ocean pixels. In particular, riparian biomes must be adjacent to/on a river, and coastal forest biomes must be adjacent to an ocean. Once we have the list of possible biomes for this pixel, the logic is then very much like climate logic: surrounding pixels are examined for their biomes, and the biome generator will either pick one of those or (less likely) pick a random one from the possible biomes for this pixel. This produces a “banding” effect similar to the climate generator. Like the climate generator, the biome generator currently doesn’t wrap seamlessly at the left and right edges of the map. Unlike the climate generator, we only make a single pass to generate the biomes, rather than several. These issues will likely be corrected/modified at some point in the future, but they are sufficient for now.
The final biome map, with the rivers overlaid, looks like this:
The color key for the map is as follows:
- Montane Grassland/Shrubland
- Temperate Coniferous Forest
- Temperate Broadleaf Forest
- Tropical Coniferous Forest
- Mixed Forest
- Coastal Forest
- Tropical Broadleaf Forest
- Temperate Grassland/Savanna/Shrubland
- Tropical Grassland/Savanna/Shrubland
- Flooded Grassland/Savanna
These classifications are all based on the World Wildlife Federation’s terrestrial biome system. No sense reinventing the wheel here, right?
Believe it or not, at this point we’ve taken care of the basics of world generation. We have a planet with a terrain map, we’ve laid out its climate, waterways, and biomes, and now we’re ready to start populating it with forms of life: plants and animals! Those will be discussed next time.
Note: In the course of working on this entry, I realized there was a bug in the river generator, which I’ve since fixed, though the images were generated with the buggy version. Let them serve as a lesson that there are always improvements to be made. Future blog entries will use the corrected river generation algorithm.
With terrain generation done, along with the segregation of land and water, we can move on to more complex matters. Here’s where things become more subjective and creative. What terrain types are necessary? How do we handle climate?
I decided to start with basic terrain types contingent on elevation. At the lowest level is water, which isn’t really a “terrain,” but it’s there for the sake of making the terrain map look sensible. Who wants to look at a black ocean, seriously? Yes, it really is that simple a decision.
The various terrain types are based strictly on elevation. Further nuances are defined later by climate and biomes. The elevation scale is in shades of gray, from 0 to 255, with 0 as water. The rest are:
- Wetlands (shades 1-5)
- Floodplains (6-25)
- Plains (26-75)
- Foothills (76-125)
- Highlands (126-170)
- Mountains (171-255)
I’ll admit those numbers are rather arbitrary, but they result in a reasonable terrain distribution. Here’s an example:
As you can see, the whole gamut of terrains is covered.
The terrain definitions, like many features of SagaSim, are parameterized and customizable. It will therefore be possible for people to tweak the terrain definitions and do fairly wacky things, if they’re so inclined.
Next up is climate! In SagaSim, climate is a function of two things: elevation and latitude. Rather than completely reinvent the wheel here, I decided to make use of the Köppen climate classification system. SagaSim’s version is rather simplified, though I’ve left the door open to make it more realistic in the future. For now, the idea is to just have SagaSim generate climate zones for the world map so the later generation steps can be carried out.
The climate system is also parameterized. I’ve currently defined 14 different climates, as based on the Köppen system:
Each climate has four parameters: its minimum base latitude, its maximum base latitude, whether it’s considered a rainy climate (a simple true/false value), and the color to use for it on the map (same colors as those in the Wikipedia article.) I say the latitudes are “base” latitudes because the climate generation algorithm doesn’t have to precisely respect them. Instead, there is a fair amount of variation allowed.
Climate generation is done by making multiple passes over the map. On the first pass, there is no existing climate information, so the algorithm starts from scratch. On each land pixel, it looks at the 8 surrounding pixels to see what climate times are immediately adjacent. It also determines which climates are possible at the current pixel, based on latitude. Then, a probability is calculated to determine whether to choose an adjacent climate type or a new one. It is heavily weighted toward picking an adjacent type. The formula is:
1 - (1 / (height * width * 0.25)
This will normally generate a very small number, roughly 1% on most maps. Then a random decimal is chosen. If it falls below the probability threshold, then we will pick a new climate from the available types, rather than an adjacent one.
This is done for every land pixel on the map, starting at the top left and working our way toward the bottom right. Next, we do a few more passes, doing a very similar calculation: for a given pixel, look at the surrounding pixels and choose one at random to assign to this pixel if we meet the probability threshold (about 5% in subsequent passes.)
At the end of the process, we have something like this:
The “banding” effect is a consequence of the latitude restrictions and the probability checks. This allows for substantial climate variation without having climate zones go too far out of their assigned latitudinal ranges.
One shortcoming of the current climate generator is that, unlike the perlin noise generator, it does not produce a seamless map. If you were to wrap the above image around a sphere, it would be discontinuous at the left and right edges of the map–the climate zones wouldn’t line up. I intend to fix that, though I’ve not yet gotten around to it.
It might seem like we haven’t done much so far, but it’s all a means to an end. Next time, I’ll get into how rivers and biomes are generated. They’re more complicated than (and dependent on) terrain and climate, so they may get split into two articles.
Thus far, I’ve explained the basic premise of SagaSim and described how to generate a basic landscape through fractal noise. Now, it’s time to put that noise to work and create land masses!
One of the input parameters to the world generator is “water percentage.” Maybe you want a world that’s like Earth, where roughly 70% of the surface is covered by water. Maybe you want much more land, so you go down to 50%. Maybe you want no ocean at all, so you put in 0%. There is no point in putting in 100%, though, since it means no biomes or climate data would be generated (sorry.) Anyway, let’s start with 70%. I’m generating a larger map this time, so you can really see the features of the landscape.
Where does the land end and the water begin? You can’t tell from this map. Instead, I have an algorithm that “normalizes” the map. What it does is go through each pixel, put it in an array (a list), and then it slices the array at whatever point you entered as your water percentage. So, if you chose 70% and had an image that’s 10,000 pixels in total (100×100), the value of the pixel at index 7000 would represent our new “base” color. All pixels below that will be turned to black, then all the pixels above index 7000 get adjusted. Since the sea level value will likely be some shade of gray, and we’re turning it into black, we therefore need to tweak all the lighter-colored pixels so that they now start just one shade above black. In computer graphics parlance, this is known as “stretching the histogram.” Let’s see what the map looks like after this processing has been done:
Now, the locations of land masses are much clearer. But the world generator takes one more step to make clear the distinction between ocean and land: it creates a “mask” where all land is white, and all water is black. That way, it’s never ambiguous as to whether a given pixel is meant to be land or ocean. (Rivers are another story, and will be covered in a future post.)
Much clearer, isn’t it? You can also see that there are five distinct continents, as well as some smaller islands. Here are a few more water masks to illustrate the variety of landforms you can get just by tweaking some of the world generation parameters:
As you can see, there’s plenty of variety to be had by messing with the world generation parameters.
So, now we’ve got our land and oceans set up. Next time, we move on to terrain and climate!
One of my goals for this blog is to have almost everything written in such a way that non-programmers can understand it. If I make especially technical posts, I will probably put them under their own category and not have them be essential reading in order to comprehend the project’s components or the project as a whole.
With that said, the basic starting point of procedurally generating a world via a computer program is create the world map, starting with terrain. To do this, it makes sense to use some type of random noise. Noise is not hard to find. The static you hear on the radio is noise. The “snow” you saw on analog TV sets was noise. In computer graphics, noise is very easy to generate. For example, here’s a sample of noise created with one of the filters in GIMP:
By itself, this type of noise is not very useful for creating things like terrains, which have great variations in elevation but nevertheless have structure, instead of appearing completely random. To get something more suitable for terrain, we must turn to fractals. Fractals offer multiple advantages:
- Large-scale structures that give definition to our generated terrain.
- Seemingly random variations that allow for easy creation of virtually endless unique terrains.
- Based on mathematical formulas, so if you “zoom in” on a fractal terrain, you can find ever finer details in the terrain.
The last point is particularly interesting since it strongly corresponds to how terrain works in real life. For instance, a difficult problem is measuring the exact length of a land mass’ coastline. Putting aside the rise and fall of tides, coastlines demonstrate fractal-like complexity as you measure them in smaller and smaller units. Famed mathematician Benoît B. Mandelbrot wrote a fascinating paper on this very subject.
Fortunately, SagaSim doesn’t need coastlines defined with molecular granularity, so I’m off the hook on that one!
While there are many ways to generate fractal noise, a very common one is the perlin algorithm, named after Ken Perlin, who created the original algorithm for the movie Tron. My algorithm is based on this version by Mr. Perlin. As an aside, in case you’re not familiar with methods for generating terrain, when presented as an image, lightness indicates elevation. Black is the lowest elevation, while white is the highest.
Without going into a lot of mathematical detail, there are a few basic inputs to a perlin algorithm: persistence, octaves, and lacunarity. Persistence indicates the “amplitude” or intensity of the noise. A high persistence means that the basic features of the noise will remain more “static” than they would if you used a lower persistence. Octaves indicate the “frequency” of the noise. Essentially, octaves work by adding the mathematical output of the noise function to itself–the same way that adding two identical sound frequencies together produces a tone that’s one octave higher. Finally, there is lacunarity, which can also be considered a “smoothness” value. This produces large-scale structures in the noise by layering zoomed-in versions of the noise on top of itself. If you didn’t understand any of that, you’re not alone. Let’s illustrate with pictures!
Here is the base case: persistence of 1.0, 1 octave, 1 lacunarity.
Pretty boring, huh? Let’s bring down the persistence a bit, to 0.3:
Notice that it has higher peaks, because the perlin function is less constrained by the persistence of the original fractal. Next, let’s add a few octaves. We can start with 4:
You can now see that there’s a lot more variation, but also that the structures are pretty much evenly distributed. No matter how much you turn up the octave count, it’s just going to make the noise fuzzier while not bringing out any large-scale features. For that, we need to tweak lacunarity! Let’s turn it up to 4:
At this point we really start to see larger structures come out, such as the deep valleys near the upper left and around the bottom, and the high peaks near the lower right. Things get more intense if we turn the lacunarity up to, say, 8:
You can see that now we have one prominent streak of higher elevations cutting from the right side down to the bottom left, and much lower elevations centered to the bottom right.
Finally, let’s go up to a lacunarity of 16. I normally wouldn’t do this because it tends to create one big “land mass,” but for illustration’s sake, here it is:
That didn’t turn out too badly, but you can likely see how there actually seems to be less variation in elevations–the lower elevations are crowded out by the higher ones, due to layering zoomed-in versions of the basic noise on top of itself so many times. In the actual SagaSim code, I find that using a lacunarity of 8 normally produces the best results.
Finally, since SagaSim is meant to be a world simulator, I had to decide how to handle “wrapping.” After all, you can circumnavigate the Earth, so why shouldn’t the intelligent life forms inhabiting a SagaSim world be able to do the same? To that end, I settled on having a “cylindrical” world map. The left and right sides of the map are analyzed, and I essentially “wrap” them onto each other, gradually fading out. This allows the map to look seamless so that land masses on either side don’t just suddenly “end” at the edge of the map. Incidentally, this also gives me maps that can be seamless wrapped around 3D spheres (something you can also do with GIMP):
By “seamless,” of course, I mean “the poles look like pinched crap.” No offense to any Poles in the audience.
Next time, I will talk about how these noise maps are turned into land masses, oceans, and such. You now probably know more about perlin noise than you ever wanted (or you’re more confused than ever.)