You said:
I am designing an entity management system for my game. It needs to track multiple items and the data about them - where they are, animation state, etc. What software design patterns might help me create a well organised system that can be expanded later. For example later I need to add in collision detection
ChatGPT said:

Good question — the design of your entity management system will shape how easy it is to add things like collision, AI, and rendering later. A few well-known software design patterns are particularly useful for game entities:


1. Component-Based (Entity-Component System, ECS)

  • Idea: Instead of a rigid inheritance tree (e.g., Player -> Enemy -> MovingEnemy -> FlyingEnemy), entities are just IDs with components attached (e.g., Position, Sprite, Animation, Physics).

  • Benefits:

    • Easy to extend (just add a new component type).

    • Systems (e.g., CollisionSystem, RenderSystem) operate on entities with the right components.

    • Promotes separation of concerns — position logic isn’t tangled with animation logic.

  • When to use: If you want flexibility and expect lots of entity types with overlapping features.


2. Factory / Builder

  • Idea: Centralize creation of entities so you don’t scatter initialization code everywhere.

  • Example: A EntityFactory could have methods like createPlayer(), createEnemy(type), etc. These attach the right components.

  • Benefits: Makes it easy to add new entity types without modifying existing code.


3. Observer (Publish/Subscribe)

  • Idea: Entities or systems can listen for events (e.g., "entity destroyed", "collision occurred", "health changed") without tightly coupling classes.

  • Benefits: Keeps systems decoupled. Collision system can publish "collision events", and AI or physics can choose whether to respond.

  • When to use: Later when you add collision detection, UI updates, or sound effects triggered by game events.


4. State (for Animation or AI)

  • Idea: Represent things like "Idle → Walking → Jumping" or "Patrol → Chase → Attack" with distinct state objects or an enum + function pointers.

  • Benefits: Keeps animation/AI behavior clean and avoids giant if-else ladders.


5. Flyweight (for Shared Data)

  • Idea: Store heavy, read-only data (e.g., sprite sheets, collision shapes) once and reference it across entities.

  • Benefits: Saves memory and improves cache friendliness.

  • Example: All skeleton enemies reference the same SkeletonData flyweight but store their own position/health.


6. Service Locator (cautious use)

  • Idea: A global way to access shared systems like Renderer, CollisionSystem, Audio.

  • Benefit: Makes system-wide services accessible without passing them everywhere.

  • Caution: Can turn into hidden dependencies if abused. Some devs prefer dependency injection.


Putting it together (Example Flow)

  • Your EntityManager is basically the Coordinator.

  • Entities are just IDs with attached components.

  • Systems (Collision, Render, Physics) process entities with specific components.

  • The Factory makes new entities with the right components.

  • The Observer pattern lets systems react to events (e.g., collisions).

  • State is used within components for animation or AI.

  • Flyweight stores shared sprite/animation data.


👉 If you want a simpler path (since you’re writing your own engine in C), you don’t need a full-blown ECS from the start. You could start with:

  • struct Entity with fields for position, animation state, etc.

  • A EntityManager that owns an array of them.

  • Later, migrate fields into separate "components" as needs grow.


Do you want me to show you a lightweight C-style ECS design that could grow into this, or would you rather see a simpler OOP-inspired entity manager with factories and states?

You said:
 I am writing in C, not C++ so does that make some of these patterns unusable, and others more relevant?
ChatGPT said:

Yes — writing in C changes things quite a bit. Some object-oriented patterns assume inheritance, virtual methods, or automatic polymorphism, which you don’t have in C. But you can still implement the ideas of those patterns with C techniques: structs, function pointers, arrays, and modules. Here’s how the patterns translate:


Patterns that map well to C

✅ Entity–Component System (ECS)

  • Very natural in C:

    • Entities are just integer IDs.

    • Components are arrays of structs (Position[ENTITY_MAX], Animation[ENTITY_MAX]).

    • Systems are plain functions that loop over those arrays.

  • No need for inheritance → data-oriented design fits C well.

✅ Factory / Builder

  • Works fine in C: a function like Entity create_enemy(EntityManager *em, EnemyType type) that sets up the right component data.

  • Lets you avoid duplicating initialization logic.

✅ Observer (Publish/Subscribe)

  • Can be done with arrays of function pointers or a list of callbacks.

  • Example: collision system generates events → entity manager calls registered listeners.

  • Works nicely in C when you want loose coupling.

✅ State

  • In C, states can be implemented with:

    • an enum + switch, or

    • a table of function pointers (update(), enter(), exit()), with the entity storing a pointer to its current state.

  • Commonly used for AI and animations.

✅ Flyweight

  • Fits C very well: you can store shared immutable data (like sprite sheets or hitbox shapes) in a global registry, and entities just store an index/pointer into it.

  • Saves memory, avoids duplication.


Patterns that are trickier in C

⚠️ Class-based inheritance patterns (e.g. Template Method, Visitor, OOP-heavy State pattern)

  • Not directly usable since C has no classes.

  • But you can simulate them with function pointers + structs if you really need polymorphism.

⚠️ Service Locator / Dependency Injection

  • You don’t get automatic DI like in OOP frameworks.

  • But you can mimic a "service locator" with a global struct holding pointers to systems (struct Services { Renderer* r; Physics* p; ... }).

  • Simpler than in C++, but still useful if you want centralised access.


Why C makes some patterns more relevant

C pushes you toward composition over inheritance by necessity.

  • ECS is arguably more natural in C than in C++ because it avoids deep class hierarchies.

  • Flyweight and Factories are easier to implement because you’re already working with explicit data structs.

  • Observer/event systems keep your modules separate, which is very important in larger C codebases.


👉 So in your case:

  • Start with ECS-lite (structs of arrays for components, or even arrays of structs if that’s simpler).

  • Use a factory function for entity creation.

  • Add event callbacks when you bring in collisions.

  • Use function-pointer state machines for AI/animations.

  • Store shared assets in flyweight-style registries.

That will give you a clean, expandable design without fighting C’s lack of OOP.


Would you like me to sketch a minimal C ECS code example (entities, components, and a simple system loop), or a simpler struct-based entity manager first, to get you started?

You said:
tell me more about ECS as a C programmer. I would be more inclined to have an "entity" structure with variables for all the data. And then put those in an array to represent all the entities in my game.
ChatGPT said:

Got it 👍 — that’s a very natural instinct in C. A simple struct Entity { ... } array gives you something that works fast without much boilerplate. But ECS (Entity–Component System) comes from moving away from “everything in one struct” toward separating data by concern, and that’s especially powerful in C. Let me break it down in terms of what you’re used to and where ECS diverges:


1. Naïve Entity Struct

What you’re describing might look like this:

c
#define MAX_ENTITIES 1024 typedef struct { int active; float x, y; float vx, vy; int spriteId; int animFrame; int health; int collidable; } Entity; Entity entities[MAX_ENTITIES];
  • Pros: Simple, easy to start with.

  • Cons: Every entity has all fields, even if unused (e.g. a “decoration” has health and animFrame it doesn’t need).

  • Harder to add features later → you must edit the entity struct and all code that touches it.


2. ECS Idea (in C terms)

Instead of one big struct, you separate concerns into components:

c
#define MAX_ENTITIES 1024 typedef struct { int active; float x, y; } Position; typedef struct { int active; int spriteId; int frame; } Sprite; typedef struct { int active; float vx, vy; } Velocity; typedef struct { int active; int hp; } Health;

Each is just an array indexed by entity ID:

c
Position positions[MAX_ENTITIES]; Sprite sprites[MAX_ENTITIES]; Velocity velocities[MAX_ENTITIES]; Health healths[MAX_ENTITIES];

An entity is now just an ID (int id).
You attach data by filling in the relevant arrays for that ID.


3. Systems

Logic is written as systems that operate on components:

c
void physics_system(float dt) { for (int i = 0; i < MAX_ENTITIES; i++) { if (positions[i].active && velocities[i].active) { positions[i].x += velocities[i].vx * dt; positions[i].y += velocities[i].vy * dt; } } } void render_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (positions[i].active && sprites[i].active) { draw_sprite(sprites[i].spriteId, positions[i].x, positions[i].y, sprites[i].frame); } } }

Notice:

  • No single function needs the whole entity.

  • Systems only care about the components they need.


4. Why ECS is nice in C

  • Data-oriented: Arrays of components are cache-friendly. Iterating over positions is blazing fast compared to chasing pointers in OOP.

  • Optionality: Entities don’t carry useless fields (decoration has Sprite but no Health).

  • Scalability: Adding collision means making a new Collider[] array and writing a collision_system(). You don’t touch rendering or physics.

  • Flexibility: You can “compose” entities by choosing which components they get.


5. Entity Manager

You’ll want a small utility to manage free/used IDs:

c
typedef struct { int alive[MAX_ENTITIES]; } EntityManager; int create_entity(EntityManager *em) { for (int i = 0; i < MAX_ENTITIES; i++) { if (!em->alive[i]) { em->alive[i] = 1; return i; } } return -1; // full } void destroy_entity(EntityManager *em, int id) { em->alive[id] = 0; positions[id].active = 0; sprites[id].active = 0; velocities[id].active = 0; healths[id].active = 0; }

Now you’ve got:

  • A simple ID-based entity pool.

  • Components stored separately.

  • Systems that iterate and act only on relevant components.


👉 So compared to your “struct of everything in an array” approach:

  • ECS uses multiple arrays of structs instead of one big struct array.

  • It feels more complicated up front, but pays off when you add features like collision, AI, particles, etc., because you don’t have to keep bloating your Entity struct.


Would you like me to show you how to start with your “single entity struct” now, but migrate later into ECS — kind of a staged approach — so you don’t over-engineer too early?

You said:
so with an ECS done in C, with arrays of structures, I assume I have some sort of process_component function for each component?
ChatGPT said:

Exactly 👍 — you’ve got the right picture. In an ECS done in C with arrays of structs, you don’t really “process components” one at a time, but instead you write systems that iterate across the relevant component arrays.

Here’s the pattern step by step:


1. Component Arrays

Each component type is a parallel array of structs, indexed by entity ID:

c
#define MAX_ENTITIES 1024 typedef struct { int active; float x, y; } Position; typedef struct { int active; float vx, vy; } Velocity; typedef struct { int active; int spriteId; int frame; } Sprite; Position positions[MAX_ENTITIES]; Velocity velocities[MAX_ENTITIES]; Sprite sprites[MAX_ENTITIES];

2. Systems Process Components

Each system looks for entities that have all the components it cares about:

c
void physics_system(float dt) { for (int i = 0; i < MAX_ENTITIES; i++) { if (positions[i].active && velocities[i].active) { positions[i].x += velocities[i].vx * dt; positions[i].y += velocities[i].vy * dt; } } } void render_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (positions[i].active && sprites[i].active) { draw_sprite(sprites[i].spriteId, positions[i].x, positions[i].y, sprites[i].frame); } } }

So instead of process_component, you usually have physics_system(), render_system(), collision_system(), etc.

Each one loops through entities and does something only if the right components exist.


3. Why Systems Instead of “process_component”

If you tried to process one component type independently (e.g., “process positions”), you’d soon realize that meaningful game logic usually requires combinations of components:

  • Physics → Position + Velocity

  • Collision → Position + Collider

  • Rendering → Position + Sprite

  • AI → Position + Health + maybe Velocity

That’s why systems are usually defined by what components they require, not by a single component type.


4. Implementation Trick in C

Since every component has an active flag, the loop check is dead simple. But you could also:

  • Keep an array of “archetypes” (bitmasks describing which components each entity has).

  • Or keep a list of entities per system.

That’s an optimization — the brute force for (i=0; i<MAX; i++) is totally fine for small/medium games.


5. Execution Order

Your game loop might look like:

c
while (running) { input_system(); physics_system(dt); collision_system(); animation_system(dt); render_system(); }

Each system updates the relevant components, and together they form the full entity behavior.


👉 So yes: you don’t have a per-component function, but rather per-system functions that process all entities with the needed components.


Do you want me to show you how to write a tiny working C example (with one moving sprite using Position + Velocity + Sprite) so you can see the ECS pattern in action end-to-end?

You said:
I assume a factory/builder pattern would be used for creating different entity types
ChatGPT said:

Yes, exactly 👍 — a factory (or builder) pattern works really well in a C-style ECS. Since entities are just IDs plus attached components, the factory is basically a set of functions that:

  1. Reserve a new entity ID from the entity manager.

  2. Attach the right components (set their active = 1 and fill in defaults).

  3. Return the entity ID.


Example: Factory Functions in C

c
int create_player(void) { int id = create_entity(); positions[id] = (Position){ .active = 1, .x = 100, .y = 100 }; velocities[id] = (Velocity){ .active = 1, .vx = 0, .vy = 0 }; sprites[id] = (Sprite){ .active = 1, .spriteId = PLAYER_SPRITE, .frame = 0 }; healths[id] = (Health){ .active = 1, .hp = 100 }; return id; } int create_enemy(float x, float y) { int id = create_entity(); positions[id] = (Position){ .active = 1, .x = x, .y = y }; velocities[id] = (Velocity){ .active = 1, .vx = -1, .vy = 0 }; sprites[id] = (Sprite){ .active = 1, .spriteId = ENEMY_SPRITE, .frame = 0 }; healths[id] = (Health){ .active = 1, .hp = 20 }; return id; }

Each function sets up only the components needed for that entity type.

  • A decoration entity might just have Position + Sprite.

  • A projectile might have Position + Velocity + Collider.

  • A player has the whole set.


Why It’s Useful

  • Centralizes entity setup → you don’t spread initialization code all over the game.

  • Easy to add new types → just add another factory function.

  • Encapsulates defaults (e.g., enemies always start with 20 health).

  • Keeps the game loop and systems clean — they don’t worry about how entities were built.


Builder Variant

If you want configurable creation, you can go builder-style with a struct of parameters:

c
typedef struct { float x, y; int spriteId; int hp; float vx, vy; } EnemyConfig; int create_enemy(EnemyConfig cfg) { int id = create_entity(); positions[id] = (Position){ .active = 1, .x = cfg.x, .y = cfg.y }; velocities[id] = (Velocity){ .active = 1, .vx = cfg.vx, .vy = cfg.vy }; sprites[id] = (Sprite){ .active = 1, .spriteId = cfg.spriteId, .frame = 0 }; healths[id] = (Health){ .active = 1, .hp = cfg.hp }; return id; }

This gives you flexibility without cluttering the game loop.


So yes — in C ECS, factories are the glue between the entity manager and the component arrays.


Do you want me to show you how a collision system could fit neatly into this pattern (e.g., how the factory would give an entity a collider component, and then a system would use it)?

You said:
how would an observer pattern fit into this? I can never figure out how to deal with the result of entity A shooting at Entity B with a rocket, and both the rocket needing to go away, and entity B losing health, without needing to give the rocket and entity B access to each others' data
ChatGPT said:

That’s exactly the problem the Observer / Event pattern solves in a C-style ECS 👍.
You don’t want entities directly poking at each other’s internals (that couples components together). Instead, you have a neutral event system:


1. Why Use Observer / Events Here

  • Without events: The rocket’s collision system would say “I hit entity B, so I’ll delete myself and subtract B’s health.” But that means rockets need to know about health → bad coupling.

  • With events: The collision system simply says “Collision happened: rocket A hit entity B.” Then:

    • The rocket system listens for “rocket collision” and removes rocket A.

    • The health/damage system listens for “damage” and subtracts HP from B.

    • The sound system could also listen and play an explosion sound.

Now collision detection has no idea what to do with rockets or health — it just reports events.


2. Event System in C (Simple Version)

Define an enum of event types:

c
typedef enum { EVENT_NONE, EVENT_COLLISION, EVENT_DAMAGE, EVENT_ENTITY_DESTROYED } EventType;

Define a generic event struct:

c
typedef struct { EventType type; int entityA; int entityB; int data; // can store damage, score, etc. } Event;

Make a ring buffer or array of events:

c
#define MAX_EVENTS 128 Event eventQueue[MAX_EVENTS]; int eventCount = 0; void push_event(Event e) { if (eventCount < MAX_EVENTS) { eventQueue[eventCount++] = e; } }

3. Systems Publishing Events

For example, the collision system doesn’t handle the consequences itself. It just reports:

c
void collision_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (colliders[i].active && positions[i].active) { // fake check: did this collide with something? int other = check_collision(i); if (other >= 0) { push_event((Event){ .type = EVENT_COLLISION, .entityA = i, .entityB = other }); } } } }

4. Systems Subscribing / Handling Events

After all systems run, process the queue:

c
void event_system(void) { for (int i = 0; i < eventCount; i++) { Event *e = &eventQueue[i]; switch (e->type) { case EVENT_COLLISION: // Example: if A is a rocket, push a DAMAGE event for B and remove A if (rockets[e->entityA].active) { push_event((Event){ .type = EVENT_DAMAGE, .entityA = e->entityB, .data = 50 }); destroy_entity(e->entityA); } break; case EVENT_DAMAGE: if (healths[e->entityA].active) { healths[e->entityA].hp -= e->data; if (healths[e->entityA].hp <= 0) { push_event((Event){ .type = EVENT_ENTITY_DESTROYED, .entityA = e->entityA }); } } break; case EVENT_ENTITY_DESTROYED: destroy_entity(e->entityA); break; default: break; } } // Clear for next frame eventCount = 0; }

5. Flow Example (Rocket hits Enemy)

  1. Collision system sees rocket ID=12 hits enemy ID=34 → pushes EVENT_COLLISION.

  2. Event system processes: sees A=12 is a rocket → pushes EVENT_DAMAGE (to entity 34) and destroys rocket 12.

  3. Event system later sees EVENT_DAMAGE → subtracts health from entity 34.

  4. If HP ≤ 0 → pushes EVENT_ENTITY_DESTROYED.

  5. Entity system cleans up.

  6. Meanwhile, audio system could subscribe to EVENT_COLLISION and play “boom”.

The rocket never needs to know what “health” is, and the enemy never needs to know what “rockets” are.


👉 This is the Observer pattern in ECS:

  • Publishers (systems) announce events.

  • Subscribers (other systems) react to events.

  • Entities remain just data; systems remain decoupled.


Would you like me to show you a minimal working C example of this (rocket + enemy + collision → events → health reduction), or do you prefer a more high-level “how to structure it” guide for now?

You said:
with this ECS system, how would my game's entities make use of a "think" function where their "AI" happens, if there's no central store of data to represent everything? Would I have to make all the arrays of components global, or provide some intermediate system to do things on the thinking functions' behalf?
ChatGPT said:

Ah — great question. You’ve hit on the biggest mental shift when moving from “entity struct with methods” to ECS in C:

  • In a traditional OOP/game-object setup, each entity has its own think() method that can reach into its own fields and maybe poke others.

  • In ECS, entities don’t think — systems think on their behalf.


1. Where “AI thinking” lives in ECS

Instead of entity[i].think(), you write an AI system:

c
void ai_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (positions[i].active && ai[i].active) { switch (ai[i].state) { case AI_IDLE: if (player_visible(i)) { ai[i].state = AI_CHASE; } break; case AI_CHASE: velocities[i].vx = (playerX - positions[i].x) * 0.1f; velocities[i].vy = (playerY - positions[i].y) * 0.1f; break; } } } }

The “brain” for all enemies is in one place (the AI system), but each entity has just enough components (Position, Velocity, AIState) to let that brain update it.


2. How the AI System Accesses Data

You don’t give each entity a pointer to all data. Instead:

  • You make component arrays accessible at the system level.

  • Systems look up data for the entity IDs they’re iterating over.

In plain C, the most pragmatic approach is either:

  • Global component arrays (simplest, very common in small engines).

  • Or group them into a struct-of-arrays container and pass that into each system:

c
typedef struct { Position positions[MAX_ENTITIES]; Velocity velocities[MAX_ENTITIES]; AIState ai[MAX_ENTITIES]; // … add more components } Components; void ai_system(Components *c) { for (int i = 0; i < MAX_ENTITIES; i++) { if (c->ai[i].active) { // use c->positions[i], c->velocities[i], etc. } } }

That way you don’t need globals, but the systems still have a unified view.


3. If You Want “Per-Entity Think()” Style

You can simulate a per-entity think function, but it’s usually just syntactic sugar around the AI system:

  • Each entity stores a function pointer in its AI component:

c
typedef void (*ThinkFunc)(int entityId); typedef struct { int active; ThinkFunc think; int state; } AIState;
  • Then your AI system looks like:

c
void ai_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (ai[i].active && ai[i].think) { ai[i].think(i); } } }
  • The think() function itself can only work by reading/writing components of that entity ID:

c
void enemy_think(int id) { if (positions[id].x < playerX) { velocities[id].vx = 1; } else { velocities[id].vx = -1; } }

So you get a “per-entity think”, but it’s still going through the ECS store.


4. Summary of Approaches

  • Pure ECS way → AI is a system operating on all entities with an AI component.

  • Hybrid way → AI system dispatches to per-entity function pointers, which read/write components.

  • Data access → Either make arrays global (simpler) or pass around a Components container (cleaner).


👉 Since you’re coding in C, the global arrays approach is very common and not a bad starting point. Later, if you want modularity (for multiple worlds, test harnesses, etc.), you can wrap them in a container struct and pass that into systems.


Would you like me to sketch out both versions of AI in ECS (system-based vs function-pointer “think()” style) side-by-side so you can see the trade-offs more clearly?

You said:
rephrase this with a comparison with the original Doom "thinker" system. I get that I can have an "enemy_think" function, but I don't know how to decide which entities the "enemy_think" function would process.
ChatGPT said:

Perfect — Doom’s “thinker” system is a really good point of comparison, because it shows the difference between the old-school “object with a think function” model and the data-driven ECS approach.


🕹 Doom’s Thinker System

  • In Doom, every active object (mobj_t) had a function pointer: thinker_t function.

  • The game loop would walk a global linked list of thinkers and call each one’s function.

  • Each object was responsible for updating itself: position, animation, collision checks, etc.

So: object owns behavior → “I am an enemy, so my enemy_think() updates me.”


🖥 ECS Approach in C

In ECS, the roles are split differently:

  • Entities are just IDs with data in component arrays.

  • Systems own behavior, not entities.

  • Instead of a linked list of thinkers, you run ai_system(), physics_system(), render_system(), etc.

  • Each system scans for entities that have the right components, and applies logic to them.

So: system owns behavior → “I am the AI system, so I update all entities with an AI component.”


🔄 Where “enemy_think” Fits

If you want Doom-style per-entity think functions, you can still do it in ECS. But you have two ways:

  1. Pure ECS Style (recommended for C ECS)

    • No per-entity thinker function.

    • Instead, an AI system runs:

      c
      void ai_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (ai[i].active && ai[i].type == AI_ENEMY) { // enemy logic here } } }
    • The system itself decides which entities to update based on component data.

  2. Hybrid “Thinker” Style (closer to Doom)

    • Each entity’s AI component has a function pointer (think) set to enemy_think.

    • The AI system just dispatches:

      c
      void ai_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (ai[i].active && ai[i].think) { ai[i].think(i); } } }
    • enemy_think(id) then reads/writes the components for that entity (positions[id], velocities[id], etc.).


✅ Comparison

  • Doom thinkers: A single global list, each object had its own function pointer.

  • ECS systems: Global arrays per component, systems filter which entities to process.

  • Hybrid ECS thinkers: A middle ground — still run through a system loop, but entities store their own “brain” as a function pointer.


👉 The key difference is who decides what logic applies to an entity:

  • Doom: the entity itself (via its think function).

  • ECS: the system (by checking “who has Position + AI?”).

  • Hybrid: a system dispatches to entity-level think() functions, which still work via the component store.


Would you like me to show you how to implement the hybrid thinker approach in C ECS so it feels close to Doom’s model, but still keeps the benefits of component-based data?

You said:
so in an ECS, how do I say which specific entities use which specific "ai_system" function? It seems strange to have an "AI" component for each kind of entity
ChatGPT said:

That’s the heart of the ECS/A.I. design question 👌 — how do you avoid ending up with 50 different “AI components” (one per enemy type), while still letting different entities behave differently?

Here’s how ECS usually handles it:


1. One AI component type, many “brains”

Instead of making different AI components, you make one AI component that has:

  • either an enum (AI_ENEMY_BASIC, AI_ENEMY_BOSS, AI_ROCKET, …),

  • or a function pointer to the entity’s specific think/update function.

Example: enum approach

c
typedef enum { AI_NONE, AI_ENEMY_BASIC, AI_ENEMY_BOSS, AI_ROCKET } AIType; typedef struct { int active; AIType type; int state; // e.g. idle, chasing, attacking } AI;

Then in your AI system:

c
void ai_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (!ai[i].active) continue; switch (ai[i].type) { case AI_ENEMY_BASIC: enemy_basic_ai(i); break; case AI_ENEMY_BOSS: enemy_boss_ai(i); break; case AI_ROCKET: rocket_ai(i); break; default: break; } } }

Example: function pointer approach (closer to Doom)

c
typedef void (*ThinkFunc)(int id); typedef struct { int active; ThinkFunc think; } AI;

Assign at spawn time in your factory:

c
ai[id] = (AI){ .active = 1, .think = enemy_basic_ai };

And the system becomes:

c
void ai_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (ai[i].active && ai[i].think) { ai[i].think(i); } } }

This is almost exactly Doom’s “thinker” system, but the difference is that the thinker functions read/write ECS components instead of poking fields in a giant struct.


2. Why not one component per entity type?

Because components in ECS are supposed to represent kinds of data, not “roles”.

  • Position, Velocity, Health, Sprite, Collider → data.

  • “Enemy AI type 3” → behavior, which belongs in a system or function, not as a new component type.

So you keep one AI component, but parameterize it with a type enum or a function pointer.


3. Summary

  • Pure ECS style → AI component has an enum → system has a switch → logic split into functions.

  • Hybrid/Doom style → AI component has a function pointer → system just dispatches → entity “owns” its own brain.

  • Both avoid creating a new component struct for every enemy.


👉 Since you’re writing in C, the function pointer version often feels nicest — it gives you Doom-style think() functions while still keeping the data in ECS arrays.

Would you like me to sketch a mini example of an ECS with two enemy types, each using a different AI function, showing both enum and function-pointer styles side by side?

You said:
yes
ChatGPT said:

Perfect 👍 Let’s build a tiny ECS-in-C example that shows two enemies with different “brains,” using both enum-based and function-pointer-based AI.


🟢 1. Setup: Core ECS components

c
#include <stdio.h> #define MAX_ENTITIES 10 // --- Position component typedef struct { int active; float x, y; } Position; // --- AI component (two styles shown below) typedef enum { AI_NONE, AI_ENEMY_BASIC, AI_ENEMY_BOSS } AIType; typedef struct { int active; AIType type; // enum-based void (*think)(int id); // function-pointer-based } AI; // Component arrays Position position[MAX_ENTITIES]; AI ai[MAX_ENTITIES];

🟢 2. Thinker functions

c
void enemy_basic_ai(int id) { printf("[Entity %d] BASIC enemy thinks... patrols area.\n", id); position[id].x += 1; // simple behavior } void enemy_boss_ai(int id) { printf("[Entity %d] BOSS enemy thinks... summons minions!\n", id); position[id].y -= 1; // different behavior }

🟢 3. Systems

(A) Enum-driven AI system

c
void ai_system_enum(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (!ai[i].active) continue; switch (ai[i].type) { case AI_ENEMY_BASIC: enemy_basic_ai(i); break; case AI_ENEMY_BOSS: enemy_boss_ai(i); break; default: break; } } }

(B) Function-pointer-driven AI system

c
void ai_system_funcptr(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (ai[i].active && ai[i].think) { ai[i].think(i); } } }

🟢 4. Factory functions

c
int create_basic_enemy(int id, float x, float y) { position[id] = (Position){ .active = 1, .x = x, .y = y }; ai[id] = (AI){ .active = 1, .type = AI_ENEMY_BASIC, .think = enemy_basic_ai }; return id; } int create_boss_enemy(int id, float x, float y) { position[id] = (Position){ .active = 1, .x = x, .y = y }; ai[id] = (AI){ .active = 1, .type = AI_ENEMY_BOSS, .think = enemy_boss_ai }; return id; }

🟢 5. Main loop demo

c
int main(void) { create_basic_enemy(0, 10, 10); create_boss_enemy(1, 50, 50); printf("=== Enum-based AI ===\n"); ai_system_enum(); printf("\n=== Function-pointer AI ===\n"); ai_system_funcptr(); return 0; }

🟢 Example Output

mathematica
=== Enum-based AI === [Entity 0] BASIC enemy thinks... patrols area. [Entity 1] BOSS enemy thinks... summons minions! === Function-pointer AI === [Entity 0] BASIC enemy thinks... patrols area. [Entity 1] BOSS enemy thinks... summons minions!

✅ The enum system is more “data-driven” — your AI system knows about all AI types and dispatches them.
✅ The function-pointer system is closer to Doom’s thinker model — entities carry their own behavior.

Both approaches keep your data in ECS arrays while still letting different entities think differently.


Would you like me to expand this into a mini game loop where the enemies move around and we check collisions, so you can see how ECS + AI + collisions actually flow together?

You said:
yes
ChatGPT said:

Great 😃 Let’s wire this up into a mini ECS game loop in C, with:

  • Position (where entities are)

  • Velocity (so enemies can move)

  • AI (brains — function pointer style, like Doom’s thinker system)

  • Health (so enemies can take damage)

  • Collision system (rockets hit enemies → rocket destroyed, enemy loses health)


🟢 Full Example

c
#include <stdio.h> #include <stdbool.h> #define MAX_ENTITIES 16 // ======================== // COMPONENTS // ======================== typedef struct { bool active; float x, y; } Position; typedef struct { bool active; float dx, dy; } Velocity; typedef struct { bool active; int hp; } Health; typedef void (*ThinkFunc)(int id); typedef struct { bool active; ThinkFunc think; } AI; // ======================== // COMPONENT ARRAYS // ======================== Position position[MAX_ENTITIES]; Velocity velocity[MAX_ENTITIES]; Health health[MAX_ENTITIES]; AI ai[MAX_ENTITIES]; // ======================== // HELPERS // ======================== void kill_entity(int id) { position[id].active = false; velocity[id].active = false; health[id].active = false; ai[id].active = false; printf("Entity %d destroyed!\n", id); } // ======================== // THINK FUNCTIONS // ======================== void enemy_basic_ai(int id) { printf("[Entity %d] BASIC AI wandering...\n", id); velocity[id].dx = 1; // just walks right velocity[id].dy = 0; } void rocket_ai(int id) { printf("[Entity %d] ROCKET flying...\n", id); // rockets already have velocity set, so nothing here } // ======================== // SYSTEMS // ======================== void ai_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (ai[i].active && ai[i].think) { ai[i].think(i); } } } void movement_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (position[i].active && velocity[i].active) { position[i].x += velocity[i].dx; position[i].y += velocity[i].dy; } } } void collision_system(void) { for (int a = 0; a < MAX_ENTITIES; a++) { if (!position[a].active || !health[a].active) continue; for (int b = 0; b < MAX_ENTITIES; b++) { if (a == b) continue; if (!position[b].active) continue; // Simple collision: same x,y if ((int)position[a].x == (int)position[b].x && (int)position[a].y == (int)position[b].y) { printf("Entity %d hit Entity %d!\n", b, a); // If rocket hits enemy if (ai[b].think == rocket_ai) { health[a].hp -= 10; kill_entity(b); // rocket destroyed if (health[a].hp <= 0) { kill_entity(a); } } } } } } // ======================== // FACTORIES // ======================== int create_basic_enemy(int id, float x, float y) { position[id] = (Position){ true, x, y }; velocity[id] = (Velocity){ true, 0, 0 }; health[id] = (Health){ true, 20 }; ai[id] = (AI){ true, enemy_basic_ai }; return id; } int create_rocket(int id, float x, float y, float dx, float dy) { position[id] = (Position){ true, x, y }; velocity[id] = (Velocity){ true, dx, dy }; ai[id] = (AI){ true, rocket_ai }; return id; } // ======================== // MAIN LOOP DEMO // ======================== int main(void) { create_basic_enemy(0, 5, 5); create_rocket(1, 0, 5, 2, 0); // rocket flies right for (int frame = 0; frame < 10; frame++) { printf("\n--- Frame %d ---\n", frame); ai_system(); movement_system(); collision_system(); // Debug print positions for (int i = 0; i < 2; i++) { if (position[i].active) printf("Entity %d at (%.1f, %.1f)\n", i, position[i].x, position[i].y); } } return 0; }

🟢 Sample Output (trimmed)

mathematica
--- Frame 0 --- [Entity 0] BASIC AI wandering... [Entity 1] ROCKET flying... Entity 0 at (6.0, 5.0) Entity 1 at (2.0, 5.0) --- Frame 2 --- Entity 1 hit Entity 0! Entity 1 destroyed! Entity 0 at (8.0, 5.0) --- Frame 3 --- [Entity 0] BASIC AI wandering... Entity 0 at (9.0, 5.0)

🟢 What this shows

  • Enemies move because their AI thinker sets velocity.

  • Rockets fly using velocity directly.

  • When rocket and enemy overlap → rocket destroyed, enemy takes damage.

  • AI, position, velocity, and health are all in ECS-style arrays.


👉 This is very close to Doom’s thinker list, but instead of a linked list, we’re using ECS arrays, which are cache-friendly and simpler in C.

Would you like me to extend this with multiple enemy types (e.g. a boss that fires back rockets), so you can see how the function-pointer AI scales when behaviors diverge?

You said:

while (running) { input\_system(); physics\_system(dt); collision\_system(); animation\_system(dt); render\_system(); }

In my ECS system, how would I create a "is controlled by the human player with a keyboard" system? And then how would I attach that to a specific entity id?
ChatGPT said:

Good question 👍 — adding player control in an ECS is very natural once you see the pattern. It works the same way as AI, physics, or rendering:


🟢 Step 1. Define a PlayerControl component

This marks which entity is “human controlled” and can hold some player-specific state if needed.

c
typedef struct { bool active; int player_id; // useful if you ever want 2P local multiplayer } PlayerControl; PlayerControl player_control[MAX_ENTITIES];

🟢 Step 2. Input system

This is a system that:

  1. Reads input (keyboard, controller, etc.).

  2. Finds all entities with a PlayerControl component.

  3. Applies the input to their other components (usually Velocity, sometimes Action, Weapon, etc.).

c
void input_system(void) { // Example: WASD controls bool left = key_pressed('A'); bool right = key_pressed('D'); bool up = key_pressed('W'); bool down = key_pressed('S'); for (int i = 0; i < MAX_ENTITIES; i++) { if (player_control[i].active && velocity[i].active) { velocity[i].dx = (right - left) * 2.0f; // move faster if right pressed velocity[i].dy = (down - up) * 2.0f; } } }

👉 In a real engine, key_pressed() would be implemented using SDL, GLFW, or your own input layer.


🟢 Step 3. Attaching to an entity

Just like any other component — in your factory function:

c
int create_player(int id, float x, float y) { position[id] = (Position){ true, x, y }; velocity[id] = (Velocity){ true, 0, 0 }; health[id] = (Health){ true, 100 }; player_control[id] = (PlayerControl){ true, 0 }; // "0" = player 1 return id; }

Now entity id will automatically respond to input because the input_system() looks for player_control[id].active.


🟢 Step 4. Game loop integration

Your main loop just needs to call the input_system() before movement/physics:

c
while (running) { input_system(); // player input → velocity ai_system(); // AI sets velocity for enemies movement_system(dt); // apply velocity → position collision_system(); // resolve collisions animation_system(dt); // advance animations render_system(); // draw to screen }

Notice that player and AI both set velocity, but through different systems. Physics doesn’t care how velocity was decided — it just applies it.


✅ This keeps the design modular:

  • Swap in different input systems (keyboard, gamepad, network) without touching physics or AI.

  • Multiple players are just more entities with PlayerControl.

  • AI and players “compete” fairly, since they’re both systems writing into the same component space.


Would you like me to show how you could later extend this so that the same entity can be controlled either by AI or by a human, just by toggling one component?

You said:
I have a "sprite definition" struct that I fill in for each entity type. This struct records the collision bounds, internal graphics ID, and other common data for each type of entity. I was planning on making some sort of "core data" component to store a pointer to this in, along with any other "global" data I might need for each entity. Is this a sensible idea
ChatGPT said:

Yes 👍 that’s actually a very common and sensible pattern in ECS-style engines — what you’re describing is essentially a prototype / archetype / prefab record for an entity type, and having entities point to it is both memory-efficient and keeps your logic clean.


🟢 Why it makes sense

  • Avoids duplication: Instead of storing bounds, sprite IDs, etc. in every entity, you define them once in the SpriteDef (or whatever you call it).

  • Consistency: If you tweak the hitbox of “Enemy Type A,” every entity of that type automatically uses the new data.

  • Memory layout stays lean: Entity component arrays only store per-instance data (position, velocity, health). All the shared data lives in one const table.

  • Easy spawning: Your factory just attaches the correct “core data” pointer to the new entity.


🟢 What it looks like in C

1. Sprite definition (shared)

c
typedef struct { const char *name; int sprite_id; int width, height; int hitbox_x, hitbox_y, hitbox_w, hitbox_h; int max_health; // you can add more (speed, sound IDs, etc.) } SpriteDef;

2. Core data component (per entity)

c
typedef struct { bool active; const SpriteDef *def; // pointer to shared definition } CoreData; CoreData core[MAX_ENTITIES];

3. Definition table (global, read-only)

c
const SpriteDef SPRITES[] = { { "Player", 1, 16, 16, 2, 2, 12, 12, 100 }, { "Enemy", 2, 16, 16, 0, 0, 16, 16, 20 }, { "Rocket", 3, 8, 8, 0, 0, 8, 8, 1 }, };

4. Factory uses it

c
int create_enemy(int id, float x, float y) { core[id] = (CoreData){ true, &SPRITES[1] }; // enemy def position[id] = (Position){ true, x, y }; velocity[id] = (Velocity){ true, 0, 0 }; health[id] = (Health){ true, core[id].def->max_health }; return id; }

5. Usage in systems

c
void render_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (position[i].active && core[i].active) { int sprite_id = core[i].def->sprite_id; draw_sprite(sprite_id, position[i].x, position[i].y); } } } void collision_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (!position[i].active || !core[i].active) continue; const SpriteDef *def = core[i].def; // use def->hitbox_* for collision } }

🟢 Extra considerations

  • Treat your SpriteDef (or EntityDef) table as read-only. That way you can safely share one copy across all entities.

  • You can extend it beyond graphics: sounds, AI type, default physics properties, etc.

  • If you want modding or data-driven content later, this structure can come from JSON, XML, or your own binary format.


✅ So yes — having a “core data” component that stores a pointer to a shared definition struct is a very sensible approach in C. It’s basically data-oriented prefab design.

Do you want me to also show how this fits with the AI system we built earlier — e.g. storing the function pointer for the entity’s “think” function inside the SpriteDef, so factories don’t need to hardcode it?

You said:

### Enum-driven AI system c Copy code `void ai_system_enum(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (!ai[i].active) continue; switch (ai[i].type) { case AI_ENEMY_BASIC: enemy_basic_ai(i); break; case AI_ENEMY_BOSS: enemy_boss_ai(i); break; default: break; } } }`

If I was to use an enum driven "AI" system, with separate functions for each type of enemy/whatever, where do I store state data for each entity instance? If I'm not careful I could end up with a bunch of random data stored in the "core data" struct, or a generic "AI" struct with everything in it. Or loads of arrays
ChatGPT said:

Yeah — you’ve hit the crux of the enum-driven AI approach: if you’re not careful, you either:

  • put all possible state for all AI types into one mega AI component,

  • or scatter it into lots of little parallel arrays that don’t feel coherent.

Here are the usual approaches in C:


🟢 1. Minimal “AI” component + type-specific state arrays

Keep the AI component light — just the type enum and maybe a pointer/index into type-specific state storage.

c
typedef enum { AI_NONE, AI_ENEMY_BASIC, AI_ENEMY_BOSS, AI_ROCKET } AIType; typedef struct { bool active; AIType type; } AI; AI ai[MAX_ENTITIES];

Then you keep separate state arrays for each type:

c
typedef struct { int patrol_dir; int cooldown; } EnemyBasicState; typedef struct { int phase; int rage_level; } EnemyBossState; EnemyBasicState enemy_basic_state[MAX_ENTITIES]; EnemyBossState enemy_boss_state[MAX_ENTITIES];

In the system:

c
void enemy_basic_ai(int id) { EnemyBasicState *s = &enemy_basic_state[id]; // read/write state here } void enemy_boss_ai(int id) { EnemyBossState *s = &enemy_boss_state[id]; }

👉 This avoids bloating a single AI component, but you do get multiple arrays. That’s not necessarily a problem in ECS — it’s the intended pattern.


🟢 2. Tagged union inside the AI component

If you don’t like lots of arrays, you can use a union for type-specific state, discriminated by the enum:

c
typedef struct { int patrol_dir; int cooldown; } EnemyBasicState; typedef struct { int phase; int rage_level; } EnemyBossState; typedef struct { bool active; AIType type; union { EnemyBasicState basic; EnemyBossState boss; } state; } AI;

Downside: all entities carry enough memory for the largest state struct. Upside: everything is in one place.


🟢 3. “CoreData” points to default values, AI points to per-instance state

Your CoreData / SpriteDef idea can store static defaults (like patrol speed, aggression range), and the AI component only tracks dynamic per-entity state (timers, cooldowns, phases).

c
typedef struct { const SpriteDef *def; // static data int ai_state; // current state (e.g., idle, chasing, attacking) int timer; // general-purpose counter } AI;

That way you don’t need a big state bag — just minimal “current mode” + timer/counter variables, and read constants from the definition struct.


🟢 4. Function-pointer + closure-like state (hybrid)

Even if you use enums for dispatch, you can also store a function pointer for AI update plus a void * to per-instance state.

c
typedef void (*AIUpdate)(int id, void *state); typedef struct { bool active; AIUpdate fn; void *state; } AI;

Then each entity can malloc its own state struct of the right shape.
This gives you maximum flexibility, but more dynamic memory management.


🔑 Rule of thumb

  • If state is small and generic → put it in the AI component directly.

  • If state is big but type-specific → use per-type arrays, indexed by entity ID.

  • If you want maximum flexibility → store void * state and cast appropriately.


👉 In C, the most “ECS-ish” option is (1): multiple state arrays keyed by entity ID. Yes, it feels like “loads of arrays,” but that’s the tradeoff you make to keep memory tight and data cache-friendly. Doom itself basically did this with per-thinker structs in linked lists.


Do you want me to sketch out a concrete worked example where we add per-instance patrol state for a “basic enemy” without bloating the AI component? That way you can see what the “loads of arrays” version looks like in practice.

You said:
with all these arrays, where should I place them? I am imagining an "entitymanager.c" file with the implementation, and maybe a bunch of separate "ai_system.c" and "render_system.c" files to separate each component into its own C file. It seems the component processing functions need to see all the arrays to function, so where should they go. Help me organise the overall layout of my source code tree
ChatGPT said:

That’s a really good question — and you’re not alone, lots of people bump into this when doing ECS in C. You’ve basically discovered the tension between modularity and data locality:

  • Each system needs access to one or more component arrays.

  • You don’t want to scatter globals everywhere, or jam everything into one entitymanager.c.

Here’s a sane way to organize things in plain C, without going overboard:


🟢 1. Component data in components/

Each component gets its own .h with its struct definition and global array declaration.

css
src/ components/ position.h velocity.h health.h ai.h coredata.h

Example components/position.h:

c
#ifndef POSITION_H #define POSITION_H #include <stdbool.h> #define MAX_ENTITIES 1024 typedef struct { bool active; float x, y; } Position; extern Position position[MAX_ENTITIES]; #endif

And in components/position.c:

c
#include "position.h" Position position[MAX_ENTITIES];

🟢 2. Systems in systems/

Each system lives in its own .c file and includes the headers of the components it needs.

css
src/ systems/ movement_system.c render_system.c ai_system.c input_system.c collision_system.c

Example systems/movement_system.c:

c
#include "position.h" #include "velocity.h" void movement_system(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (position[i].active && velocity[i].active) { position[i].x += velocity[i].dx; position[i].y += velocity[i].dy; } } }

🟢 3. Entity manager in entity/

This is where you put factory functions, destruction helpers, and general bookkeeping.

css
src/ entity/ entity_manager.c entity_manager.h

Example entity_manager.c:

c
#include "position.h" #include "velocity.h" #include "health.h" #include "ai.h" #include "coredata.h" int create_enemy(int id, float x, float y) { position[id] = (Position){ true, x, y }; velocity[id] = (Velocity){ true, 0, 0 }; health[id] = (Health){ true, 20 }; ai[id] = (AI){ true, AI_ENEMY_BASIC }; return id; }

🟢 4. Main loop in game.c

This pulls everything together, calling each system in order:

c
#include "movement_system.h" #include "ai_system.h" #include "render_system.h" #include "input_system.h" #include "collision_system.h" #include "entity_manager.h" int main(void) { int player = create_player(0, 10, 10); int enemy = create_enemy(1, 20, 10); while (running) { input_system(); ai_system(); movement_system(); collision_system(); render_system(); } }

🟢 5. Source tree overview

css
src/ components/ position.h / position.c velocity.h / velocity.c health.h / health.c ai.h / ai.c coredata.h / coredata.c systems/ movement_system.c render_system.c ai_system.c input_system.c collision_system.c entity/ entity_manager.c entity_manager.h game.c (main loop)

🟢 Notes on style

  • Yes, components are global arrays, but declared in one .c file and exposed with extern in .h. That keeps them consistent and avoids copy/paste declarations.

  • Each system includes only the headers for the components it needs. No one needs to know about all components.

  • entity_manager.c is the only place that needs to know about all components, because it’s where you construct/destroy entities.

  • You can always group small components into one file if it feels like too many .c files.


✅ This gives you:

  • Clear separation (components vs systems vs entity factories).

  • Easy scaling when you add new systems/components.

  • Still simple C (just headers + globals).


Would you like me to show how this layout would change if you wanted to hide the global arrays completely (e.g. wrap them in an EntityWorld struct instead of globals)? That’s another common step once projects grow.

You said:

int create\_enemy(int id, float x, float y) { position\[id\] = (Position){ true, x, y }; velocity\[id\] = (Velocity){ true, 0, 0 }; health\[id\] = (Health){ true, 20 }; ai\[id\] = (AI){ true, AI\_ENEMY\_BASIC }; return id; }

This function should probably really find the next available ID, rather than having me choose one
ChatGPT said:

Exactly 👍 — right now you’re basically “manually allocating” IDs, which isn’t very ECS-ish. It’s more natural to let the entity manager hand out IDs by finding the next free slot.

Here’s one simple way to do it:


entity_manager.c

c
#include "entity_manager.h" #include "position.h" #include "velocity.h" #include "health.h" #include "ai.h" #include "coredata.h" static bool entity_used[MAX_ENTITIES] = {false}; int create_entity(void) { for (int i = 0; i < MAX_ENTITIES; i++) { if (!entity_used[i]) { entity_used[i] = true; return i; } } return -1; // no free slots } void destroy_entity(int id) { if (id < 0 || id >= MAX_ENTITIES) return; entity_used[id] = false; // mark all components inactive position[id].active = false; velocity[id].active = false; health[id].active = false; ai[id].active = false; coredata[id].active = false; } int create_enemy(float x, float y) { int id = create_entity(); if (id == -1) return -1; position[id] = (Position){ true, x, y }; velocity[id] = (Velocity){ true, 0, 0 }; health[id] = (Health){ true, 20 }; ai[id] = (AI){ true, AI_ENEMY_BASIC }; return id; }

entity_manager.h

c
#ifndef ENTITY_MANAGER_H #define ENTITY_MANAGER_H #define MAX_ENTITIES 1024 int create_entity(void); void destroy_entity(int id); int create_enemy(float x, float y); int create_player(float x, float y); #endif

✅ This way:

  • Entity IDs are auto-assigned from the pool.

  • You can still destroy them and recycle IDs.

  • All your factories (create_enemy, create_player, etc.) just call create_entity() internally.


Would you like me to also show how to make the systems skip inactive entities automatically (so you don’t have to keep checking active flags everywhere)?

You said:

Minimal “AI” componen

I want a better term than "AI" to describe the unique logic each entity type will run
ChatGPT said:

Yeah, “AI” feels too narrow — because not every entity with special per-tick logic is really doing “artificial intelligence.” Even in Doom, the “thinkers” covered everything from doors to monsters.

Here are some alternatives that work better for a generic ECS:

  • Thinker – classic Doom term, captures “this entity thinks once per tick.”

  • Brain – playful but intuitive: each entity can have a brain module.

  • Logic – plain and accurate: the entity’s per-frame logic.

  • Behavior – good middle ground: describes what the entity does.

  • Script – if you want to hint that entity logic is data-driven or externally defined.

  • Controller – emphasizes that this drives the entity (could be AI, player input, etc.).

  • Update – neutral, just “this runs each tick.”

👉 For a C-based ECS that will also cover things like projectiles, triggers, or items, I’d suggest Behavior or Thinker. They’re flexible and not misleading.

Do you want me to sketch how your current AI component and system would look if you rename it to, say, Behavior — including how multiple behavior types fit in?

You said:

if (ai\[i\].active

With the component system, there's a lot of this code everywhere, to check the system is processing the relevant entities. Can I somehow turn this into a preprocessor macro statement, so I don't have to type the code out all the time, and can instead have something like the following code hidden behind a macro neatly, so it is obvious what's going on if (!ai[i].active) continue; but for all components, supplying them to the macro as a parameter. I can then write this at the start CONTINUE_IF_INACTIVE_COMPONENT(ai) CONTINUE_IF_INACTIVE_COMPONENT(velocities) etc, but with a better name. Also I feel compelled to put a ; at the end of the macro, but it's redundant and I feel I'm breaking some unwritten C rule by trying to do this at all
ChatGPT said:

That’s a neat instinct — you’ve spotted the repetition, and in an ECS that can get really noisy fast. You can absolutely wrap those “skip if inactive” checks in macros, and you’re not breaking a C rule by doing it. The ; thing is a stylistic quirk: the common idiom is to write macros that expand to a single statement (often wrapped in a do { … } while(0)), so they behave consistently whether or not you put a semicolon after them.


Basic form

c
#define CONTINUE_IF_INACTIVE(comp, i) \ if (!(comp)[(i)].active) continue

Usage:

c
for (int i = 0; i < MAX_ENTITIES; i++) { CONTINUE_IF_INACTIVE(ai, i); CONTINUE_IF_INACTIVE(velocity, i); // now safe to use ai[i] and velocity[i] }

Here the semicolon after each call is natural, because the macro expands to a single if (...) continue.


“Nicer” form with do { } while (0)

This trick ensures the macro behaves like a statement block, so you can always write ; safely:

c
#define CONTINUE_IF_INACTIVE(comp, i) \ do { if (!(comp)[(i)].active) continue; } while (0)

Usage is the same:

c
CONTINUE_IF_INACTIVE(ai, i);

Multiple components in one go

If you often need all of them active, you could make a variadic macro that chains them:

c
#define CONTINUE_IF_INACTIVE_2(c1, c2, i) \ do { if (!(c1)[(i)].active || !(c2)[(i)].active) continue; } while (0) #define CONTINUE_IF_INACTIVE_3(c1, c2, c3, i) \ do { if (!(c1)[(i)].active || !(c2)[(i)].active || !(c3)[(i)].active) continue; } while (0)

Example:

c
for (int i = 0; i < MAX_ENTITIES; i++) { CONTINUE_IF_INACTIVE_2(ai, velocity, i); // both are active }

C macros aren’t very expressive, so you usually make a small family of them (_2, _3, _4) for common cases.


Naming

Something clearer than CONTINUE_IF_INACTIVE might be:

  • REQUIRE_COMPONENT(comp, i)

  • SKIP_IF_MISSING(comp, i)

  • ONLY_IF_ACTIVE(comp, i)


👉 Want me to sketch a ready-to-drop-in header file (component_macros.h) with _2, _3, _4 variants and good naming so you can just include it and avoid boilerplate everywhere?


ChatGPT can make mistakes. Check important info. See Cookie Preferences.