AgentSkillsCN

Flecs Patterns

Flecs 模式

SKILL.md

FLECS Patterns & Simulation Domain Knowledge

Component Design

  • Plain structs, no methods: struct Position { float x, y; };
  • Tags (empty structs) for classification: struct NormalBoid {}; struct DoctorBoid {};
  • State tags: struct Infected {}; struct Alive {}; struct Male {}; struct Female {}; struct Antivax {};
  • Singletons: world.set<SimConfig>({...}); world.set<SimStats>({...}); world.set<SpatialGrid>({...});

Pipeline Phase Mapping

  • PreUpdate: Rebuild spatial grid from all alive boid positions
  • OnUpdate: Boid steering (separation, alignment, cohesion), velocity integration, position update
  • PostUpdate: Collision detection → infection → cure → reproduction → death → doctor promotion
  • OnStore: Render pass (read-only queries populating RenderState for drawing)

Deferred Operations

Always wrap entity creation/destruction inside deferred blocks during system iteration:

cpp
world.defer_begin();
// Safe: create/destroy entities, add/remove components
world.defer_end();

Cached Queries

Create at startup, reuse every frame:

cpp
auto q = world.query_builder<Position, Velocity, NormalBoid>().build();
q.each([](Position& p, Velocity& v, NormalBoid) { /* ... */ });

Entity Lifecycle

  • Spawn: world.entity().add<NormalBoid>().add<Alive>().set<Position>({x,y}).set<Velocity>({vx,vy})
  • Kill: e.destruct() inside deferred block
  • Classify: e.add<Infected>(), e.remove<Infected>(), e.remove<NormalBoid>().add<DoctorBoid>()

Simulation Parameter Reference

All parameters are stored in the SimConfig singleton. Never use magic numbers.

ParameterField NameDefaultDescription
Initial infection (normal)p_initial_infect_normal0.05Chance normal boid starts infected
Initial infection (doctor)p_initial_infect_doctor0.02Chance doctor starts infected
Infection spread (normal)p_infect_normal0.50Normal×Normal infection chance on collision
Infection spread (doctor)p_infect_doctor0.50Doctor×Doctor infection chance on collision
Reproduction (normal)p_offspring_normal0.40Normal×Normal reproduction chance
Reproduction (doctor)p_offspring_doctor0.05Doctor×Doctor reproduction chance
Cure probabilityp_cure0.80Doctor cures sick boid on collision
Doctor promotionp_become_doctor0.05Adult normal boid becomes doctor (per frame)
Antivax percentagep_antivax0.10Normal boids that actively avoid doctors
Interact radius (normal)r_interact_normal30.0fNormal boid collision radius
Interact radius (doctor)r_interact_doctor40.0fDoctor boid interaction radius
Death timet_death300.0fFrames until infected boid dies
Adult aget_adult500.0fFrames until eligible for doctor promotion
Offspring count (normal)N(2,1)std::normal_distribution<float>(2.0f, 1.0f), clamp ≥0
Offspring count (doctor)N(1,1)std::normal_distribution<float>(1.0f, 1.0f), clamp ≥0
World sizeworld_width × world_height1920×1080Simulation bounds
Initial countsinitial_normal_count / initial_doctor_count200 / 10Starting populations
Max speedmax_speed3.0fBoid velocity cap
Max forcemax_force0.1fSteering force cap
Flocking weightsseparation_weight / alignment_weight / cohesion_weight1.5 / 1.0 / 1.0Boid flocking

Extension: Infected Debuff Multipliers

ParameterNormal DebuffDoctor Debuff
r_interact×0.8×0.7
p_offspring×0.5×0.5
p_cure×0.5

Store as debuff_r_interact_normal, debuff_r_interact_doctor, etc. in SimConfig.


Behavior Rules Matrix

EventNormal×NormalDoctor×DoctorDoctor×Normal
Infection spreadp_infect_normalp_infect_doctor✗ (no cross-swarm)
Reproductionp_offspring_normal, N(2,1) kidsp_offspring_doctor, N(1,1) kids✗ (no cross-swarm)
Curep_cure (doctor cures doctor)p_cure (doctor cures normal)

Critical Rules

  1. Cross-swarm infection does NOT happen — only cure crosses swarm boundary
  2. Two infected parents reproduce — children get contagion from only ONE parent (roll p_infect once)
  3. Doctors cannot cure healthy boids — no-op if target is not infected
  4. Reproduction requires cooldown — use ReproductionCooldown component
  5. Death = deferred destroy — always use world.defer_begin()/defer_end()
  6. Doctor promotion preserves state — remove NormalBoid tag, add DoctorBoid tag, keep everything else

Extension-Specific Rules

  • Sex system: Male/Female tags at 50/50 spawn. Reproduction only between Male + Female pairs.
  • Antivax: Antivax boids add strong repulsion force from DoctorBoid. This is ADDITIVE to existing flocking, never a replacement. They can still be cured if a doctor physically reaches them.
  • Infected debuffs: Apply multipliers to effective parameters during collision checks. Do not modify SimConfig base values — multiply at point of use.

Implementation Patterns

Infection Check Pattern

cpp
// In PostUpdate system — after spatial grid query returns neighbors
void check_infection(Entity a, Entity b, const SimConfig& cfg) {
    bool a_normal = a.has<NormalBoid>();
    bool b_normal = b.has<NormalBoid>();
    // Cross-swarm: no infection
    if (a_normal != b_normal) return;
    // Both must be same type; one infected, one not
    if (a.has<Infected>() == b.has<Infected>()) return;
    float p = a_normal ? cfg.p_infect_normal : cfg.p_infect_doctor;
    if (rng.uniform() < p) {
        // Infect the healthy one
        Entity& target = a.has<Infected>() ? b : a;
        target.add<Infected>();
        target.set<InfectionState>({true, 0.0f});
    }
}

Reproduction Pattern

cpp
// Only same-swarm pairs. With sex extension: require Male+Female.
void check_reproduction(Entity a, Entity b, const SimConfig& cfg, World& world) {
    bool a_normal = a.has<NormalBoid>();
    if (a_normal != b.has<NormalBoid>()) return; // no cross-swarm
    // Sex check (extension)
    if (a.has<Male>() == b.has<Male>()) return; // same sex, skip
    float p = a_normal ? cfg.p_offspring_normal : cfg.p_offspring_doctor;
    if (rng.uniform() < p) {
        float mean = a_normal ? 2.0f : 1.0f;
        int n_kids = std::max(0, (int)std::round(rng.normal(mean, 1.0f)));
        world.defer_begin();
        for (int i = 0; i < n_kids; i++) { /* spawn child */ }
        world.defer_end();
    }
}

Antivax Steering Pattern (Extension)

cpp
// ADDITIVE to existing flocking forces — never replace
Vector2 compute_antivax_repulsion(Entity boid, const SpatialGrid& grid, float visual_range) {
    Vector2 repulsion = {0, 0};
    if (!boid.has<Antivax>()) return repulsion;
    auto doctors = grid.query_neighbors(boid_pos, visual_range);
    for (auto& [doc_id, dist] : doctors) {
        if (!doc_id.has<DoctorBoid>()) continue;
        Vector2 away = normalize(boid_pos - doc_pos) / dist;
        repulsion += away * ANTIVAX_REPULSION_WEIGHT;
    }
    return repulsion; // Add this to the existing flocking force
}

Anti-Patterns (DO NOT)

  • std::rand() — always use <random> with a seeded std::mt19937
  • Magic numbers — all values come from SimConfig
  • Raylib includes outside src/render/ — rendering is isolated
  • Raw pointer ownership — use FLECS entity handles
  • Modifying existing component fields — only add new components/fields
  • Entity create/destroy outside deferred blocks — always defer during iteration
  • Cross-swarm infection — only cure crosses the swarm boundary
  • Replacing flocking forces for antivax — antivax repulsion is additive
  • using namespace std; — explicit namespacing only
  • double for sim values — use float everywhere