04.animating-and-coding-the-ghost-effect

Animating and coding the ghost effect

In this lesson, we will code the ghost effect pickup.

When the player touches the pickup, the character will be able to move through walls for a short time.

To achieve that, we will split the code across two places:

  1. The character’s script will contain the code that enables the ghost effect.
  2. The pickup detects the player, and triggers the effect.

This is a typical way to code items, powerups, and other pickups. It allows you to later delete, add, and modify pickups without having to modify the character’s code.

We’ll get started by adding the ghost effect code on the character.

Adding the ghost effect functionality

When activating the ghost effect, we want to:

Let’s start by adding the missing nodes to the character. Open the scene ObstacleCourse_Part2/Godot.tscn.

Add a Timer node and a second AnimationPlayer as children of the Godot node. Rename the nodes to TimerGhost and AnimationPlayerGhost.

Select the TimerGhost and increase its Wait Time to 3 seconds in the Inspector. That’s the effect’s duration. Turn on the One Shot property to ensure the timer does not cycle continuously.

Designing the ghost animation

We will now create a new animation that makes the character sprite fade in and out.

Select the AnimationPlayerGhost and create a new animation by clicking on Animation -> New.

Name the animation “ghost.”

We want to animate the modulate property of the Godot node to make all the sprites in the scene blink.

Click the Add Track button in the Animation editor and select Property Track.

In the popup window, select the Godot node and then search for its modulate property.

This approach to creating tracks is an alternative to using the key icon in the Inspector.

Right-click in the animation track at 0.0 seconds and create a new keyframe by clicking Insert Key.

Then, right-click around 0.4 seconds in the timeline and insert a second key.

Select the newly inserted white square and make the color next to Value transparent in the Inspector.

To do so, click the white rectangle to open the color picker and pull the A slider to the left.

You should now see a gradient going from white to transparent white in the animation editor, as indicated by the checkerboard pattern.

Lastly, click the loop icon in the animation editor’s top-right to make the animation cycle.

If you play the animation now, you’ll see your character fade in and out.

Also, you can play the hover animation on the first AnimationPlayer node to see how both can play simultaneously.

Coding the ghost effect

With the ghost animation ready, we can code a function that toggles the ghost effect on the character.

We will call that function from the pickup when the player collects it. The function will:

  1. Play the animation we just designed.
  2. Remove all collisions.
  3. Start the timer.

When the timer times out, we will:

  1. Stop the animation.
  2. Restore all collisions.

Open the Godot.gd script and reference the newly added nodes in onready variables.

onready var animation_player_ghost := $AnimationPlayerGhost
onready var timer_ghost := $TimerGhost

We will now define a new function toggle_ghost_effect() to toggle the ghost effect on the player.

Before we do, we will store the physics body’s collision layers and masks in a variable to restore them when the effect times out.

# We store the active collision layers and masks at the start of the game to 
# later toggle them on and off using code.
var start_collision_layer := collision_layer
var start_collision_mask := collision_mask

# This function activates or deactivates the ghost effect depending on the
# `is_on` argument.
func toggle_ghost_effect(is_on: bool) -> void:
    if is_on:
        timer_ghost.start()
        # We turn off collision detection by setting both the collision layer
        # and mask to 0.
        collision_layer = 0
        collision_mask = 0
        animation_player_ghost.play("ghost")
    else:
        # When turning off the effect, we restore the original collision layer
        # and mask.
        collision_layer = start_collision_layer
        collision_mask = start_collision_mask
        animation_player_ghost.stop()

Using the is_on parameter, we can group the code in a single function. If the value is true, we apply the ghost effect. Otherwise, we remove it.

To activate the effect, we’ll call the function from the ghost effect pickup’s script.

However, the Timer to deactivate the effect is on this character. We need to connect the Timer.timeout signal to our function to turn off the ghost effect.

func _ready() -> void:
    # We directly connect the signal to toggle_ghost_effect, and ask Godot to
    # call the function with a value of `false`.
    timer_ghost.connect("timeout", self, "toggle_ghost_effect", [false])

Now the question is, how do we call the toggle_ghost_effect() function when the player interacts with the ghost pickup?

We’ll do that in the pickup’s script in the next lesson.

Practice: Triggering an animation

Open the practice Triggering an animation.

This practice serves as a recap for animating and controlling animations.

The code

Here is the complete code for Godot.gd.

extends KinematicBody2D


const DIRECTION_TO_FRAME := {
    Vector2.DOWN: 0,
    Vector2.DOWN + Vector2.RIGHT: 1,
    Vector2.RIGHT: 2,
    Vector2.UP + Vector2.RIGHT: 3,
    Vector2.UP: 4,
}

const SPEED := 700.0

var drag_factor: float = 0.13
var velocity := Vector2.ZERO

# We store the active collision layers and masks at the start of the game to 
# later toggle them on and off using code.
var start_collision_layer := collision_layer
var start_collision_mask := collision_mask

onready var sprite := $Sprite
onready var animation_player_ghost := $AnimationPlayerGhost
onready var timer_ghost := $TimerGhost


func _ready() -> void:
    # We directly connect the signal to toggle_ghost_effect, and ask Godot to
    # call the function with a value of `false`.
    timer_ghost.connect("timeout", self, "toggle_ghost_effect", [false])


func _physics_process(delta: float) -> void:
    var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
    var desired_velocity := SPEED * direction
    var steering_vector := desired_velocity - velocity
    velocity += steering_vector * drag_factor
    move_and_slide(velocity)

    var direction_to_frame_key := direction.round()
    direction_to_frame_key.x = abs(direction_to_frame_key.x)
    if direction_to_frame_key in DIRECTION_TO_FRAME:
        sprite.frame = DIRECTION_TO_FRAME[direction_to_frame_key]
        sprite.flip_h = sign(direction.x) == -1


# This function activates or deactivates the ghost effect depending on the
# `is_on` argument.
func toggle_ghost_effect(is_on: bool) -> void:
    if is_on:
        timer_ghost.start()
        # We turn off collision detection by setting both the collision layer
        # and mask to 0.
        collision_layer = 0
        collision_mask = 0
        animation_player_ghost.play("ghost")
    else:
        # When turning off the effect, we restore the original collision layer
        # and mask.
        collision_layer = start_collision_layer
        collision_mask = start_collision_mask
        animation_player_ghost.stop()