Gaming and Fantasy, Technology Temerity

Shadow Trail Sample

Introduction

This example demonstrates a simple shadow trail effect using OpenBOR script arrays. It is intended as a reference sample for articles covering:

  • OpenBOR arrays (coming soon)
  • Flattened data structures (coming soon)
  • Temporary visual effects (coming soon)

This is not a complete shadow trail system. It is a minimal working example meant to show the underlying data pattern.

Concept

A shadow trail is created by recording snapshots of an entity over time. Each snapshot stores the entity’s position, current sprite, and expiration time.

The snapshots are stored in a self-contained ring buffer attached to the entity. During a global update, entities with active trail data are scanned, and their stored snapshots are drawn back to the screen as afterimages.

Data Stored Per Trail Segment

Each trail entry stores:

FieldPurpose
XStored X position
YStored Y position
ZStored Z position
SpriteSprite pointer to draw
TimeExpiration time

The trail data is stored in one flat array. The first element holds the ring buffer cursor. The remaining elements contain fixed-size trail entries.

#define SHADOWTRAIL_VARKEY_DATA       "shadowtrail_data"
#define SHADOW_TRAIL_DURATION         100 // Duration for each trail segment in centiseconds.

#define SHADOWTRAIL_COUNT             6   // Number of trail segments to store and draw.

#define SHADOWTRAIL_DATA_CURSOR       0   // Ring buffer cursor/index.
#define SHADOWTRAIL_DATA_START        1   // Offset where trail entries begin.

#define SHADOWTRAIL_ENTRY_X           0
#define SHADOWTRAIL_ENTRY_Y           1
#define SHADOWTRAIL_ENTRY_Z           2
#define SHADOWTRAIL_ENTRY_SPRITE      3
#define SHADOWTRAIL_ENTRY_TIME        4
#define SHADOWTRAIL_ENTRY_SIZE        5

Recording Trail Data

Run this function from an entity update whenever shadow trails should be recorded.

/*
* 2018-08-25
* Caskey, Damon V.
* 
* Sample shadow trail recording function.
* Accepts acting entity and records shadow
* trail data to a flattened ring buffer for
* use by draw function.
*/
void dc_shadowtrail_record(void acting_entity) {
    
    /* Guards */
    if(!acting_entity 
        || typeof(acting_entity) != openborconstant("VT_PTR")) {
        shutdown(1, "\n\n ERROR: shadowtrail_store_good(" + acting_entity + ") - Missing or invalid parameter.\n");
    }

    /*
    * Retrieve the trail data array from the entity.
    * If it doesn't exist yet, initialize it with the 
    * appropriate size and structure, then store it back 
    * in the entity variable.
    */
    void trail_data = getentityvar(acting_entity, SHADOWTRAIL_VARKEY_DATA);

    if(!trail_data || typeof(trail_data) != openborconstant("VT_PTR")) {

        /*
        * No trail data array found for this entity, so we 
        * need to initialize it.
        */

        /* 
        * Calculate the total size of the trail data array 
        * based on the number of segments and entry size. 
        * Full explanation below in the code comments where 
        * we store the data.
        */
        int list_size = SHADOWTRAIL_DATA_START + (SHADOWTRAIL_COUNT * SHADOWTRAIL_ENTRY_SIZE);

        /*
        * Allocate the trail data array with the calculated 
        * size and store its pointer in the entity variable.
        */
        trail_data = array(list_size);
        set(trail_data, SHADOWTRAIL_DATA_CURSOR, 0);
        setentityvar(acting_entity, SHADOWTRAIL_VARKEY_DATA, trail_data);
    }

    /* We'll need elapsed time + duration. */
    int expire_time = openborvariant("elapsed_time") + SHADOW_TRAIL_DURATION;

    /* Get current sprite and position of the entity. */
    void current_sprite = getentityproperty(acting_entity, "sprite");
    float x_pos = getentityproperty(acting_entity, "x");
    float y_pos = getentityproperty(acting_entity, "y");
    float z_pos = getentityproperty(acting_entity, "z");
    int entry_index = 0;

    /* Get ring buffer index, initialize to 0 if necessary. */
    int cursor = get(trail_data, SHADOWTRAIL_DATA_CURSOR);

    if(typeof(cursor) != openborconstant("VT_INTEGER")) {
        cursor = 0;
    }

    cursor = cursor % SHADOWTRAIL_COUNT;

    /* 
    * Flatten the grid data into a singular array
    * through index offset math.
    */
    entry_index = SHADOWTRAIL_DATA_START + (cursor * SHADOWTRAIL_ENTRY_SIZE);

    set(trail_data, entry_index + SHADOWTRAIL_ENTRY_X, x_pos);
    set(trail_data, entry_index + SHADOWTRAIL_ENTRY_Y, y_pos);
    set(trail_data, entry_index + SHADOWTRAIL_ENTRY_Z, z_pos);
    set(trail_data, entry_index + SHADOWTRAIL_ENTRY_SPRITE, current_sprite);
    set(trail_data, entry_index + SHADOWTRAIL_ENTRY_TIME, expire_time); // Store expiration time

    /* Increment the cursor for the next trail segment. */
    cursor++;
    set(trail_data, SHADOWTRAIL_DATA_CURSOR, cursor);
}

Drawing Trail Data

Run this function during a global update as part of entity enumeration. It reads stored trail entries and draws valid, unexpired sprites back to the screen.

/*
* 2018-08-25
* Caskey, Damon V.
* 
* Sample shadow trail draw function.
* Accepts acting entity, reads shadow
* trail ring buffer (if any) and draws
* after-images to screen. Clears the
* buffer element if expired.
*/
void dc_shadowtrail_draw(void acting_entity) {
    
    /* Guards */
    if(!acting_entity || typeof(acting_entity) != openborconstant("VT_PTR")) {
        shutdown(1, "\n\n ERROR: shadowtrail_draw_good(" + acting_entity + ") - Missing or invalid parameter.\n");
    }

    /*
    * Retrieve the trail data array from the entity.
    * If it doesn't exist or is invalid, we can't draw 
    * any trails, so we just exit.
    */
    void trail_data = getentityvar(acting_entity, SHADOWTRAIL_VARKEY_DATA);

    if(!trail_data || typeof(trail_data) != openborconstant("VT_PTR")) {
        return;
    }

    int i = 0;  // Main loop index for iterating through trail segments.
    int j = 0;  // Inner loop index for clearing expired trail segments when we find them.
    int entry_index = 0;
    void trail_sprite = NULL();
    float trail_x = 0.0;
    float trail_y = 0.0;
    float trail_z = 0.0;
    int sort_id = 0;
    int expire_time = 0;
    int elapsed_time = openborvariant("elapsed_time");
    
    /* Scan through all possible trail segments in the ring buffer. */
    for(i = 0; i < SHADOWTRAIL_COUNT; i++) {

        /* See store function for index calculation. */
        entry_index = SHADOWTRAIL_DATA_START + (i * SHADOWTRAIL_ENTRY_SIZE);

        /* 
        * Get the trail sprite for this segment. 
        * Make sure it's a valid pointer before 
        * trying to draw. If it's not valid, this 
        * segment is either uninitialized or cleared, 
        * so we skip it.
        */
       
        trail_sprite = get(trail_data, entry_index + SHADOWTRAIL_ENTRY_SPRITE);

        if(!trail_sprite || typeof(trail_sprite) != openborconstant("VT_PTR")) {
            continue;
        }

        /* 
        * Check if the trail segment has expired. 
        * If it has, we clear the data for this 
        * segment and skip to next iteration. 
        */

        expire_time = get(trail_data, entry_index + SHADOWTRAIL_ENTRY_TIME);
        
        if(typeof(expire_time) != openborconstant("VT_INTEGER") 
            || expire_time <= elapsed_time) {

            /* Clear the trail segment data for this entry. */
            for(j = 0; j < SHADOWTRAIL_ENTRY_SIZE; j++) {
                set(trail_data, entry_index + j, NULL());
            }

            continue;
        }

        /*
        * Retrieve the position and whatever other data 
        * we need for this trail segment.
        */

        trail_x = get(trail_data, entry_index + SHADOWTRAIL_ENTRY_X);
        trail_y = get(trail_data, entry_index + SHADOWTRAIL_ENTRY_Y);
        trail_z = get(trail_data, entry_index + SHADOWTRAIL_ENTRY_Z);

        /*
        * Older trails go further back, all go one step
        * behind the current entity z to prevent overlap.
        */
        
        sort_id = i - 1;

        /*
        * Draw the trail sprite at its position.
        */

        drawsprite(trail_sprite, trail_x, trail_y, trail_z, sort_id);        
    }
}

Why the Array Is Flattened

Instead of creating nested arrays, each trail segment is packed into a single flat array.

For example, with six trail entries and five values per entry:

[cursor, x, y, z, sprite, time, x, y, z, sprite, time, ... ]

This layout is compact, predictable, and easy to index with offset math.

Notes

This sample intentionally keeps the stored data minimal. More advanced versions could also store:

  • Drawmethod data
    • Alpha or tint values
    • Scale
    • Blending
    • Palettes
  • Facing direction
  • Animation frame timing
  • Per-segment delay
  • Entity-specific trail settings

Use this sample as a foundation for tutorials explaining how OpenBOR arrays, flattened structures, and ring buffers can support reusable visual effects.

Author: Damon Caskey

Hello all, Damon Caskey here - the esteemed owner of this little slice of cyberspace. Welcome!

1 Comment

Leave a Reply