fre

LD 43

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

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

Ludum Dare 45

Fairytiles - A literary adventure

My girlfriend and I made a game!

fairytiles_preview.gif

Imagine the love child of a “Choose your own Adventure” book :blue_book: and a game of Scrabble :a:

Armed with only a handful of letter tiles, take a bold adventurer :dancer: through the journey of her lifetime!

  • Start with nothing, end with… a cat :cat:? Dragons :dragon_face:?
  • Complete with 120 creative chapters and 26 alphabetized achievements.
Play it here: https://ldjam.com/events/ludum-dare/45/fairytiles