Wilden Dawn

Follow random organism

By Robin Dowling · 2 months ago

Following organisms by clicking them works well once you're looking at the simulation, but it requires finding an organism to click first. On a large world with organisms scattered across thousands of positions, this can take time. They can also be hard to catch sometimes. I wanted a faster way to jump into observation, to start following an interesting organism immediately without searching.

I added a "follow random" feature. When you're not currently following anything, the "following" label becomes clickable. Click it and you're instantly following a randomly selected motile organism, wherever it happens to be in the world.

The server implements this using reservoir sampling, an algorithm that selects a uniformly random element from a collection in one pass, using constant memory regardless of population size. The simulation maintains a cached random motile organism, updated each iteration, so responding to a follow random request is instantaneous.

When the server receives the follow random message, it retrieves the cached random organism and establishes a follow relationship, just as if you had clicked on that specific organism. Your viewport immediately centers on it. If the organism dies, the standard cleanup behavior applies and you stop following.

The implementation handles two distinct cases: following a specific organism you clicked, versus requesting any random organism. When you click the label, the client sends a "follow random" message. The server picks a random organism and establishes the same follow relationship as if you had directly clicked that organism. Both interaction types flow through the same code paths, with type safety ensuring consistent handling throughout.

The feature improves exploration significantly, especially for new visitors. Instead of navigating through the world looking for something interesting to follow, you can immediately jump into tracking an organism's life: see where it goes, what it does, how it interacts with others. If it's not interesting, stop following and request another random organism.

It's a small usability enhancement, but it removes friction from the primary way people explore the simulation, by following individuals and observing their behavior over time.

Optimizing position updates

By Robin Dowling · 2 months ago

The simulation's double-buffer state architecture had a performance bottleneck. Every iteration, it rewrote the position of every entity in the world, thousands of organisms and environmental objects, into a spatial index. This happened regardless of whether an entity had actually moved.

Sessile organisms like plants never move, yet their positions were being rewritten every single tick. Dead organisms weren't moving either, but their positions were still being updated. Even motile organisms that decided to stay in place for that tick would trigger position updates.

I was using a double-buffer implementation where all positions from one buffer were copied into the other, then the buffers swapped. This ensured determinism, all reads happened from a stable buffer while writes went to the other, but the cost was O(N) writes every iteration for all N entities.

The fix was to replace double-buffering with a single persistent spatial index and a deferred update queue. When an organism decides to move, instead of immediately updating the spatial index, the change is queued. The queue stores each entity's "from" position and "to" position.

At the end of the iteration, after all organisms have made their decisions, the queued updates are applied atomically. Each relevant entity is removed from its old position in the spatial index and added to its new position. Then the queue is cleared.

Organisms that don't move never queue updates. Sessile organisms, often the majority of the population, cost nothing. Dead organisms that remain positioned during decay cost nothing. Only organisms that actually move create entries in the update queue.

The performance impact is substantial. In a simulation with 1000 entities, maybe 200 motile organisms move while 800 sessile organisms stay in place, an 80% reduction in position writes.

Determinism is preserved because all position queries read from the frozen spatial index during iteration. No entity sees updated positions until the deferred updates are applied at the end, after all decisions are made. The simulation remains fully deterministic, order-independent, and correct.

The change was entirely internal. External consumers see no difference in behavior, only improved performance. It's the kind of optimization that makes scaling possible, reducing unnecessary work without changing what the system does.

Explore lineages across generations

By Robin Dowling · 2 months ago

Following an organism gives you its perspective, but it doesn't tell you much about the organism itself. I wanted a way to see detailed information: family relationships, health, age, fears, diseases, pregnancy status, in addition to the sparese information in the label.

I added an inspect organism panel that displays comprehensive data about the organism you're following or controlling. Press O or click on the "Following X" label to toggle the panel. It shows the organism's name, species, parents, siblings, children, generation, energy level, age category, fear level, diseases, and pregnancy information.

Family member names are clickable. Click on a mother's name and the panel immediately switches to showing that organism's details. Click on a sibling, a child, the organism's mate - any family connection becomes a navigation point. This lets you explore family trees and track lineages across generations.

The panel updates automatically every 10 seconds with fresh data. The server tracks which organism each session has requested inspection for and broadcasts updates only when the data changes, using etags to avoid redundant transmissions.

The architecture separates concerns cleanly. Server-side utilities extract and minimize organism data. Client-side utilities transform that minimized data into human-readable formats with clickable references. A generic info display component renders the data as DOM elements, handling clickable values and arrays without knowing anything about organisms specifically.

Inspecting organisms changes how you explore the simulation. You can follow an organism, see its entire family history, click through to its parents and see their histories, and follow lineages forward through children and grandchildren. You can watch fear levels rise and fall as organisms encounter threats. You can see diseases spreading through family groups. You can verify pregnancy status and track when offspring will be born.

It transforms observation from passive watching to active exploration. The simulation's data becomes directly accessible rather than hidden behind the visual representation.

Organism labels

By Robin Dowling · 2 months ago

Organisms in the simulation have individual names, belong to species, can be infected with diseases, be pregnant, following or being followed. But this information wasn't visible. You could watch an organism move across the world without knowing anything about it beyond its appearance.

I added labels that display directly above organisms. Each label shows the organism's individual name and species name. Additional indicators appear based on the organism's current state: a symbol for pregnancy, another for infection, another for interaction state.

Labels update in real time as an organism's state changes. When an organism becomes pregnant, the indicator appears. When it gives birth, the indicator disappears. If it becomes infected, that status shows immediately.

This makes the simulation much easier to understand. You can see family relationships, organisms with similar species names are possibly related. You can track disease spread through populations by watching infection indicators move through groups. You can identify which organism you're following at a glance.

The labels also make it easier to find specific organisms. If you want to follow a particular species or individual, you can scan the visible organisms and identify them by name rather than appearance alone.

Adding labels required balancing information density with visual clarity. Too much text clutters the viewport. Too little leaves observers without context. The current implementation shows essential information concisely, name, species, and state indicators, without overwhelming the view.

It's a simple addition, but it transforms observation from watching anonymous sprites move around to watching individuals with identities and stories.

Dead organism decay

By Robin Dowling · 3 months ago

When organisms died in the simulation, they vanished instantly. One moment they were there, the next they were gone. This created a jarring experience, you couldn't tell when or why an organism died, and you couldn't see the patterns of death across the ecosystem.

I changed death to be gradual and visible. When an organism dies, it now remains in the world for a while, visually decaying over time. Motile organisms tilt to the side, as if fallen over, and gradually darken. Sessile organisms simply darken in place. Eyes close on dead motile organisms. A death indicator appears in the organism's label.

The decay is tracked over adjustable time, currently 50 iterations. Each iteration increments the decay value, and the visual effects intensify accordingly. After reaching full decay, the organism is removed from state entirely. As before, its energy is transferred to the soil, made available for plants to consume.

This change required coordinating state between server and client. The server tracks each organism's death decay value and includes it in broadcasts. The client reads the value and applies appropriate visual effects—rotation, darkening, closed eyes.

Both pooled and non-pooled rendering paths support death effects. Movement and interaction systems skip dead organisms while keeping them positioned in the world. The visual effects are applied idempotently to prevent duplicate transformations if an organism is rendered multiple times during decay.

The impact on understanding the simulation is significant. You can now see where organisms are dying: starvation clusters in food-depleted areas, death along territorial boundaries between species, mass die-offs during disease outbreaks. You can watch predation events complete rather than having the prey vanish mysteriously. You can see the lifecycle close rather than experiencing it as a sudden disappearance.

Death statistics also shifted from tracking only cumulative totals to including current dead count. This gives a clearer picture of ecosystem health. A stable population maintains a consistent rate of deaths matching births, while a collapsing population shows deaths accumulating.

It's a more realistic representation of how ecosystems work. Death is visible, gradual, and part of the ongoing cycle rather than an instant state transition.

Stats websocket server

By Robin Dowling · 3 months ago

The evolution simulation generates a constant stream of data: births, deaths, species counts, population dynamics. This data was always available inside the simulation, but visitors to the site had to enter the full interactive experience to see any of it.

I wanted visitors to see the simulation's state on the webpage, before deciding whether to jump in. So I built a dedicated WebSocket server that broadcasts simulation statistics to the front page in real time.

The stats server is lightweight and separate from the main state server that handles game clients. It connects to the simulation engine, receives stats every 5 seconds, compresses the data, and broadcasts it to all connected clients. When a new visitor opens the homepage, they immediately receive the current stats without waiting for the next broadcast cycle.

On the client side, a stats display component cycles through different formatted views of the data. Total organisms with animal/plant breakdown. Births, deaths, concurrent generations. Species count. Dominant species for both animals and plants. The display fades smoothly between stats every 4 seconds.

The architecture keeps everything decoupled. Stats clients and game clients don't interfere with each other—they connect to separate endpoints. The stats broadcast logic is isolated from game state logic, making both easier to reason about and scale independently.

The result is a homepage that feels alive. Visitors see the simulation evolving in real time, species appearing and disappearing, populations rising and falling, without needing to enter the world themselves. It gives immediate context about what's happening inside before deciding to explore further.

Fear-driven movement

By Robin Dowling · 3 months ago

Survival in nature involves avoiding danger. In the simulation, organisms needed a way to recognize and flee from threats, specifically, larger organisms that might be dangerous.

I implemented a fear-driven behavior where organisms detect nearby organisms significantly larger than themselves. If a detected organism is at least 1.5 times their mass and from a different species, the smaller organism has a high chance of fleeing. If the larger organism is from the same species, there's a smaller chance, about 7.5%, to flee, modeling occasional avoidance of dominant members within a species.

The system filters out organisms that aren't actually threatening. Pregnant organisms aren't perceived as threats, they're vulnerable. Low-energy organisms aren't threatening either, they're weak. And family bonds override fear entirely: organisms never flee from their parents or children, regardless of size difference.

When an organism decides to flee, it calculates the centroid of all nearby threats and moves in the opposite direction. The system uses weighted direction selection to maintain momentum - organisms don't just move randomly away, they continue fleeing smoothly in a consistent direction.

Fear also creates a form of memory. When an organism flees from a position, that position is added to its recent position history with a penalty. This prevents the organism from immediately wandering back into the danger zone after fleeing. The penalty decays over time as the position ages out of the organism's memory.

The result is visible in the simulation. Smaller organisms maintain distance from larger ones. Larger organisms naturally dominate areas. Groups stay cohesive because smaller members avoid straying into territories occupied by larger organisms of other species. There's no explicit territorial behavior coded, it emerges from individual fear responses.

It's a simple threat-detection system, but it creates complex spatial dynamics across the simulated ecosystem.

Before

After

 

Movement momentum and memory

By Robin Dowling · 3 months ago

Organisms were oscillating. They would move one position toward food, then immediately move back the next tick, move toward a mate, then reverse course. The movement logic evaluated each tick independently, with no memory of where the organism had just been or which direction it was already moving.

This ping-ponging created unnatural, jerky movement that made the simulation feel mechanical rather than alive.

I implemented two mechanisms to solve this. First, directional momentum. The existing momentum system that weighted random movement to continue in the same direction was extended to all movement types. When an organism moves toward a goal or away from a threat, it now prefers to continue in roughly the same direction rather than making sharp turns. The weighting is exponential: continuing straight ahead is most likely, adjacent angles are less likely, and reversing direction is least likely.

Second, short-term position memory. Each organism now tracks the last 8 positions it visited in a circular buffer. When evaluating potential moves, recently visited positions receive penalties that decay exponentially. The most recent position gets about a 70% penalty, making it very unlikely the organism will immediately return there. Older positions have weaker penalties, eventually aging out of memory and becoming acceptable again.

These penalties are probabilistic, not absolute blocks. An organism can still return to a recent position if the situation demands it, if food is there or if fleeing danger requires it, but there's a strong behavioral preference to keep moving forward rather than backtracking.

The combination creates movement that feels organic. Organisms trace continuous paths, avoid constantly reversing course, and exhibit spatial awareness of where they've recently been. They still respond appropriately to goals and threats, but they do so with momentum and memory rather than instant reactions.

The change was technically and mathematically straightforward, a circular buffer and some weighted probability adjustments, but the behavioral impact is substantial. Movement looks and feels natural now.

Area-based spatial queries

By Robin Dowling · 4 months ago

In the simulation, organisms need to know what's nearby: finding food, detecting threats, locating mates. Early on, I stored entity positions individually and queried them one by one. For an organism checking its surroundings within a distance of 3 positions, that meant iterating through roughly 7 positions. At distance 10, that became 49 positions. At distance 20, over 400 positions. The cost grew quadratically.

This worked fine during development, but it didn't scale. Checking many positions sequentially, especially with thousands of organisms doing this every tick, became a significant bottleneck.

The fix was to restructure how positions are stored. Instead of mapping every individual position to the entities occupying it, I grouped positions into 25×25 area buckets. Now, querying nearby entities means identifying which 1-4 area buckets overlap with the organism's observation range, then filtering by actual distance, essentially a simplified quad-tree.

The performance improvement was dramatic. Benchmarking showed queries became 10 to 113 times faster depending on observation distance. More importantly, query time became essentially constant—checking distance 3 takes the same time as checking distance 20, both hovering around 0.001 to 0.002 milliseconds.

The change was entirely internal to the state management layer. The API shifted slightly, queries now return entity-to-position mappings instead of simple entity sets. No breaking changes leaked out to the simulation logic.

With this in place, I can safely increase observation distance from 3 to 10 or even 20 without worrying about performance degradation. The simulation feels more reactive and spatially aware, without the cost.

Texture pooling

By Robin Dowling · 4 months ago

Rendering thousands of organisms in a browser comes with constraints. Each organism rendered as individual sprites creates thousands of draw calls and consumes significant GPU memory. As the simulation scaled, this became a bottleneck, especially on mobile devices.

The solution was texture pooling, generating organisms as static textures that can be reused across visually similar organisms. Instead of rendering each organism from scratch every frame, the client generates a texture once for a given combination of species, size, and traits, then reuses that texture for all organisms matching those characteristics.

This is standard rendering stuff, but I'm new to it.

For motile organisms, I implemented a hybrid approach. The body is pooled as a static texture, but the eyes remain as live sprites layered on top. This preserves the blinking animation while achieving a 90% reduction in vertex count—from about 1400 vertices per organism down to around 106.

For sessile organisms like plants, the entire organism is pooled as a single static texture since they don't have animated features. The swaying motion is acheived simply by swaying the texture.

The system automatically adjusts pooling aggressiveness based on device capabilities. High-end desktops get 5 texture variants per combination with mass bucketed every 5 units. Mobile devices get 2 variants with mass bucketed every 7 units. Low-end devices pool even more aggressively.

Textures that haven't been used in 2 minutes are automatically evicted, keeping memory footprint reasonable as species evolve and go extinct. The system maintains a hard cap of 5,000 textures across the entire pool to prevent unbounded growth in long-running sessions.

The result is a simulation that runs smoothly with thousands of organisms visible simultaneously, adapting automatically to whatever device is viewing it.