How we made Welcome to the North Pole (Part 1 - Platformer mechanics)

In this post I'll be describing the implementation of some of the platformer elements in Welcome to the North Pole.

Perhaps this can serve as inspiration for others or as a way to start exchanging tips. We made the game using Unity but these concepts apply elsewhere.

See also: * Part 2 - Building snowmen

Detectors

WTTNP_Detectors.gif

Each snowball has 4 detectors that report contact with the ground or other characters. We use them to detect whether a snowball can jump and what shape a snowman has.

Detectors are configured as trigger and only react to contact with other characters or the environment:

WTTNP_GroundDetectorStats.PNG

WTTNP_Layers.PNG Our layer setup, note that it avoids inter-collisions between static layers or between triggers

This is the simplified code for the collision detection:

csharp private void OnTriggerStay2D(Collider2D other) { // Layers is defined by hand. if (other.gameObject.layer == (int) Layers.GROUND) { GroundContact = true; lastGroundContactTime = Time.time; } }

Contacts are reset in FixedUpdate(), which executes just before the physics loop (where OnTriggerStay2D is called). We moved CharacterGroundDetector last in the script execution order to make sure that the correct value can be read from other scripts' Update() or FixedUpdate() loops.

```csharp void FixedUpdate() { // Do not clear contact if the rigidbody is sleeping, // otherwise it won't be re-registered until the rigidbody wakes up. if (controller.rb.IsSleeping()) return;

// Allow some tolerance after the last contact before clearing the flag.
// This allows e.g. jumping slightly after leaving a platform.
if (Time.time > lastGroundContactTime + ContactTimeTolerance) {
  GroundContact = false;
}

} ```

Detectors are children of an "Upright" GameObject that preserves its orientation even though the snowball rotates:

csharp void FixedUpdate() { transform.rotation = Quaternion.identity; }

Visual vs physical size

WTTNP_Size.PNG Two size 1 snowballs: before accumulating snow (left) and about to grow (right)

Main ideas: * The physical size of a snowball is fixed for each stage (small/medium/large). * The visual sprite grows slowly as you accumulate snow.

This let us balance the game for a fixed collider size while giving a visual feedback that a snowball changes size (coherent with the world).

We kept the ball diameter below the size of one tile to make sure a snowball can easily fit through a gap of the corresponding size (collider diameters are 0.8, 1.8 and 2.6, a tile is 1x1).

Because snowballs are made of snow, the default visual size is already slightly bigger than the collider because a slight overlap looks more natural than having snowballs touch the ground right along their edge.

Momentum

WTTNP_Momentum.gif

We took the movement principles from this Sonic physics guide. Another helpful resource was this blog post by Yoann Pignole.

Main ideas: * Each snowball has a maximum speed and an acceleration factor (the bigger the snowball, the slower it is). * While a direction is held, we increase the snowball's velocity in that direction up to the maximum speed. * Velocity is not reduced if it's above the maximum speed. This lets you gather a lot of momentum on the ground and keep it while jumping, or as a small snowball and preserve it as you grow bigger (adds a nice flow and is actually helpful for one of the challenges). * The ball does have some amount of friction that we let the physics engine resolve. This is helpful to make the snowball rotate naturally as it rolls along the ground.

Translated into code, it looks something like this:

csharp private void MoveSideways() { var currentSpeed = Mathf.Abs(rb.velocity.x); var currentDirection = Mathf.Sign(rb.velocity.x); // Air speed is lower than ground speed to allow less control in the air than on ground, // but momentum is preserved if you hold the direction. var maxSpeed = Grounded ? stats.GroundSpeed : stats.AirSpeed; var targetSpeed = currentSpeed; if (currentDirection == Direction) { // Accelerate up to maxSpeed, but do not decelerate while holding the key. targetSpeed += Mathf.Min(stats.Acceleration * Time.deltaTime, Mathf.Max(maxSpeed - targetSpeed, 0)); } else if (Direction == 0) { // Decelerate up to 0 if no key is held. targetSpeed -= stats.IdleDeceleration * Time.deltaTime; targetSpeed = Mathf.Max(targetSpeed, 0); } else { // Decelerate faster until we flip direction if the opposite key is held. targetSpeed -= stats.OppositeDeceleration * Time.deltaTime; } // Apply the velocity change through a force on the rigidbody. rb.AddForce(Vector2.right * currentDirection * (targetSpeed - currentSpeed) * rb.mass, ForceMode2D.Impulse); }

Long jump

Holding the jump key makes you jump higher:

WTTNP_Jump.gif

This is implemented as a combination of forces: * JumpImpulse is applied right as you press the jump key. * JumpHoldForce is applied over time, while the jump key is held, up to a maximum duration (0.35s).

In code, it looks something like this:

```csharp private void HandleJump() { // Trigger jump if (Grounded && input.Jump && !Jumping) { Jumping = true; jumpEndTime = Time.time + stats.MaxJumpDuration; // Cancel vertical velocity and jump up. // Vertical velocity must be canceled because the snowball may still be falling // when the ground detector triggers. rb.AddForce(Vector2.up * (stats.JumpImpulse - rb.velocity.y) * rb.mass, ForceMode2D.Impulse); }

    if (Time.time > jumpEndTime) {
        Jumping = false;
    }

    if (Jumping && input.JumpHold) {
        rb.AddForce(Vector2.up * stats.JumpHoldForce * rb.mass, ForceMode2D.Force);
    } else {
        // Stop jump hold force when input is released.
        Jumping = false;
    }
}

```

We later added a small tweak: moving against a wall while holding jump gives you a small additional boost (ClimbHoldForce) to counteract the added friction and because it feels satisfying:

WTTNP_Climb.gif

This allows you to clear 4-high walls without building a snowman :)

End of Part 1

Thanks for reading!

See also: * Part 2 - Building snowmen

In the next parts we plan to describe: * Art style and implementation * Music and sound effect design

Feel free to post other suggestions in this thread.

@fre and @catie-jo