I've recently added a major new feature to my evolution simulation project - a dual rendering system with a navigable viewport. This update introduces two distinct ways to visualize the simulated world:
Global view
The original view remains available, showing the entire simulation world at once. This provides an overview with simplified entity representations, suited for monitoring large-scale patterns and population distributions.
Rich view
The new rich client offers a limited rectangular viewport showing only a portion of the world at any time. This scoped view enables:
Detailed organism rendering with segments, patterns, and dynamic visual traits
Amplitude-based wobble effects for environmental conditions
Cross patterns with wobble effects for resource elements
Enhanced color generation based on species traits
Most importantly, users can now navigate through the world using arrow keys, moving their viewport to explore different regions of the simulation as it evolves. This is what will become the actual game players will play.
Technical implementation
Behind the scenes, this update required significant architecture changes:
Created a dual client state system with appropriate viewport handling
Implemented separate drawable classes for minimal and rich entity rendering
Added user input handling for movement
Developed canvas coordinate transformations for viewport display
Rendered oragnisms closer to actual animals and plants - with much more to be done there
The result is a more immersive and interactive experience that allows users to explore the evolution simulation in unprecedented detail while maintaining the option for a global overview.
Next step is to make the organisms actually look interesting. The wobbly circles was mostly for me as a developer to see what was happening in the world, but it doesn't make for an exciting world to look at. At least not compared to having actual animals and plants.
Again, the organisms all have a varied genetic composition. Each organism is most likely unique, just like each human most likely is. Some organisms have a genetic composition similar enough for them to be seen as the same species, or the same genus etc.
Mapping genetics to visible traits
I mapped the genetic composition of each organism to different granular traits of animals and plants respectively. For example, high values of calcium and phosphor make the organism more likely to have bigger teeth, while high values of iron, potassium and sulphur might create a zigzag patter on a plants leaves.
With a large number of mappings to granular traits, I then render and animate the complete organism, based entirely on the genetics, resulting in a wide variety of animals and plants, all evolved organically in the simulated evolution.
Genetics affect behavior and appearence
These animals fight each other, form groups with their own, eat plants and other animals, sometimes after fighting them to death. They find suitable mate for reproduction, threaten rivals, have offspring, teach them which plants to eat and which to stay away from, and where to find them. They multiply across generations, and their species mutate over time, making way for new species.
All based on their genetics. Now it's also starting to become visible. More details will emerge over time.
In the evolution simulation, I’ve introduced a system that models fear-driven behavior to promote grouping among motile organisms. Each organism continuously evaluates its surroundings to determine its level of fear, shaped by both its social context and environment.
Fear is reduced when nearby organisms of the same species are present—especially in larger numbers. Conversely, fear increases over time when the organism is isolated or surrounded by groups of other species. As fear increases, the organism becomes increasingly likely to return to the last known location where it encountered a group of its own kind.
This creates a soft pressure for organisms toward staying near their own kind, and away from others . Organisms that leave a large group but remain close to a few peers experience only a mild increase in fear, allowing smaller offshoots to explore independently and eventually rejoin the group. The result is emergent flocking behavior: groups form, fragment, and reassemble through fully decentralized decisions. There is no leader or controller—each organism acts based on its own. The overall movement resembles bird murmurations, where coordination emerges without any top-down mechanism.
Over time, this dynamic encourages stable group formation and adaptive movement across the landscape, driving species-level divergence and more complex ecological patterns in the simulated world.
In the evolution simulation, groups of organisms often move together—shifting, splitting, and reforming in fluid patterns that resemble bird murmurations. There’s no leader, no shared objective, and no explicit rule to move as a group. Yet the behavior emerges naturally from decisions made by each individual.
Each organism evaluates its surroundings and decides whether to move based on its current individual needs: eating, reproducing, fleeing danger, or just exploring. If it’s low on energy and remembers a location where energy was once found, it moves in that direction. If it’s looking for a mate, it looks for nearby compatible organisms or moves toward places where it has seen others of its kind.
Organisms also prefer to stay close to others of their own species—especially when resting or reproducing—so they tend to cluster. But occasionally, one ventures off. If that organism finds something valuable, others nearby might gradually follow, drawn by the same logic, not by any awareness of the movement as a group.
What emerges is lifelike: organisms migrating across the world, forming temporary herds or colonies, and collectively shifting direction without coordination. The group behavior is entirely emergent, built from individual logic and memory—no central control, no global plan.
Problem: simulating plants or other sessile organisms pollinating and mating with others of the same species over distance.
A straightforward approach was to check for same-species organisms at adjacent positions. This mirrors pollen spreading by wind and was convenient because I already had the functionality for adjacent position searches. The drawback was that it put more pressure on an already tight loop with little FPS headroom.
After weighing alternative designs and their complexity against possible efficiency gains, I kept the adjacent-position search but added two under-the-hood improvements. I wanted to cache adjacent data per position and species, but that required a stable state to cache. Since my state changed constantly by design, a cache would go stale immediately.
Solutions
Double buffer read/write state Previously, I used a single “hot” state for both reads and writes, meaning the state changed constantly during iteration. This worked because I ensured each organism was processed only once per iteration, but it made caching impractical.
The fix was to implement a double-buffered state manager with two parallel states. One state is read-only for the duration of the iteration. All updates are written to the second state, which is rebuilt in the background. At the end of the iteration, the states swap, so the next iteration reads from the previously written state while a new one is built from scratch.
Cache With a static read-only state, I could implement a simple key-value cache for lookups, such as adjacent positions. This allowed plants to locate nearby same-species plants and attempt mating efficiently.
Building an efficient server-client communication system for my evolution simulation has been a fascinating journey. Each iteration brought new challenges and insights as I balanced performance, bandwidth usage, and user experience. Here's a look at my progression:
1. Always Full State
My first implementation was straightforward: send the entire world state to every client on each update. I created a data structure that contained all entity information, metadata about the world state, and statistics. This provided a reliable baseline but quickly revealed scalability issues as my simulation grew.
This approach worked well for testing but wasn't sustainable with hundreds of organisms and multiple clients.
2. Custom Serialization
To reduce payload size, I implemented custom serialization. Instead of sending verbose JSON with property names, I used a simple array where keys and values are packed together, removing the need for many control chars.
This significantly reduced my payload size, with some entities shrinking from ~200 bytes to just ~50 bytes.
3. Minimized IDs
Entity IDs were initially small 4 char base62 IDs. I implemented a more compact ID system using incremental base62 IDs for session-scoped references, reducing size even more.
4. Change Deltas
Instead of sending complete entities on each update, I began tracking client state and sending only what changed. I implemented a function that compares two states of an entity and returns only the properties that differ between them. This delta approach meant that if only an entity's position changed, I would send just the position rather than the entire entity definition.
This reduced typical payloads by 70-90% in stable world states, as most entities only change position or a few properties between frames.
5. Notify Server of Missing States
Delta-based approaches created a new problem: what if clients miss an update? I implemented a feedback mechanism allowing clients to notify the server when they're missing entity data. When a client sends a feedback message indicating it has missing entities, the server clears its assumptions about what the client knows and sends a full update on the next iteration.
6. Skip Out-of-Order State Iterations
Network delays sometimes resulted in iterations arriving out of sequence. I added a simple mechanism to track the last applied iteration and ignore older ones. In the client code, I check if the received state's iteration number is less than the already applied iteration, and if so, discard it.
This prevented visual glitches where the world would temporarily "jump backward."
7. Client-Side State Buffering
To further address the occasional out-of-order delivery, I implemented a buffering system in the client. The system stores incoming states in a map keyed by iteration number. When processing states, it first looks for the exact next iteration needed. If that iteration is available, it's applied and removed from the buffer. If too many states accumulate in the buffer, it falls back to using the oldest one to prevent memory issues.
This buffer allowed the client to store states that arrived early and apply them in the correct sequence, creating smoother animations.
8. Aggregate Similar Organisms
As my simulations grew more complex, areas with high organism density created bandwidth spikes. A solution was to aggregate similar organisms in congested positions. When detecting multiple organisms of the same species in the same position, I add a special "aggregate" metadata property to the first organism's state, indicating how many similar organisms are at that location.
The client then expands these aggregates into individual entities. It checks for the aggregate property, and if present, creates that many individual entities with slightly modified IDs to distinguish them.
Results
The iterative improvements paid off:
Average payload size: reduced from ~1.5MB to ~2KB per update
Peak bandwidth usage: reduced from ~10MB/s to ~20KB/s
Each optimization built upon the previous one, creating a data transfer system that's efficient, resilient, and scalable.
When sending frequent updates in a real-time system, JSON wastes a surprising amount of bandwidth. Every key is wrapped in quotes, separated by colons, and repeated on every update. For small payloads, that overhead can be bigger than the actual data.
For example, a JSON delta update like:
{ "a": "Alice", "b": "30" }
is 23 bytes on the wire. By removing quotes, braces, and colons, and by making the first character of each segment the key, the same data becomes:
aAlice,b30
— just 14 bytes.
This compact format works best when keys are short and fixed, and values are simple strings. It’s trivial to encode and decode, and in high-frequency updates the savings add up quickly, without needing the complexity of a binary protocol.
In order to minimize data size in memory and over the wire, I wanted to reduce all unnecessary bloat. Entity ids was one area that could be optimized. minimizing data size saves latency, memory and cost of data egress, which adds up over time for many clients.
I used to have a four char base62 id for entities, which is already very small. But it could be smaller, using base62 incremental ids.
The new id scheme is incrementing values in a base62 address space, starting with ”a” up to ”9”, then continues from ”aa” up to ”a9”, increasing the size of the id slowly, always ensuring the smallest possible set of unique ids.
These ids are purely internal, and the typical number of entities in an evolution is below 1 million.
Sessile organisms feed off of elements in the soil, while motile organisms feed off of each other, and off of sessile organisms. When an organism dies, part of its internal elements are returned to the soil as elements for sessile organisms to feed on.
This creates an eternal cycle of energy in the simulated world.
Balancing the weight variables that drive the evolution simulation is very tricky. Each factor pushes and pulls on the others, and even the smallest tweak can set off a chain reaction. Sometimes two values directly oppose each other, forcing me to decide which way the scales should tip.
At the moment, there are about 15–20 core variables, all intertwined — a web of influence that's roughly 15² connections deep. It's like tuning an ecosystem’s DNA: the goal is to find an equilibrium where life can flourish without overrunning itself, resilient enough to weather big shocks, but fluid enough to evolve in new directions. History is full of surprises — who would have bet on a world without dinosaurs after the Cretaceous?
Watching the environment adapt is really fascinating. Sometimes it recovers, sometimes it collapses spectacularly. Either way, I'm still turning the dials, chasing that perfect balance.