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:
| Field | Purpose |
|---|---|
| X | Stored X position |
| Y | Stored Y position |
| Z | Stored Z position |
| Sprite | Sprite pointer to draw |
| Time | Expiration 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.
More on flattening coming soon.
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.
1 Comment