Pallas Athena

Artificial Life: Wandering Behavior

In this Post ...

I'll borrow from Craig Reynolds' Steering Behavior to create organic motion for the the SVG Creators' Framework™

Introduction

This week I've been working on a reference piece for an artwork I'll be commissioning (as part of a launch for the SVG artworks initiative I've been working toward). Ever a fan of fantasy art and literature, I wanted a piece that evokes a sylvan woodland type setting -- think Midsummer Night's Dream with fairy-like sprites fluttering around. As usual, I started naive with simple random behavior; fine for a preliminary pass. But what an artist really needs for efforts like this is a more organic set of behaviors. Which brought me right back to NPC steering concepts I've been thinking about lately...

Defining what it means to Wander

For this piece I've focused on wandering. I took a quick look at some definitions of what it means to wander and was immediately presented with; "traveling aimlessly". Personally I prefer "traveling without a preset route". Read on and see why.

The 'random' definition wasn't quite enough for me. Naively changing sprite velocity at random generates a jittery jarring experience -- like the tiny flies you see at twilight on a rocky beach. For my sylvan setting I wanted something more organic -- like the concept Reynolds outlined in his Steering Behaviors. In that context, wandering is more than just random changes in direction. Wandering implies intent.

Allowing Sprites to Wander Freely

Conceptually, Reynolds' wander behavior produces a smooth, meandering motion (not just random jitter). We can achieve this by applying random perturbations to a target point on a "wander circle" projected ahead of the sprite. And this is exactly what I want you to see in the following artwork.

Artwork: Studying the Spotlight of "Attention"

image/svg+xml
FPS:
The Spotlight of Attention

Meandering Fairies and a "Happy Accident"

The art depicts a sylvan setting in a sort of pre-dawn light. Fairy-like sprites are visible meandering around the scene emitting trails of "fairy dust". I've included the full sprite implementation in an appendix. But what I want to talk about here is actually somewhat of a "happy accident" revealed in a debug visualization.

If you play with the artwork, you'll see a "wand-like" line pointing to the center of a circle -- the very same "wander" circle I described conceptually above. Also visible on the circle is a magenta dot -- the target of the sprite's "attention".

To me, this debug visualization itself is gold. It turns conceptually abstract steering forces into something you can see and feel. Rendering these abstract concepts literally amounts to painting the forces that drive the sprites' behavior. As an artist and visual thinker I get more of an appreciation for the math whenever I can visualize these abstractions. Seeing the "spotlight of attention" dancing around like a thought bubble and being able to observe small changes to the "wander factor" is of huge value to my process.

The other important feature of the visualization are the motion trails which I've left visible as curves in this rendering. My original intent was for the sprites to become emitters as I continue refinements on my particle system. The motion trails in this piece are really an early iteration of particle emission prior to introducing a bit of stochasticity. But combined with the debug scaffolding the visualization turned into much, much more. As I played with the acceleration on the sprites I was able to visualize the changes over time in the resultant motion trails -- like a "Lissajous dance" of sorts. I saw emergent sinusiodal paths and observed the changes in velocity in the distance between particles.

Discussion

As I was working on this piece I had a conversation with a friend. He was talking about his experience learning math and recalled how he was fine with concrete number crunching but when it got to algebraic abstractions -- not so much. That prompted me to show him the sylvan setting WIP -- from the viewpoint of someone trying to visualize the math.

Over the course of the discussion it became clear to me that part of the beauty of the visualization lies in how it renders visible the concept of acceleration. The artist in me craves that sort of visualization to help me "think in math" -- it transforms mathematical concepts into a visual language and, as such, it's a perfect bridge across cognitive intuition, art, and computation. Art and code reflections of the same underlying language.

In this visualization, the wander circle is more than just a mathematical construct -- it's a "zone of curiosity" and the target is the "spotlight of attention". The acceleration trail is a record of thought -- not just motion. And the beauty of it all is that the motion trail isn't hard coded. It emerges naturally from the system's local constraints like an artwork born of simple brush strokes.

More to Come

This piece is just a start at giving life to sprites in the SVG Collaborative Arts framework. Even in this first-pass, wandering sprites have been endowed with believable inertia. But if you study and play with the implementation I provide in the appendix, you'll see how small changes in wanderRadius, wanderDistance, and jitter affect behavior. Factoring these variables out as tunable parameters enables artists to create NPC's with distinct personalities” -- shy ones with small jitter, playful ones with wide radius and high wander-gain and so on. So really this is just a start. Look for more ready-to-use SVG behavior packages coming soon!

Appendix: The Wandering Sprite Implementation

Here, for the technically inclinded reader, is a listing of my implementation of the wandering sprite class. Look carefully at the move method and the associated wander. Below the listing I've provided aditional analysis.

export class FairySprite extends Movable {

    /**
     * Fairy c-tor ... 
     * @param {*} id 
     * @param {*} svg 
     * @param {*} p 
     * @param {*} v 
     * @param {*} a 
     * @param {*} r 
     * @param {*} s 
     * @param {GameController} gc reference to the GameController (injected by the factory)
     */
    constructor (id, svg, p, v, a, r, s, gc ) {
        super( p, v, a );
        this.rotation = r;
        this.scale    = s;
        this.id  = id;
        this.svg = svg; 
        this.gameController = gc;
        this.emissionRate   = 1;
        this.maxSpeed       = 100;

        // Wander parameters (basically a circle in front of the sprite...)
        this.wanderTheta    = 0.5 * ( -(Math.PI/4)  + Math.random() * (Math.PI / 2) ) ;
        this.wanderRadius   = 25;
        this.wanderDistance = 60;
        this.wanderJitter   = 5;

        // -------- Debug visualization ----------------------
        this.debug = true;
        this.debugElements = {
            circle: null,
            target: null,
            line: null,
        };

    } 


    /**
     * SVG transform update for DOM. This is the *view* update.
     * it updates sprite's position, rotation and scale in light
     * of changes to the sprite model...
     */
    updateTransform (  ) {
        const { x, y } = this.pos;
        const rotation = this.rotation;
        const scale    = this.scale;
        const bbox = this.svg.getBBox();
        // const Y_OFFSET = bbox.height / 2 - 10; // <--| 10 is just a constant
        const xOffset = (bbox.width / 2)  * scale.xAxis;
        const yOffset = (bbox.height / 2) * scale.yAxis;
        this.svg.setAttribute(
            'transform',
            `translate( ${x-xOffset} ${y-yOffset} ) rotate( ${rotation} ) scale( ${scale.xAxis} ${scale.yAxis} )`
        );
    }

    /**
     * Apply wander steering force to simulate smooth, organic 
     * directional drift (wandering behavior).
     */
    wander(deltaTime) {
        this.wanderTheta += ( Math.random() * 2 - 1 ) * this.wanderJitter * deltaTime;
        let wanderVector   = Vector2D.getNormalized( this.vel );
        const circleCenter = wanderVector.multiply( this.wanderDistance );
        const displacement = Vector2D.fromCartesian (
            this.wanderRadius * Math.cos( this.wanderTheta ),
            this.wanderRadius * Math.sin( this.wanderTheta )
        );
        const wanderForce = circleCenter.add(displacement);
        this.acceleration.x += (wanderForce.x - this.vel.x) * 10;  //0.2;
        this.acceleration.y += (wanderForce.y - this.vel.y) * 10; //0.2;

        // ---- DEBUG VIZ ----------------
        if (this.debug) {
            this.renderDebug(circleCenter, displacement);
        } else {
            this.clearDebug();
        }
    }

    /**
     * Fairies wander about (using Reynolds' wander concept...)
     * 
     * @param {*} deltaTime 
     */
    move( deltaTime ) {
        // Reset acceleration each frame (ELSE runaway acceleration bug...
        this.acceleration.x = 0;
        this.acceleration.y = 0;

        // update the acceleration  
        this.wander( deltaTime );
        // Literally integrate motion ... 
        super.move( deltaTime );

        // Limit velocity magnitude 
        const speed = this.vel.length();
        if (speed > this.maxSpeed) {
            let vNorm = Vector2D.getNormalized( this.vel );
            vNorm     = vNorm.multiply(this.maxSpeed);
            this.vel.x = vNorm.x;
            this.vel.y = vNorm.y;
        }

        this.wrap();
    }

    update ( deltaTime ) {
        this.move( deltaTime );
        this.updateTransform();
        this.emitFairyDust();
    }

    wrap() {
        // horizontal wrap
        if( this.pos.x > this.WorldModel.bounds.width ) {
            this.pos.x = this.WorldModel.bounds.x ;
        } else if ( this.pos.x < this.WorldModel.bounds.x ) {
            this.pos.x = this.WorldModel.bounds.width;
        }
        // vertical wrap
        if( this.pos.y > this.WorldModel.bounds.height ) {
            this.pos.y = this.WorldModel.bounds.y;
        } else if ( this.pos.y < this.WorldModel.bounds.y ) {
            this.pos.y = this.WorldModel.bounds.height;
        }
    }


    setWorldModel( WorldModel ) {
        this.WorldModel = WorldModel;
    }

    setParticleSystem( ps ) {
        this.particleSystem = ps;
    }

    ...

    createFairyDust () {
        function particleSvg () {
            const circle = 
                document.createElementNS(
                    "http://www.w3.org/2000/svg", 
                    "circle"
                );
            circle.setAttribute( "cx", 0 );
            circle.setAttribute( "cy", 0 );
            circle.setAttribute( "r", "1" );
            circle.setAttribute( "fill", "green" );
            return( circle );
        }
        const pos = Vector2D.fromCartesian(
            this.pos.x,
            this.pos.y
        );
        const vel = Vector2D.fromCartesian(
            0,
            0.25
        );
        const acc = Vector2D.fromCartesian(
            0,
            0
        );
        const rot = 0;
        const scale = {xAxis:1, yAxis:1};
        const ttl = 2; // <--| seconds
        const svg = particleSvg();
        const pSystem = this.particleSystem;

        let dust = new FairyDust( 
            pos,
            vel,
            acc,
            rot,
            scale,
            ttl,
            svg,
            pSystem
         );
        return dust;
    }

    /**
     * Turns this sprite into a particle emmiter ... 
     */
    emitFairyDust() {
        for ( let i=0; i this.ttl ) {
            this.particleSystem.removeParticle( this );
        }
    }

}

Analysis of Key Parts

This implementation is part of the larger SVG Creators' Collaborative™ artists collective framework. In creating that framework I've defined a javascript vector math module and an associated frame for kinematics integration to enable physics in behavioral simulations which I've documented elsewhere.

In this system, I've provided a base-class for defining sprites; Movable. Movable is designed around vector composition and can be readily extended to create new sprite types. In FairySprite, I introduce steering behaviors by overriding the move method. In the override, I update acceleration using Reynolds' wander conceptualization and then call the parent move to integrate kinematics.

And that's it. Using the SVG Creators' framework, integrating NPC behaviors is that easy!

Resources

  1. Steering Behaviors For Autonomous Characters.