# Extensibility Patterns in ArmorPaint: How `trait.c` Enables Modular Plugin Architecture

> Explore ArmorPaint's extensibility patterns using trait c. Learn how function pointers enable modular plugin architecture for adding gameplay mechanics without core code changes.

- Repository: [Armory 3D/armorpaint](https://github.com/armory3d/armorpaint)
- Tags: internals
- Published: 2026-09-14

---

**ArmorPaint implements a registry-based plugin architecture through [`trait.c`](https://github.com/armory3d/armorpaint/blob/main/trait.c) that enables dynamic attachment of behaviors via function pointers, allowing developers to add gameplay mechanics without modifying core engine code.**

ArmorPaint's extensibility relies on a lightweight trait system that decouples gameplay behaviors from the engine loop. By utilizing static registration tables and function pointer dispatch in [`paint/sources/trait.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/trait.c), the system allows modules like [`trait_point_and_click_controller.c`](https://github.com/armory3d/armorpaint/blob/main/trait_point_and_click_controller.c) to integrate seamlessly while maintaining clean separation of concerns.

## The Registry-Based Plugin Architecture

The core extensibility mechanism centers on a statically defined registry that maps string identifiers to lifecycle function groups. This pattern eliminates hard-coded dependencies between the engine main loop and specific gameplay features.

### Static Trait Table and Function Pointer Interface

In [`paint/sources/trait.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/trait.c), the system defines a uniform contract that all traits must implement:

```c
typedef void (*trait_init_t)(char *object);
typedef void (*trait_run_t)(void);
typedef void (*trait_stop_t)(void);

typedef struct trait {
    char *name;
    trait_init_t init;
    trait_run_t run;
    trait_stop_t stop;
} trait_t;

```

The global registry `static trait_t traits[]` at lines 15-18 holds descriptors for every available trait, each containing a name and three function pointers. Adding a new trait requires only appending an entry to this array; the engine core never needs recompilation to recognize new behaviors.

### Dynamic Dispatch Through Function Pointers

ArmorPaint achieves **dynamic dispatch** at runtime by invoking behaviors through function pointers stored in the registry. When `script_add_trait("object_name", "trait_name")` is called, the system looks up the name in `traits[]` and executes the corresponding `init` pointer. This decouples the engine from concrete implementations while guaranteeing all traits expose the same lifecycle interface.

## Lifecycle Management and Idempotent Attachment

Safe composition requires preventing duplicate initialization when scripts attach traits multiple times.

### Attachment Flag Array

The `static bool trait_attached[TRAIT_COUNT];` array at lines 22-33 tracks which traits are active on specific objects. Before calling `init`, the system checks this flag, ensuring idempotent behavior—calling `script_add_trait` twice silently succeeds the second time without side effects.

### Centralized Update Loops

The engine invokes `trait_update()` and `trait_stop()` functions (lines 39-53) to manage execution. These iterate over the registry, invoking `run` callbacks only for attached traits:

```c
void trait_update(void) {
    for (int i = 0; i < TRAIT_COUNT; i++) {
        if (trait_attached[i] && traits[i].run != NULL) {
            traits[i].run();
        }
    }
}

```

This centralized dispatch keeps the main game loop clean; it only needs to call `trait_update()` each frame rather than managing individual behavior states.

## Concrete Implementation: [`trait_point_and_click_controller.c`](https://github.com/armory3d/armorpaint/blob/main/trait_point_and_click_controller.c)

The file [`paint/sources/traits/trait_point_and_click_controller.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/traits/trait_point_and_click_controller.c) demonstrates how complex subsystems conform to the trait interface. This controller implements A*-style pathfinding and mouse picking while exposing only the standard lifecycle functions.

### Internal State Encapsulation

Despite containing rich internal logic—pathfinding data, movement flags, and position tracking—the controller exposes only three public functions matching the `trait_t` signature: `trait_point_and_click_controller_init`, `trait_point_and_click_controller_run`, and `trait_point_and_click_controller_stop`. Static helper functions (`pac_*`) encapsulate the pathfinding algorithm, keeping the public trait surface minimal.

### Callback Pattern for Completion

At lines 25-30, the controller implements a lightweight observer mechanism through function pointer storage:

```c
void *on_arrive;

void trait_point_and_click_controller_arrive(void (*callback)()) {
    on_arrive = callback;
}

```

When the character reaches its destination, the controller invokes `on_arrive`, allowing external scripts to react to state changes without coupling to the internal movement logic.

## Implementation Guide: Adding a New Trait

Creating a new behavior requires three steps following the established extensibility pattern:

**1. Implement lifecycle functions** in a new file (e.g., [`fly_controller.c`](https://github.com/armory3d/armorpaint/blob/main/fly_controller.c)):

```c
void trait_fly_controller_init(char *object) {
    // Setup flight physics for object
}

void trait_fly_controller_run(void) {
    // Update position each frame
}

void trait_fly_controller_stop(void) {
    // Cleanup resources
}

```

**2. Register in the trait table** in [`paint/sources/trait.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/trait.c):

```c
static trait_t traits[] = {
    {"point_and_click_controller", 
     trait_point_and_click_controller_init,
     trait_point_and_click_controller_run,
     trait_point_and_click_controller_stop},
    {"fly_controller", 
     trait_fly_controller_init,
     trait_fly_controller_run,
     trait_fly_controller_stop},
};

```

**3. Attach via script**:

```c
// Attach to an object
script_add_trait("player", "fly_controller");

```

## Summary

- **Registry-based architecture**: The `traits[]` table in [`trait.c`](https://github.com/armory3d/armorpaint/blob/main/trait.c) provides a central catalog of behaviors using function pointers for decoupled integration.
- **Idempotent attachment**: The `trait_attached` array prevents duplicate initialization, ensuring safe dynamic composition at runtime.
- **Uniform lifecycle**: All traits implement `init`, `run`, and `stop` functions, enabling generic dispatch through `trait_update()` and `trait_stop()`.
- **Callback hooks**: Concrete implementations like [`trait_point_and_click_controller.c`](https://github.com/armory3d/armorpaint/blob/main/trait_point_and_click_controller.c) store function pointers to notify external code of state changes without exposing internals.
- **Zero core modification**: New traits require only table registration and implementation file addition, never touching the engine's main loop.

## Frequently Asked Questions

### How does ArmorPaint prevent duplicate trait initialization on the same object?

The system uses a `static bool trait_attached[TRAIT_COUNT]` array to track which traits are active on specific objects. When `script_add_trait` is called, it checks this flag before invoking the `init` function, making the operation idempotent—subsequent calls with the same trait and object combination silently succeed without re-initialization.

### What is the performance overhead of the trait dispatch system?

The overhead consists of a single array iteration in `trait_update()` combined with a boolean check per trait. Because the `traits[]` table is static and `TRAIT_COUNT` is known at compile time, the compiler can optimize the loop aggressively. The function pointer indirection adds minimal cost compared to direct calls while providing significant architectural flexibility.

### Can traits communicate with each other or share state?

While the base [`trait.c`](https://github.com/armory3d/armorpaint/blob/main/trait.c) system does not implement direct inter-trait messaging, individual traits can expose callback registration functions like `trait_point_and_click_controller_arrive`. Objects can store shared state in their own data structures, and traits can access object-specific data through the `char *object` parameter passed to `init` functions.

### Is it possible to remove or swap traits at runtime?

The current implementation in [`trait.c`](https://github.com/armory3d/armorpaint/blob/main/trait.c) supports stopping traits via `trait_stop()`, which iterates through attached traits and invokes their `stop` callbacks. However, removing a trait from the attachment array would require additional logic to unset the corresponding `trait_attached` index, which developers can extend following the existing pattern used for attachment.