Interactive 2D Foliage

Having interactive foliage in your game helps bring some life to your environment. It makes everything feel more alive. There are lots of different ways to handle interactive foliage. The easiest way is to stick a trigger collider on your GameObject. When the player hits the trigger just move it back and forth on the x-axis. In this post we will go over how to kick that up a notch.

If you read the Modeling 2D Water with Springs posts, you will know that I love springs. For that reason we are going to model our grass with a spring. The first approach I took when making interactive foliage was to handle all the animation in the vertex shader. With Unity that will break batching so I had to fall back to the method detailed in this post.

Handling Collision

The first thing we need is a trigger collider. You can use a BoxCollider2D or a CircleCollider2D. When the player enters the collider we take note of the _enterOffset which is just the distance between the player collider and the foliage collider. We need the _enterOffset so that we don't start bending the foliage until the player has passed the midpoint of the foliage. Things don't start bending until you are dragging them past their resting position. That's just how the world works.

void OnTriggerEnter2D( Collider2D col )  
{
    if( col.gameObject.layer == k.Layers.PLAYER )
    {
        _enterOffset = col.transform.position.x - transform.position.x;
    }
}

We will use OnTriggerStay2D to keep track of the player + foliage interaction. Once the player has moved past the midpoint (offset and _enterOffset will have opposite signs) we set some flags and start to bend the foliage. Bending the foliage is done by just sliding the top 2 verts of the foliage quad back and forth. At this point, the foliage bend is entirely based on the position of the player.

void OnTriggerStay2D( Collider2D col )  
{
    if( col.gameObject.layer == k.Layers.PLAYER )
    {
        var offset = col.transform.position.x - transform.position.x;

        if( _isBending || Mathf.Sign( _enterOffset ) != Mathf.Sign( offset ) )
        {
            _isRebounding = false;
            _isBending = true;

            // figure out how far we have moved into the trigger and then map the offset to -1 to 1.
            // 0 would be neutral, -1 to the left and +1 to the right.
            var radius = _colliderHalfWidth + col.bounds.size.x * 0.5f;
            _exitOffset = map( offset, -radius, radius, -1f, 1f );
            setVertHorizontalOffset( _exitOffset );
        }
    }
}


// simple method to offset the top 2 verts of a quad based on the offset and BEND_FACTOR constant
void setVertHorizontalOffset( float offset )  
{
    var verts = _meshFilter.mesh.vertices;

    verts[1].x = 0.5f + offset * BEND_FACTOR / transform.localScale.x;
    verts[3].x = -0.5f + offset * BEND_FACTOR / transform.localScale.x;

    _meshFilter.mesh.vertices = verts;
}

Bounceback Oscillation

Once the player exits the trigger the spring takes over and it will handle simulating the foliage oscillation (springs are the best!). We apply a force to the spring and let it do its thing. Here the _isRebounding flag is set which lets the spring know to take over (that is all handled in the Update method). When the spring acceleration dies down the oscillation is stopped. This is done as an optimization. There is no reason to continously update the mesh vertices for movements too small to see. You can the result in the video below the code block.

void OnTriggerExit2D( Collider2D col )  
{
    if( col.gameObject.layer == k.Layers.PLAYER )
    {
        if( _isBending )
        {
            // apply a force in the opposite direction that we are currently bending
            _spring.applyForceStartingAtPosition( BEND_FORCE_ON_EXIT * Mathf.Sign( _exitOffset ), _exitOffset );
        }

        _isBending = false;
        _isRebounding = true;
    }
}


void Update()  
{
    if( _isRebounding )
    {
        setVertHorizontalOffset( _spring.simulate() );

        // apply the spring until its acceleration dies down
        if( Mathf.Abs( _spring.acceleration ) < 0.00005f )
        {
            // reset to 0 which is neutral
            setVertHorizontalOffset( 0f );
            _isRebounding = false;
        }
    }
}

Adding Wind

What we have now is a firm base: interactive foliage and a configurable spring system to give it some life. Adding something like a wind force is super easy. All we have to do is use a sin wave to vary the wind and apply the force to the spring.

// addition to the Update method to add a wind force
if( isWindEnabled && !_isBending )  
{
    var windForce = baseWindForce + Mathf.Pow( Mathf.Sin( Time.time * windPeriod + _windOffset ) * 0.7f + 0.05f, 4 ) * 0.05f * windForceMultiplier;
    _spring.applyAdditiveForce( windForce );

    // we only simulate if we are not rebounding. While rebounding the simulation will occur in the next block
    if( !_isRebounding )
        setVertHorizontalOffset( _spring.simulate() );
}

Jumping Interactions

The last piece of extra polish we can add is to make our foliage part when we jump. This is also a simple addition due to the solid base we have already set. We just need to detect when the player jumps into the foliage and apply a force to the spring. We use the positions of the player and foliage to see if we should apply the force to the left or right. Nice and simple.

// addition to the OnTriggerEnter2D method to handle jumping into the foliage
if( col.GetComponent<Player>().velocity.y < -3f )  
{
    // apply a force in the proper direction based on where we impacted
    if( col.transform.position.x < transform.position.x )
        _spring.applyAdditiveForce( BEND_FORCE_ON_EXIT );
    else
        _spring.applyAdditiveForce( -BEND_FORCE_ON_EXIT );
    _isRebounding = true;
}