A multi-agent ecosystem simulation built with Enviro and C++. The project explores the population-resource balance problem: a species of autonomous gatherer agents competes for finite, regenerating resources. Units that successfully gather can fuel the base to reproduce; units that fail to find food starve and die. The system self-regulates toward a dynamic equilibrium -- or collapses if parameters tip the balance.
This simulation models a closed ecosystem inspired by ecological dynamics such as the Lotka-Volterra model. Instead of differential equations, the population and resource interactions emerge from individual agent behaviors running in real time.
The core feedback loop:
Resources regenerate --> Gatherers collect resources --> Base spawns more gatherers
^ |
| v
Resources deplete <-- More gatherers consume faster <-- Population grows
|
v
Gatherers starve and die --> Population shrinks --> Resources recover
This creates a natural oscillation: population booms when resources are abundant, then crashes as overconsumption depletes the map, then slowly recovers as resources regenerate into a less crowded arena.
- Base -- a stationary structure that receives resources from gatherers and spends them to produce new units (cost: 5 resources each). If all units die, the base performs an emergency free spawn to prevent extinction.
- Gatherer (UnitA_collect) -- a small triangular unit with five range sensors. It wanders the arena searching for resources, picks them up, and navigates back to the base. A gatherer that fails to find any resource within 600 frames starves and dies. A gatherer stuck returning for over 900 frames also perishes. This mortality is what prevents unbounded population growth.
- Resource -- a static diamond-shaped node. Disappears when collected by a gatherer. The map manager periodically spawns new resources, modeling environmental regeneration.
- MapManager -- an invisible agent that drives resource regeneration: every 200 frames, it spawns up to 5 new resources at random positions (capped at 20 total). It also displays a real-time HUD showing population count and resource stockpile.
- Walls -- four boundary walls. Gatherers detect them via sensors and steer away.
All agents communicate through Enviro's event system (emit / watch). No agent reads another's internal state -- behaviors emerge purely from local perception and decentralized events.
The central design challenge is tuning parameters so the system neither explodes (infinite units, instant resource depletion) nor collapses (all units die, resources sit uncollected). The key parameters are: unit spawn cost (5 resources), starvation timeout (600 frames), resource respawn interval (200 frames), and respawn batch size (5). These were tuned through experimentation to produce visible boom-bust oscillations rather than a flat steady state.
When multiple gatherers reach the same resource simultaneously, a naive pickup model causes double-counting. The solution is a two-phase claim/grant protocol: the gatherer emits resource_claim; the resource, if uncollected, marks itself collected and emits resource_granted back to that specific unit only. This ensures each resource is consumed exactly once, which is critical for accurate population dynamics.
Both gatherers and resources are created and destroyed at runtime. Enviro's remove_agent() cannot be called from inside a collision or event callback without risking a segmentation fault. The solution is a two-frame delayed deletion: on the first frame the agent sets a dead flag; on the next update() call it performs remove_agent(). All handlers check dead and removed flags before acting.
Gatherers have five sensors (front, front-left, front-right, left, right). The avoid_walls() method compares open space on each side and steers toward the more open direction, slowing down proportionally to front-wall distance. It only reacts to WallH/WallV reflections, ignoring resources and the base, so gathering behavior is not disrupted by the avoidance logic.
- Docker installed on your machine.
# 1. Clone this repository
git clone https://github.com/doubleBlack2/enviro-ecosystem-sim.git
cd enviro-ecosystem-sim
# 2. Start the Docker container
docker run -p80:80 -p8765:8765 -v $PWD:/source -it klavins/enviro:1.6 bash
# 3. Inside the container, build and run
make
esm start
enviroOpen your browser and go to:
http://localhost
You will see the arena with a red base in the upper-left corner, one initial gatherer unit, seven resource nodes (gold diamonds), and dark boundary walls. The simulation starts immediately.
- Population growth phase: The initial gatherer finds resources and delivers them to the base. Once the base accumulates 5 resources, it spawns a new gatherer. The population grows as long as resources are plentiful.
- Resource pressure: As the population increases, resources are consumed faster than they regenerate. The map becomes sparse.
- Starvation and decline: Gatherers that cannot find resources within the timeout period die (disappear from the arena). The population contracts.
- Recovery: With fewer gatherers competing, resources accumulate again. The base eventually spawns new units, and the cycle repeats.
- Visual cues: Gatherers turn orange when carrying a resource, red when searching. The HUD in the upper-right corner shows
R:(base resource stockpile) andU:(current unit count) -- watch these numbers oscillate over time.
enviro-ecosystem-sim/
├── config.json # World configuration (agents, arena size, walls)
├── Makefile # Compiles src/*.cc into lib/*.so
├── README.md
├── LICENSE
├── defs/
│ ├── base_a.json # Base definition (static, 20x20 square)
│ ├── unit_a_coll.json # Gatherer definition (dynamic, triangle, 5 sensors)
│ ├── resource.json # Resource definition (static, diamond shape)
│ ├── map_manager.json # Map manager definition (noninteractive)
│ ├── wall_h.json # Horizontal wall definition (static, 1100x10)
│ └── wall_v.json # Vertical wall definition (static, 10x700)
└── src/
├── base_a.h / base_a.cc # Base controller (resource tracking, unit spawning)
├── unit_a_coll.h / unit_a_coll.cc # Gatherer controller (wander, sense, claim, deliver)
├── resource.h / resource.cc # Resource controller (claim/grant protocol, removal)
├── map_manager.h / map_manager.cc # Resource regeneration and HUD display
├── wall_h.h / wall_h.cc # Horizontal wall (empty controller)
└── wall_v.h / wall_v.cc # Vertical wall (empty controller)
All inter-agent communication uses Enviro events. No agent reads another agent's internal state.
| Event | Sender | Receiver | Payload |
|---|---|---|---|
base_a_position |
Base | Gatherers | {x, y} -- base coordinates for return navigation |
base_a_stats |
Base | MapManager | {resources, units} -- for HUD display |
resource_nearby |
Resource | Gatherers | {resource_id, x, y} -- broadcast every frame |
resource_claim |
Gatherer | Resource | {resource_id, unit_id} -- request pickup |
resource_granted |
Resource | Gatherer, MapManager | {resource_id, unit_id} -- confirm pickup |
resource_delivered |
Gatherer | Base | {faction, amount} -- deposit resources |
unit_spawned |
Base | (logged) | {faction} -- new unit created |
unit_died |
Gatherer | Base | {faction} -- unit removed |
- Enviro framework by Prof. Eric Klavins
- Elma event loop manager
- Lotka-Volterra population dynamics model (Alfred Lotka, 1925; Vito Volterra, 1926)
- EEP 520 course materials, University of Washington, Winter 2026