Reactive Variables

Learn about Functional Reactive Programming and composable transformation pipelines.

Functional Reactive Programming (FRP)

ClosureBT embraces Functional Reactive Programming principles through reactive variables and transformation pipelines. Variables are signal-based data streams that automatically propagate changes through a series of transformations.

What is FRP?

FRP treats variables as streams of values over time. When a source variable changes, that change flows through a pipeline of transformations automatically, creating a declarative data flow.

Think of it like Excel formulas: when a cell changes, all dependent cells recalculate automatically.

UsePipe: Composing Transformations

UsePipe chains multiple transformation operations on a reactive variable, creating a functional pipeline where data flows from left to right through each stage:

var mousePos = UseEveryTick(MousePlaneCastPosition);

// Create a pipeline: throttle → buffer → transform
var mousePosAverage = UsePipe(mousePos,
    v => UseThrottle(0.025f, v),          // Stage 1: Sample every 25ms
    v => UseRollingBuffer(15, false, v),  // Stage 2: Keep last 15 values
    v => UseSelect(v, buffer =>           // Stage 3: Calculate average
    {
        var avg = Vector3.zero;
        foreach (var pos in buffer)
            avg += pos;
        return avg / buffer.Count;
    }));

// Result: mousePosAverage automatically updates with smoothed mouse position
var mousePos = UseEveryTick(MousePlaneCastPosition);

// Create a pipeline: throttle → buffer → transform
var mousePosAverage = UsePipe(mousePos,
    v => UseThrottle(0.025f, v),          // Stage 1: Sample every 25ms
    v => UseRollingBuffer(15, false, v),  // Stage 2: Keep last 15 values
    v => UseSelect(v, buffer =>           // Stage 3: Calculate average
    {
        var avg = Vector3.zero;
        foreach (var pos in buffer)
            avg += pos;
        return avg / buffer.Count;
    }));

// Result: mousePosAverage automatically updates with smoothed mouse position

The visualization below shows how data flows through a pipeline:

UsePipe Data Flow

Throttle: 600ms
mousePos
High frequency updates
UseThrottle
Drops most packets
UseRollingBuffer
Collects to List
UseSelect
Computes average
💡 How it works:
Each circle represents a data packet. mousePos emits frequently.UseThrottle drops packets that arrive too soon (shown in red). Try adjusting the throttle slider to see how it affects the flow!
Vector3
Dropped
List<Vector3>
Computed Value

Each stage in the pipeline transforms the incoming data stream:

  • Source: The origin of data (e.g., mouse position, sensor input)
  • Throttle: Limits update frequency to prevent excessive computations
  • Buffer: Collects multiple values for aggregate operations
  • Select/Transform: Maps values to new forms (like LINQ's Select)

Use Methods: Reactive Operators

ClosureBT provides a library of Use* methods for building reactive pipelines. These are similar to RxJS operators or LINQ methods, designed for composing reactive data flows:

UseEveryTick

Samples a function or variable every tick, creating a reactive stream.

var mousePos = UseEveryTick(MouseWorldPosition);
var health = UseEveryTick(() => player.health);
var mousePos = UseEveryTick(MouseWorldPosition);
var health = UseEveryTick(() => player.health);

UseThrottle

Limits signal propagation to once per time period (in seconds).

var mousePos = UseEveryTick(MousePosition);
var throttled = UseThrottle(0.1f, mousePos); // Max once per 100ms
var mousePos = UseEveryTick(MousePosition);
var throttled = UseThrottle(0.1f, mousePos); // Max once per 100ms

UseRollingBuffer

Maintains a sliding window of the last N values.

var positions = UseRollingBuffer(10, false, mousePos);
// positions.Value is a List<Vector3> with last 10 positions
var positions = UseRollingBuffer(10, false, mousePos);
// positions.Value is a List<Vector3> with last 10 positions

UseSelect

Transforms each value through a projection function (like LINQ Select).

var health = UseEveryTick(() => player.health);
var healthPercent = UseSelect(health, h => h / 100f);
var health = UseEveryTick(() => player.health);
var healthPercent = UseSelect(health, h => h / 100f);

UseValueDidChange

Returns true for one tick when the source variable signals a change.

var target = UseEveryTick(() => currentTarget);
var targetChanged = UseValueDidChange(target);

OnTick(() => {
    if (targetChanged.Value)
        Debug.Log("New target acquired!");
});
var target = UseEveryTick(() => currentTarget);
var targetChanged = UseValueDidChange(target);

OnTick(() => {
    if (targetChanged.Value)
        Debug.Log("New target acquired!");
});

UseTimePredicateElapsed

Tracks how long (in seconds) a condition has been true.

var isMoving = UseEveryTick(() => velocity.magnitude > 0.1f);
var movingDuration = UseTimePredicateElapsed(isMoving, _ => isMoving.Value);
// movingDuration.Value = seconds the entity has been moving
var isMoving = UseEveryTick(() => velocity.magnitude > 0.1f);
var movingDuration = UseTimePredicateElapsed(isMoving, _ => isMoving.Value);
// movingDuration.Value = seconds the entity has been moving

Complete Use Methods Reference

All available reactive operators in ClosureBT:

UseEveryTick UseThrottle UseDebounce UseRollingBuffer UseSelect UseWhere UseScan UseValueDidChange UseDistinctUntilChanged UseCountChanged UseTimePredicateElapsed UseTicksElapsed UseElapsed UseWindowTime UsePipe

Explore More

Continue exploring the documentation.

Back to Getting Started →