Panda3d & EnTT ECS work well together

Panda3d and the EnTT Entity Component System play nicely together out-of-the-box, offering a nice ECS solution for C++ Panda developers.

Working from UI to rendering, we begin with a standard textured Pgui button:

PT(PGButton) node = new PGButton(node_name);
node->setup("", 0.1);

// Load textures
PT(Texture) button_ready = TexturePool::load_texture("ui/play_button.png");
PT(Texture) button_rollover = TexturePool::load_texture("ui/play_button_active.png");
PT(Texture) button_pressed = TexturePool::load_texture("ui/play_button_pressed.png");
PT(Texture) button_inactive = TexturePool::load_texture("ui/play_button_inactive.png");

// Get default button styling
PGFrameStyle MyStyle=node->get_frame_style(0); // frame_style(0): ready state

// Apply textures
MyStyle.set_texture(button_ready);    node->set_frame_style(0, MyStyle);
MyStyle.set_texture(button_rollover); node->set_frame_style(1, MyStyle);
MyStyle.set_texture(button_pressed);  node->set_frame_style(2, MyStyle);
MyStyle.set_texture(button_inactive); node->set_frame_style(3, MyStyle);

// Attach to UI
NodePath node_path = framework.get_window(target_window)->get_aspect_2d().attach_new_node(node);
node_path.set_transparency(TransparencyAttrib::M_alpha);  // Enable texture alpha blending

framework.define_key(node->get_click_event(MouseButton::one()), "button press",
                         &Callback_PlayButtonClicked, play_button_node);

We’ll need a variable to control if the game is running or not:

inline static bool game_running = true;
inline static double game_time_elapsed = 0.0;

Here’s the UI callback function:

void Window_PlayGame::Callback_PlayButtonClicked(const Event *event, void *data){
    game_running = !game_running;

We need to call our ECS systems each frame to manage our entities. So we’ll use the Panda task system. Note that our function only calls the EnTT systems if our game is running:

AsyncTask::DoneStatus Window_PlayGame::Task_EntityProcessors(GenericAsyncTask *task, void *data) {
    if(!game_running) return AsyncTask::DS_cont;  // No game processing happens while the clock is stopped
    StructureProcessor::Update(window, registry);
    return AsyncTask::DS_cont;

A useful pattern is to set up factory methods for your ECS entities:

entt::entity EntityFactory::GoatPen_1_1(WindowFramework *window, glm::ivec2 location, CardinalDirection facing_direction) {
    entt::entity entity = registry.create();

    // It's a structure
    registry.emplace<StructureComponent>(entity, 1, 1, facing_direction);

    // At a location
    registry.emplace<GridLocationComponent>(entity, location.x, location.y);

    // The goats will need to eat. We'll put their food in a container.
    registry.emplace<StorageComponent>(entity, 500.0f, 1.0f, entity);

    // And they need space to wander. Define pasture space.
    registry.emplace<PastureComponent>(entity, 100.0f, entity);

    // Panda render node
    NodePath node_path = window->load_model(window->get_panda_framework()->get_models(), "models/fence_square_1_1.bam");
    node_path.set_pos(location.x * Tile::tile_width, location.y * Tile::tile_width, 0);
    node_path.reparent_to(window->get_render());  // Everything under render is visible

    registry.emplace<PandaNodePathComponent>(entity, node_path);
    return entity;

And instantiate a game-world entity:

entt::entity goat_pen = entity_factory.GoatPen_1_1(window, glm::ivec2(1, 2), CardinalDirection::NORTH);

The EnTT/Panda component just holds the node path:

#include <pandaFramework.h>

struct PandaNodePathComponent {
    NodePath node_path;

Here’s the structure processing system. We’re going to spin it around for this demo:

void StructureProcessor::Update(WindowFramework *window, entt::registry &registry) {
    PT(ClockObject) globalClock = ClockObject::get_global_clock();

    auto view = registry.view<StructureComponent>();

    // For every entity that has a structure component
    for(auto entity:view){
        auto panda = registry.get<PandaNodePathComponent>(entity);
        LVector3 hpr = panda.node_path.get_hpr();

        hpr = LVector3(hpr.get_x() + 15 * globalClock->get_dt(), hpr.get_y(), hpr.get_z());
        if (hpr.get_x() > 360) hpr.set_x(hpr.get_x() - 360);

Later on we can expand this component and system to take advantage of instancing or other panda tools. We’ve got got access to the entire Panda framework through the *window.