1 Proposal-Next-Gen-Grid-Entity-System
John McCardle edited this page 2025-10-25 22:52:39 +00:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Proposal: Next-Generation Grid & Entity System

Status: Design Phase
Complexity: Major architectural overhaul
Impact: Grid System, Entity Management, Performance

Related Pages:

Source Documents:

  • NEXT_GEN_GRIDS_ENTITIES_SHORTCOMINGS.md - Analysis of current limitations
  • NEXT_GEN_GRIDS_ENTITIES_PROPOSAL.md - Detailed technical proposal
  • NEXT_GEN_GRIDS_ENTITIES_IDEATION.md - Use cases and ideation

Related Issues:

  • #115 - SpatialHash for 10,000+ entities
  • #116 - Dirty flag system
  • #113 - Batch operations
  • #117 - Memory pool
  • #123 - Subgrid system
  • #124 - Grid Point Animation

Executive Summary

The current UIEntity/UIGrid system has fundamental architectural limitations preventing implementation of modern roguelike features. This proposal outlines a comprehensive redesign supporting:

  • Flexible entity content - Entities containing any UIDrawable (Frame, Caption, Grid, Sprite)
  • Multi-tile entities - 2x2, 3x3, or arbitrary-sized creatures and structures
  • Custom layer system - Weather effects, particle layers, UI overlays
  • Spatial optimization - O(1) entity queries via spatial hashing
  • Memory efficiency - Optional gridstate, chunk-based loading

Key Insight: Maintain entity as grid-specific container (no inheritance from UIDrawable), but allow flexible content (any UIDrawable).


Current Limitations

1. Entity Type Rigidity

Problem:

  • UIEntity hardcoded to contain only UISprite
  • Cannot place Frames, Captions, or Grids on grids
  • Blocks speech bubbles, nested grids, complex UI

Current Code:

class UIEntity {
    UISprite sprite;  // Hardcoded!
    // Should be: std::shared_ptr<UIDrawable> content;
}

2. Single-Tile Limitation

Problem:

  • Entity position is single point
  • No concept of dimensions or occupied tiles
  • Blocks large enemies (2x2 dragons), multi-tile structures (castle doors)

Missing:

  • width/height properties
  • Spatial occupancy tracking
  • Collision detection for multi-tile entities

3. Fixed Layer System

Problem:

  • Grid has three hardcoded layers: tiles, entities, visibility
  • No custom layers
  • Blocks cloud layer, particle effects, weather overlays

4. Performance Issues

Problem:

  • Linear O(n) iteration through all entities
  • No spatial indexing
  • Full grid re-render every frame

Current Code:

// O(n) iteration every frame
for (auto e : *entities) {
    if (in_viewport(e)) render(e);
}

5. Memory Inefficiency

Problem:

  • Every entity maintains full gridstate vector (width × height)
  • Decorative entities (clouds) waste memory on visibility data
  • Cannot unload distant chunks

Proposed Architecture

Core Change 1: Flexible Entity Content

class UIEntity {  // No inheritance - grid-specific container
private:
    std::shared_ptr<UIDrawable> content;      // Any drawable!
    sf::Vector2f gridPosition;                // Position in grid coords
    sf::Vector2i dimensions;                  // Size in tiles (default 1x1)
    std::set<sf::Vector2i> occupiedTiles;     // Cached occupied positions
    std::vector<UIGridPointState> gridstate;  // Optional perspective data
    
public:
    void setContent(std::shared_ptr<UIDrawable> drawable);
    void renderAt(sf::RenderTarget& target, sf::Vector2f pixelPos);
    bool occupies(int x, int y) const;
};

Python API:

# Entity with sprite (backward compatible)
enemy = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=5)

# Entity with frame (NEW - speech bubble)
speech_frame = mcrfpy.Frame(size=(100, 50))
speech_caption = mcrfpy.Caption(text="Hello!")
speech_frame.append(speech_caption)
speech_entity = mcrfpy.Entity(grid_pos=(player.x, player.y - 2))
speech_entity.content = speech_frame

# Entity with nested grid (NEW - mini-map)
minimap_grid = mcrfpy.Grid(grid_size=(20, 20), pos=(0, 0), size=(100, 100))
minimap_entity = mcrfpy.Entity(grid_pos=(5, 5))
minimap_entity.content = minimap_grid

Core Change 2: Multi-Tile Entities

class GridOccupancyMap {
private:
    std::unordered_map<int, std::set<std::shared_ptr<UIEntity>>> spatialHash;
    int cellSize = 16;
    
public:
    void addEntity(std::shared_ptr<UIEntity> entity);
    void removeEntity(std::shared_ptr<UIEntity> entity);
    std::vector<std::shared_ptr<UIEntity>> getEntitiesAt(int x, int y);  // O(1)
    std::vector<std::shared_ptr<UIEntity>> getEntitiesInRect(sf::IntRect rect);
};

Python API:

# Large enemy (2x2 tiles)
dragon = mcrfpy.Entity(
    grid_pos=(20, 20),
    sprite_index=10,
    dimensions=(2, 2)  # NEW: multi-tile support
)

# Check what tiles dragon occupies
occupied = dragon.occupied_tiles  # [(20, 20), (21, 20), (20, 21), (21, 21)]

# Collision detection accounts for size
if grid.can_move_to(dragon, new_x, new_y):
    dragon.x = new_x
    dragon.y = new_y

Core Change 3: Flexible Layer System

class GridLayer {
public:
    enum class Type { TILE, ENTITY, EFFECT, OVERLAY, CUSTOM };
    
private:
    Type type;
    std::string name;
    int zOrder;
    float opacity = 1.0f;
    bool visible = true;
    std::shared_ptr<EntityCollection> entities;  // For ENTITY layers
    
public:
    virtual void render(UIGrid* grid, sf::RenderTarget& target);
};

class UIGrid : public UIDrawable {
private:
    std::map<std::string, std::shared_ptr<GridLayer>> layers;
    std::vector<std::shared_ptr<GridLayer>> sortedLayers;  // By zOrder
    
public:
    std::shared_ptr<GridLayer> addLayer(const std::string& name, 
                                         GridLayer::Type type, int zOrder);
};

Python API:

# Create custom layers
grid.add_layer("clouds", mcrfpy.LayerType.EFFECT, z_order=100)
grid.add_layer("particles", mcrfpy.LayerType.EFFECT, z_order=50)
grid.add_layer("units", mcrfpy.LayerType.ENTITY, z_order=10)

# Add entities to specific layers
cloud = mcrfpy.Entity(grid_pos=(15, 15), sprite_index=20)
grid.get_layer("clouds").entities.append(cloud)

# Layer control
grid.get_layer("clouds").opacity = 0.5
grid.get_layer("clouds").visible = False

Core Change 4: Spatial Optimization

SpatialHash Implementation:

int hashPosition(int x, int y) const {
    return (x / cellSize) * 1000000 + (y / cellSize);
}

std::vector<std::shared_ptr<UIEntity>> getEntitiesInRect(sf::IntRect rect) {
    std::set<std::shared_ptr<UIEntity>> result;
    
    // Only check relevant hash cells
    for (int y = rect.top; y < rect.top + rect.height; y += cellSize) {
        for (int x = rect.left; x < rect.left + rect.width; x += cellSize) {
            int hash = hashPosition(x, y);
            if (spatialHash.count(hash)) {
                result.insert(spatialHash[hash].begin(), spatialHash[hash].end());
            }
        }
    }
    
    return std::vector<std::shared_ptr<UIEntity>>(result.begin(), result.end());
}

Performance Impact:

  • Current: O(n) for entity queries
  • Proposed: O(1) average case

Benchmark Targets:

  • 10,000 entities with 50 visible: <1ms per frame
  • Entity collision detection: O(k) where k = entities in same hash cell

Migration Path

Phase 1: Performance Foundation (Issues #115, #116, #117)

Backward compatible improvements:

  1. Add SpatialHash to existing UIGrid
  2. Implement dirty flag system
  3. Add memory pool for entities

No breaking changes to Python API.

Phase 2: Multi-Tile Support (Issue #123)

Add to existing UIEntity:

// Add to UIEntity class
sf::Vector2i dimensions = {1, 1};  // Default 1x1 (backward compatible)
std::set<sf::Vector2i> occupiedTiles;

void updateOccupiedTiles();
bool occupies(int x, int y) const;

Python API additions:

# Backward compatible - existing code works unchanged
enemy = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=5)

# New code can specify dimensions
dragon = mcrfpy.Entity(grid_pos=(20, 20), sprite_index=10, dimensions=(2, 2))

Phase 3: Flexible Content (Issue #124)

Replace UIEntity::sprite with content:

// Deprecate: UISprite sprite;
// Add: std::shared_ptr<UIDrawable> content;

// Backward compatibility shim:
PyObject* get_sprite() {
    auto sprite = std::dynamic_pointer_cast<UISprite>(content);
    if (!sprite) {
        // Legacy: entity still has sprite member
        return legacy_sprite_accessor();
    }
    return RET_PY_INSTANCE(sprite);
}

Migration period: 1-2 releases with deprecation warnings.

Phase 4: Layer System

Add GridLayer system alongside existing architecture:

// Existing entities automatically added to default "entities" layer
// New layer API available for advanced use

Backward compatible - existing code works unchanged.


Use Cases Enabled

Speech Bubbles

speech = mcrfpy.Frame(size=(100, 40))
speech.append(mcrfpy.Caption(text="Hello adventurer!"))
bubble = mcrfpy.Entity(grid_pos=(npc.x, npc.y - 1))
bubble.content = speech
grid.entities.append(bubble)

Large Enemies

dragon = mcrfpy.Entity(grid_pos=(25, 25), sprite_index=DRAGON, dimensions=(3, 3))

# Pathfinding accounts for size
if grid.can_large_entity_move_to(dragon, new_x, new_y):
    dragon.move_to(new_x, new_y)

Weather Effects

grid.add_layer("rain", mcrfpy.LayerType.EFFECT, z_order=200)
rain_layer = grid.get_layer("rain")

for i in range(100):
    raindrop = mcrfpy.Entity(
        grid_pos=(random.randint(0, 49), random.randint(0, 49)),
        sprite_index=RAINDROP
    )
    rain_layer.entities.append(raindrop)

Nested Mini-Map

minimap = mcrfpy.Grid(grid_size=(20, 20), pos=(0, 0), size=(100, 100))
# ... populate minimap ...

minimap_entity = mcrfpy.Entity(grid_pos=(0, 0))
minimap_entity.content = minimap
hud_layer.entities.append(minimap_entity)

Performance Expectations

Before (Current System)

  • 1,000 entities: 60 FPS
  • 10,000 entities: 15 FPS (unacceptable)
  • Entity query: O(n) = slow

After (Proposed System)

  • 1,000 entities: 60 FPS
  • 10,000 entities: 60 FPS (with spatial hash + culling)
  • Entity query: O(1) average case

Memory Impact

  • Per-entity overhead: +24 bytes (dimensions, occupied tiles set)
  • Spatial hash: ~8KB for 1000 entities (negligible)
  • Optional gridstate: Save width × height × sizeof(UIGridPointState) per decorative entity

Open Questions

  1. Backward Compatibility Timeline

    • How many releases should deprecation period last?
    • Support for legacy entity.sprite accessor?
  2. Layer API Design

    • Should layers have separate render textures?
    • Layer blending modes (multiply, add, alpha)?
  3. Multi-Tile Pathfinding

    • Should large entities use separate TCOD maps?
    • How to handle partially-blocked paths?
  4. Content Delegation

    • Should entity forward all UIDrawable methods to content?
    • Or keep explicit entity.content.method() pattern?

Implementation Complexity

Estimated Effort:

  • Phase 1 (SpatialHash, dirty flags): 40-60 hours
  • Phase 2 (Multi-tile): 20-30 hours
  • Phase 3 (Flexible content): 30-40 hours
  • Phase 4 (Layers): 40-50 hours

Total: 130-180 hours (3-4 months part-time)

Risk Areas:

  • Backward compatibility testing
  • Python binding complexity for flexible content
  • Performance regression testing
  • Documentation updates

Decision Needed

Should this proposal move forward?

  • Addresses major architectural limitations
  • Enables modern roguelike features
  • Clear migration path with backward compatibility
  • ⚠️ Significant implementation effort
  • ⚠️ Risk of introducing bugs in core systems

Alternative: Focus on incremental improvements (SpatialHash, dirty flags) without architectural redesign.


Navigation: