How we made Welcome to the North Pole (Part 2 - Building snowmen)

This is part 2 of a series of blog posts describing how we made Welcome to the North Pole.

If you've made a similar blog post, paste the link here so we can create an index of tips and tricks.

See also: * Part 1 - Platformer mechanics.

Accumulating snow

Snow accumulates as you move on the ground.

WTTNP_Grow.gif

The ground detector detects whether the snowball is touching the ground (see Part 1).

As long as we haven't reached maximum size, we grow by a fraction of the GrowthDistance defined for that snowball size. Along with other stats, GrowthDistance is stored in a ScriptableObject instance for each size of snowball:

WTTNP_ScriptableObjects.PNG

This is what the code looks like:

```csharp void Grow() { var position = transform.position; var delta = (position - lastPosition).magnitude; lastPosition = position;

if (controller.Grounded) {
  SizeProgress += delta / stats.GrowthDistance;
  SizeProgress = Mathf.Clamp01(SizeProgress);
  if (SizeProgress == 1) {
    if (statsTemplates.Count > SizeIndex + 1) {
      SizeProgress = 0;
      SizeIndex++;
    } else {
      Die();
    }
  }
}
stats = statsTemplates[SizeIndex];

} ```

Stacks

Using a snowball's four detectors (see Part 1), we compute the top, bottom, left and right stacks of connected snowballs as seen from that snowball. Each stack reports its total weight.

We use stack information for the following: * Crushing snowballs under too much weight (see below). * Controlling multi-tiered snowmen (see below). * Jumping over other active snowballs (only the last snowball in a horizontal stack jumps).

WTTNPemStacks/emannotated.png Snowball and stack weight, as seen by the center snowball

A small trick to avoid code redundancy was to use C# lambda expressions:

```csharp delegate CharacterGroundDetector GetNextDetector(CharacterController current);

[System.Serializable]
public struct StackResult {
    public int Depth;
    public int Size;
    public CharacterController End;
}

private void UpdateStacks() {
    BottomStack = LookupStackEnd(c => c.groundDetector);
    TopStack = LookupStackEnd(c => c.ceilingDetector);
    ForwardStack = LookupStackEnd(
        c => Direction > 0 ? c.rightDetector : Direction < 0 ? c.leftDetector : null);
    BackStack = LookupStackEnd(
        c => Direction > 0 ? c.leftDetector : Direction < 0 ? c.rightDetector : null);
}

private StackResult LookupStackEnd(GetNextDetector detectorLookup) {
    var result = new StackResult();
    result.Depth = 0;
    result.Size = 0;
    result.End = this;
    // Limit the max depth to prevent loops in rare cases where detectors point to each other.
    while (detectorLookup(result.End) && result.Depth < 20) {
        var next = detectorLookup(result.End).CharacterInContact;
        if (!next || next == result.End) return result;
        result.Depth++;
        result.Size += next.Size;
        result.End = next;
    }
    return result;
}

```

Crush weight

If a snowball lands on top of another, the bottom snowball becomes inert. If the weight of its top stack is too high, the bottom snowball gets crushed.

WTTNP_Crush.gif

Other than looking cool, this helps manage the number of snowballs in existence and prevents clogging up doorways and tunnels. We slightly increase the linear friction of inert snowballs to keep them from rolling around too much (deceleration for active snowballs is handled by the movement code).

The crush code looks like this:

csharp void FixedUpdate() { if (TopStack.Depth > 0) { if (TopStack.Size > Size * WeightLimitFactor) { Explode(); } else { Die(); } } }

Snowman magnet

To keep snowmen in balance, we apply an elastic force to the top snowball towards its ideal position on top of the bottom snowball. To help you climb, this force is also applied as you move against another snowball:

WTTNP_Magnet.gif

The force is applied by enabling a TargetJoint2D on the top snowball and adjusting its target position each frame:

WTTNP_TargetJoint2D.PNG

This is what the update code looks like:

csharp private void UpdateLink() { var other = groundDetector.CharacterInContact; if (!other && forwardDetector != null) { other = forwardDetector.CharacterInContact; } if (other && !Jumping && !input.JumpHold && !input.Down) { PullJoint.target = other.transform.position + Vector3.up * (other.Scale + Scale) * 0.5f; PullJoint.maxForce = status.stats.LinkForce; PullJoint.enabled = true; } else { PullJoint.enabled = false; } }

Snowman control

A snowman is controlled by its head:

WTTNP_SnowmanControl.gif

Instead of applying the sideways movement to the head (see Part 1), we apply it on the bottom-most snowball.

We add a slight multiplier to account for the added friction between snowballs:

csharp private void MoveStack() { var target = Jumping ? this : BottomStack.Target; var powerMultiplier = 1f; if (target != this) { powerMultiplier = target.stats.StackControlMultiplier; } MoveSideways(target, powerMultiplier); }

End of Part 2

Thanks for reading!

See also: * Part 1 - Platformer mechanics.

In the next parts we plan to describe: * Gameplay polish (camera logic, smoothing) * Art style and implementation * Music and sound effect design

Feel free to post other suggestions in this thread.

If you've made a similar blog post, paste the link here so we can create an index of tips and tricks.

@fre and @catie-jo