bevy_society
Give your Bevy world a living society: villagers who remember, gossip, take sides, fall out and make up — and who you can script.
The demo

One afternoon in a village of twenty-four people: a weaver tells the rumor that Bram stole bread, and in the six game-hours the clip covers (14:00 to 20:00) it passes to one more villager — the plate in the corner reads known by 2/24 on the last frame. The same run recorded twice as long, twelve game-hours, is still at 2, and which weaver tells matters: of ten tellers tried, four passed it on inside six hours and six did not (docs/DEMOS.md), so this clip tells it from one of the four. A rumor crosses a village in game-days, not hours, because each telling is one choice among many and the Daley–Kendall rule stifles a teller whose listener already knew. Tie lines show friendships green; the magenta ring marks whoever knows the rumor.
Every clip below is the same village, same seed, same run — only the overlay changes, except Goods, which also opens with bread handed out and says so. None of them draws a rivalry: the plate reads Rivalries 0 from first frame to last, because six game-hours is not long enough for one to form. A longer run does form them — 28 directed rival ties against 62 friend ties at seed 7 over 17 game-days (examples/evidence, docs/loop/runs/2026-10-30-25/eval.md). The clips were recorded on 2026-10-25, before Tuning::forgiveness (2026-10-30) changed how the settled village's ties form; they show the village as it was then.
One clip per feature
Mood — a halo on every villager. Green is pleased, amber neutral, red displeased; halo size is arousal. The colour scale is normalised to the ±0.45 this village actually spans (MOOD_SPAN), so a small mood shift is a visible colour change rather than every villager the same olive. |
Opinions — one bar per villager on the honesty topic, green above the midpoint and red below, length by strength, all drawn from a fixed origin so a near-neutral opinion is still legible. |
Needs — a full green halo is a fed villager, a small red one is hungry. |
Relationships — the tie graph alone: green friendship, red rivalry, brightness by familiarity, and only for pairs already familiar. Every line in this clip is green — the village has no rivalry yet. |
Factions — orange millers, green weavers, violet watch, no ties, so the group structure is the only thing on screen. |
Goods — a gold ring is a villager holding bread, sized by how much; a small dark one holds none; a white line is a loaf being shared. The recording opens with every other villager holding four loaves (the village's own stores sat near zero when this was recorded, so a clip from the settled state had nothing to show; they no longer do — a 24-villager village holds about five loaves a head by its seventh game-day, and from this release every person starts with a loaf), and the plate reads 0 loaves shared · 12 hold bread on the first frame and 5 loaves shared · 17 hold bread on the last. The hero clip above is the same run with every overlay on. |
docs/DEMOS.md is the recipe: the scene each clip is recorded from, the camera, the encode, and what a visual review has caught in each rebuild.
What you get
- People with a mind — Big Five personalities, moods on a pleasure/arousal/dominance axis, an OCC emotion set, needs that decay, and memories that fade. personality-and-mood, memory Each person can keep their own hours: a
Chronotypeis how far their body clock runs behind the village's,Input::BodyHourreads it, and a sleeping person is out of everyone else's reach. Measured on the 24-person village over 17 game-days, four seeds: 60-64 % of the sleeps used to begin in one clock hour and 7-9 % of all exchanges were aimed at somebody asleep; with a population of lark and owl offsets the busiest hour holds 19-27 % of them and under 0.1 % of exchanges find a sleeper. A person with no offset keeps the village's own clock. chronotype - Daily life and trades — utility-AI decisions over tagged places and other people, with travel, arrival, durations and need satisfaction. A task is not a skill: an activity can declare a
profession, and aSkill("craft")consideration reads the trait that capability comes from, so who does the skilled work is decided by comparative advantage rather than by assignment. Nothing is gated — an unskilled villager can still take the task, at a lower score. needs-and-utility A full place costs instead of vanishing: a place with no free seat keeps1 - Tuning::crowding_penaltyof its score, so a villager with an alternative goes elsewhere and one with none still arrives, whilecapacity: 0is closed. Measured, this holds a village's rival density at 2.9-6.1% of pairs as it grows from 24 to 48 people in a 30-seat village, where the old veto took it to 23% and lost the working day. capacity - A week —
Input::Weekand aDays(from, to)curve are the day-of-week analogue ofHourandHours(withSocietySettings::start_weekdayandctx.weekday()), so a pack can shut the workshop on rest days without a script. The starter pack has no week;examples/assets/weekly.society.ronis an opt-in overlay with two rest days. Shuttingworkalone damages the village (the freed hours go to company: friend ties 51 → 25 and rival ties 44 → 80 at seed 7), so the overlay adds alazethat takes two-hour spells of nothing; measured over seven runs, a rest day then has 7.5 approaches a villager against 10.7 on a workday, 3.6 different people met against 4.7, and 44 % of approaches to a friend against 37 % (Sekara et al. 2016: weekends are spent with a few selected friends), at the price of asocialneed 0.05 lower. weekly-rhythm - Relationships — directed ties carrying affinity, trust, respect and familiarity, with power-law forgetting, structural balance, a Dunbar-style cap and friend/rival labels. Affinity is clamped to
±Tuning::affinity_ceiling(0.35) and aFriendneeds trust as well, so a large town with daily contact keeps friendship scarce: measured, 60 people over 30 game-days reach 155 friend ties of 3 540 ordered pairs (4.4%). A person who has been turned down can be made to not ask the same person again: setTuning::rebuff_memory_hourstoSome(12.0)(off by default; Maner et al. 2007, excluded people do not seek reconnection with those who excluded them) and, measured on the 24-person village and towns of 40 and 60, a refused approach that was followed within the hour by another to the same person 24-29 % of the time is followed by one 0.3-1.1 % of the time, and people approach 5.2-6.3 different people a day where they approached 4.1-5.1. relationships, rebuff, the measurement - Opinions — bounded-confidence influence, stubborn anchors and conformity to a clear group majority. opinion-dynamics
- Rumors and reputation — gossip with the Daley–Kendall stifling rule, belief scaled by trust, witnesses who judge deeds and hold grudges. rumor-and-reputation
- Factions and status — in-group affinity, standing as conferred respect, contests and intergroup contact. groups-and-status
- Crowd fields — a stigmergy grid of presence and commotion per cell, readable as a
Field(Crowd)consideration and walkable as aGradienttarget, so an activity can be "join the crowd" without naming a place. needs-and-utility - Scripts — Rhai triggers over a per-tick snapshot, with a
ctxreader and anfxwriter that cannot see its own writes. scripting - Dialogue — barks and conversations through
bevy_speech_bubbles. dialogue - Decision traces — why a person chose what they did: every option weighed, its score, and how far the chosen one fell short of the best. Insert a
DecisionTraceand read it; recording changes no outcome. decisions - Obstacles — solid buildings that walkers path around, as hard walls in the ORCA solver, with a resolver that pushes anyone who lands inside back out. getting started
- A basic economy — goods as quantities rather than currency, a per-person store,
produces/consumeson a task, a price per good read from how full the village's stores are, per head — empty is double, full is half, the shape the corpus's own game-economy model uses (economics) and readable from a script asctx.price("bread"), plus goods that change hands: an interaction canactor_gives/target_givesa good, and aSurplusconsideration scores a transfer by the joint surplus of two stores. In the village that is sharing — measured over nine seeds, it cuts the spread of bread across the village by 38% — and the engine does a two-way swap too. Nothing gates: an empty store never stops anyone eating — but it stops the meal feeding them, because a task'ssatisfiesis paid in proportion to what itsconsumesactually found, so a meal from an empty larder costs the walk and restores nothing while half a loaf feeds half. Measured, that closed a village fed by arithmetic: 4.80 loaves baked a game-day against 21.06 meals, ~77% of them out of an empty shelf. A person starts holding each good's authoredinitial(the starter pack gives one loaf), so a fresh village is fed at its first dawn; with every larder empty, 39% of its second game-day's villager-ticks were spent withfoodbelow 0.05. A consideration can read the actor's own shelf (Stock("bread")), so a pack can make production answer to the larder instead of the clock: a hungry household with nothing bakes, one holding bread does not (examples/assets/provisioning.society.ron; on the evidence town of sixty it is the difference between a store that holds and one that runs out). economics · authoring - Persistence — the whole society captured and restored through
serde: ties, memories, rumors, the clock, the random stream and every trigger's bookkeeping, keyed by your own ids, so the ticks after a reload are the ticks that would have followed. persistence
Every formula traces to a paper; every rate is a named field on Tuning; every default either comes from a paper or says it is a design default.
Install
[]
= "0.9"
Quick start
This is the crate's own doctest, and tests/readme.rs fails if the two ever differ.
use *;
use ;
let defs = starter.expect;
// A headless app has no frame clock, so `Time<Fixed>` never advances on its own. Without this the
// society takes zero ticks however many updates you run.
let step = from_secs_f64;
let mut app = new;
app.add_plugins
.insert_resource
.insert_resource
.insert_resource;
// `DialoguePlugin` needs a speech-bubbles plugin over a camera, which this snippet has not.
let plugins = SocietyPlugins.build;
let plugins = plugins.;
app.add_plugins;
let well = app
.world_mut
.spawn
.id;
for name in
for _ in 0..600
// The society advanced: this line is what proves the fixed step was pinned.
assert!;
Run it headlessly with cargo run --example quickstart. The example sets a short 16-second game-day, so 600 updates are enough for the village to start talking; on this code it reports 229 social exchanges, 112 accepted.
The full village, with balloons and the debug server:
cargo run --example village -- /System/Library/Fonts/Supplemental/Arial.ttf
Font paths: macOS /System/Library/Fonts/Supplemental/Arial.ttf; Linux /usr/share/fonts/liberation/LiberationSans-Regular.ttf; Windows C:\Windows\Fonts\arial.ttf.
Cargo features
| feature | default | turns on |
|---|---|---|
dialogue |
yes | DialoguePlugin, TalkTo, barks and conversations through bevy_speech_bubbles. The host adds SpeechBubblesPlugin over its own camera. |
steering |
yes | SteeringPlugin: SocialMover walking under ORCA (bevy_orca), and Obstacle. |
fields |
yes | SocialFieldPlugin: crowd presence and commotion grids and per-faction presence grids (bevy_stigmergy), the Field consideration and the Gradient target. |
serde |
no | SocietySave, capture and restore: save and load the whole society. |
none (--no-default-features) |
the core: mind, daily life, relationships, opinions, rumors, reputation, factions, scripts and DecisionTrace. |
A tour
People
app.world_mut.spawn;
A player character is a Person without Autonomous: it does only what the game tells it.
Needs and daily life
(
id: "sleep",
target: Home,
duration_hours: 8.0,
rank: 1,
considerations: [
(input: Hour, curve: Hours(22.0, 6.0)),
(input: Need("rest"), curve: Linear(-1.0, 1.0)),
],
satisfies: [("rest", 0.15)],
)
Options are scored by compensated considerations, tiered by rank, and drawn by weight. A rank can follow a need: urgent: Some((need: "food", below: 0.25, rank: 1)) puts an activity in the higher tier while that need is low. The provisioning overlay (examples/assets/provisioning.society.ron) uses it on eat and bake, so a hungry villager eats, or bakes when the shelf is bare, before work, company and play; the starter pack leaves it off (docs/guide/authoring.md).
Relationships
Ties holds one directed edge per person with affinity, trust, respect and familiarity. Labels are recomputed each tick and a change writes TieLabelChanged. A label is a latitude, not a line: entering a category and leaving it are different tests (Tuning::label_hysteresis at the low end, Tuning::friend_band for a friendship, Tuning::rival_band for a rivalry), so a value resting on a bar does not announce a relationship change every time the model nudges it. Measured on village-evening at seed 7, a friendship now lasts a median 1.01 game-days instead of 0.36 (CHANGELOG.md, 0.3.0), and a rivalry 2.64 instead of 0.29 — before the rival band a Rival was released when its affinity climbed 0.0001 above the bar, so the village made rivalries it could not keep (the sweep).
Opinions
An accepted chat pulls both sides' opinions toward each other inside their confidence bounds, and a confident group majority pulls an agreeable person further, one tick at a time.
Rumors and reputation
app.world_mut.write_message;
Every witness who knows it can pass it on; a listener who already knows it stifles the teller.
Factions and status
(factions: [(id: "millers"), (id: "weavers"), (id: "watch", ingroup_bias: 0.3)])
Two co-members start a tie with in-group affinity, and standing is the respect conferred by everyone who knows a person well enough.
Triggers and scripting
(
id: "watch_stops_theft",
on: NormViolated("steal"),
if: Some("ctx.member(actor, \"watch\")"),
then: "fx.bark(actor, \"Stop, thief!\")",
)
Dialogue
app.add_plugins;
app.world_mut.write_message;
Built on
| Crate | Used for | Source |
|---|---|---|
| Bevy | the engine | https://github.com/bevyengine/bevy |
| bevy_speech_bubbles | barks and conversations (dialogue) |
https://crates.io/crates/bevy_speech_bubbles |
| bevy_stigmergy | per-cell presence and commotion fields (fields) |
https://crates.io/crates/bevy_stigmergy |
| bevy_orca | reciprocal collision avoidance for walkers (steering) |
https://crates.io/crates/bevy_orca |
| bevy_emerge_rng | the seeded ChaCha8 stream | https://crates.io/crates/bevy_emerge_rng |
| Rhai | trigger and effect scripts | https://github.com/rhaiscript/rhai |
| serde | authored definitions | https://github.com/serde-rs/serde |
| RON | the definitions' file format | https://github.com/ron-rs/ron |
| rand_chacha | names the ChaCha8 generator the stream comes from | https://github.com/rust-random/rand |
| bevy_fairy | the agentic debugger in the village example | https://github.com/Ladvien/bevy_fairy |
The science
| Module | Key papers | Note |
|---|---|---|
| Personality and mood | Mehrabian 1996 · Gebhard 2005 (ALMA) | note |
| Body clocks | Roenneberg et al. 2007 · Fischer et al. 2017 · Wittmann et al. 2006 | note |
| A week | Sekara et al. 2016 · Dodds et al. 2011 | note |
| Needs and utility | Mark & Dill (Game AI Pro 3 ch. 13) · Dill 2015 | note |
| Relationships | Heider 1946 · Zajonc 1968 · Dunbar 1992 | note |
| Rejection and re-approach | Maner et al. 2007 · Downey & Feldman 1996 · Bondü & Richter 2016 | note |
| Opinions | Deffuant et al. 2000 · Friedkin & Johnsen 1990 · Asch 1956 | note |
| Rumors and reputation | Daley & Kendall 1964 · Nowak & Sigmund 1998 · Ohtsuki & Iwasa 2004 | note |
| Factions and status | Tajfel et al. 1971 · Pettigrew & Tropp 2006 · Bonabeau et al. 1995 | note |
| Memory | Park et al. 2023 · Anderson & Schooler 1991 | note |
| Social simulation in games | Game AI Pro chapters · McCoy et al. 2014 | note |
The papers were gathered with home-still, a local corpus pipeline; docs/research/README.md lists a DOI and a corpus id for every mechanism.
Bevy compatibility
| bevy_society | Bevy |
|---|---|
| 0.1, 0.2, 0.3 | 0.20 |
Documentation
Getting started · Concepts · Authoring · Scripting · Dialogue · Decisions · Persistence · Research · Demos
Contributing
Issues and pull requests are welcome. Agents working in this repository should read AGENTS.md first: it lists the invariants (determinism, the message rule, the single script-application point) that a change must not break.
License
MIT OR Apache-2.0, at your option.
Mood — a halo on every villager. Green is pleased, amber neutral, red displeased; halo size is arousal. The colour scale is normalised to the ±0.45 this village actually spans (
Opinions — one bar per villager on the honesty topic, green above the midpoint and red below, length by strength, all drawn from a fixed origin so a near-neutral opinion is still legible.
Needs — a full green halo is a fed villager, a small red one is hungry.
Relationships — the tie graph alone: green friendship, red rivalry, brightness by familiarity, and only for pairs already familiar. Every line in this clip is green — the village has no rivalry yet.
Factions — orange millers, green weavers, violet watch, no ties, so the group structure is the only thing on screen.
Goods — a gold ring is a villager holding bread, sized by how much; a small dark one holds none; a white line is a loaf being shared. The recording opens with every other villager holding four loaves (the village's own stores sat near zero when this was recorded, so a clip from the settled state had nothing to show; they no longer do — a 24-villager village holds about five loaves a head by its seventh game-day, and from this release every person starts with a loaf), and the plate reads