Dropping and picking up entities

From the first chapter, we have a system where a removed entity pops out of existence, gone forever.

In an actual game, that feels a little unfair. Instead, the player should be able to pick up an entity they dropped. When their inventory is full, we should also drop a blueprint on the ground to pick it up later.

Ground Items

To do this, we add a new entity type: the ground item. This special entity holds a reference to a blueprint and applies its graphic to a sprite. When the player walks into range, if there’s space in their inventory, the item animates jumping to him, and the blueprint goes into the slot.

The GroundItem scene

Create a new scene, GroundItem.tscn, with a Node2D named GroundItem as its root.

Remember that our Library will load anything that ends with the “Entity.tscn” suffix, but we don’t need ground items in it. So with the scene’s name, “GroundItem”, we ensure the library won’t register it as a regular entity.

For the ground item’s graphic, we add a Sprite with a Scale of 0.25 x 0.25. This keeps ground entities distinct from ghosts by how small they are, and it makes it obvious the player can pick them up.

We’ll code its texture programmatically, but you can assign it a texture for preview purposes. I chose the battery’s sprite.

To detect when the player comes into range of the item, we can use an Area2D with a circular collision shape. Add an Area2D as a child of GroundItem and a CollisionShape2D as a child of the area with a CircleShape2D in its Shape property. A 10-pixel radius circle will do and wrap around the whole sprite.

To give it a little flavor, we can add an AnimationPlayer we will use to make the item float up and down off the ground. It will look like it’s hovering and draw attention to itself so the player doesn’t miss its presence.

I gave it an animation named “Float” that raises the Sprite up and down over 2 seconds. The animation’s set to cycle.

We also add a Tween node. When the player does pick up the item, we want it to fling itself into their inventory pleasingly and satisfyingly. Since we don’t know where the player will be, Tween lets us set that programmatically.

The GroundItem script

Add a new script, GroundItem.gd, to the GroundItem node. Its job is to store a reference to the blueprint we provide it, set its graphics, and do some animation.

class_name GroundItem
extends Node2D

## Reference to the blueprint that was just dropped. Grabbed by the inventory
## GUI system when picked up.
var blueprint: BlueprintEntity

## Reference to the nodes that make up this scene so we can animate, tween, or
## toggle whether collisions work.
onready var collision_shape := $Area2D/CollisionShape2D
onready var animation := $AnimationPlayer
onready var sprite := $Sprite
onready var tween := $Tween


## Assigns a blueprint, sets graphics, and positions the item
func setup(_blueprint: BlueprintEntity, location: Vector2) -> void:
    blueprint = _blueprint
    
    # We configure the sprite to be exactly how the blueprint entity's sprite is
    # so it looks the same, just scaled down.
    # Note that for this code to work, every blueprint scene should have a `Sprite`
    # node named "Sprite".
    var blueprint_sprite := blueprint.get_node("Sprite")
    sprite.texture = blueprint_sprite.texture
    sprite.region_enabled = blueprint_sprite.region_enabled
    sprite.region_rect = blueprint_sprite.region_rect
    sprite.centered = blueprint_sprite.centered
    
    global_position = location
    
    # Trigger the "pop" animation, where the item goes flying out of where the
    # entity was deconstructed.
    _pop()


## Animates the item so it flies to the player's position before being erased.
func do_pickup(target: KinematicBody2D) -> void:
    # We start with a speed of 10% of the distance. The item starts slow.
    var travel_distance := 0.1
    
    # Prevent the collision of the shape from working. Otherwise, we might pick up
    # the same item twice in a row!
    collision_shape.set_deferred("disabled", true)

    # We'll manually break out of the loop when it's time, so keep looping.
    while true:
        # Calculate the distance to the player.
        var distance_to_target := global_position.distance_to(target.global_position)
        # Break out of the loop once we're inside of 5 pixels, which is sufficiently "on top."
        if distance_to_target < 5.0:
            break

        # Interpolate the current position of the ground item by a percentage
        # of the way to the target's position.
        global_position = global_position.move_toward(target.global_position, travel_distance)
        # In our case, starting at 10% and increasing by another 10 every frame,
        # Ramping slow to fast.
        travel_distance += 0.1
        
        # Yield out of the function call until the next frame where we can keep animating.
        yield(get_tree(), "idle_frame")

    # Erase the ground item now that it's reached the player.
    queue_free()


## Animates the item flying out of its starting position in an arc up and down,
## like popcorn.
func _pop() -> void:
    # PI in radians is half a circle, or 180 degrees. So this takes the up direction
    # and rotates it a random amount left and right.
    var direction := Vector2.UP.rotated(rand_range(-PI, PI))
    
    # In our isometric perspective, every vertical pixel is half a horizontal pixel.
    direction.y /= 2.0
    # Pick a random distance between 20 and 70 pixels.
    direction *= rand_range(20, 70)

    # Pre-calculate the final position
    var target_position := global_position + direction
    
    # Pre-calculate a point halfway horizontally between start and end point,
    # but twice as high. `sign()` returns -1 if the value is negative and 1 if
    # positive, so we can use it to keep the vertical direction upwards.
    var height_position := global_position + direction * Vector2(0.5, 2 * -sign(direction.y))

    # Interpolate from the start to the middle point
    tween.interpolate_property(
        self,
        "global_position",
        global_position,
        height_position,
        0.15,
        Tween.TRANS_SINE,
        Tween.EASE_OUT
    )
    # then middle point to end point. We delay this tween by the duration of the
    # previous tween.
    tween.interpolate_property(
        self, "global_position", height_position, target_position, 0.25, 0, Tween.EASE_IN, 0.15
    )
    tween.start()
    
    # Wait until all tweens that we created have finished
    yield(tween, "tween_all_completed")
    animation.play("Float")

Making player pick up items

The player needs a way to detect if it got close enough to a ground item to tell it we’re picking it up. So open Player.tscn and add a new Area2D node named PickupRadius with a CollisionShape2D child. Assign it a CircleShape2D with a healthy radius. I went with a 100-pixel radius, though how far items can be is a matter of game balance and testing.

Select the PickupRadius area and in the Node dock, connect the area_entered signal to a function on the Player node. In this function, we’ll trigger a new global event the GUI can detect and trigger the inventory pick up.

func _on_PickupRadius_area_entered(area: Area2D) -> void:
    # Get the area's parent - that's the actual blueprint entity class.
    var parent: GroundItem = area.get_parent()
    if parent:
        # Triggers an event on our event bus pattern about an item getting
        # picked up. This signal can be connected to by the GUI.
        Events.emit_signal("entered_pickup_area", parent, self)

In Events.gd, make sure to add the new signal.

## Signal emitted when the player has arrived at an item that can be picked up
signal entered_pickup_area(entity, player)

In the next lesson, we’ll finish this up by adding some helper functions to the GUI to find the nearest available inventory panel that could contain the item and triggering the pickup animation.

Causing the entity to drop

Now we’re back in EntityPlacer.gd and we need to change what happens when we deconstruct an item. We need to check if it has a blueprint it can drop. If it does, we create a new ground item and set it up.

## The ground item packed scene we instance when dropping items
var GroundItemScene := preload("res://Entities/GroundItem.tscn")


func _unhandled_input(event: InputEvent) -> void:
    #...
    # Replace the existing code block where we checked for the "drop" input action with the following.
    elif event.is_action_pressed("drop") and _gui.blueprint:
        if is_on_ground:
            _drop_entity(_gui.blueprint, global_mouse_position)
            _gui.blueprint = null


func _finish_deconstruct(cellv: Vector2) -> void:
    #...
    
    # Get the entity's name so we can check if we have access to a blueprint.
    var entity_name := Library.get_entity_name_from(entity)
    # We convert the map position to a global position.
    var location := map_to_world(cellv)
    # If we do have a blueprint, we get it as a packed scene.
    if Library.blueprints.has(entity_name):
        
        var Blueprint: PackedScene = Library.blueprints[entity_name]

        _drop_entity(Blueprint.instance(), location)
    
    #_tracker.remove_entity(cellv)
    #...


## Creates a new ground item with the given blueprint and sets it up at the
## deconstructed entity's location.
func _drop_entity(entity: BlueprintEntity, location: Vector2) -> void:
    # We instance a new ground item, add it, and set it up
    var ground_item := GroundItemScene.instance()
    add_child(ground_item)
    ground_item.setup(entity, location)

If you run the game now and place down some objects, then deconstruct them, you’ll see their little items popping into existence.

You can’t pick them up yet, though. We’ll add that feature in the next lesson. Also, note that you can’t drop them by pressing the Q key either yet.

Code reference

GroundItem.gd

class_name GroundItem
extends Node2D

var blueprint: BlueprintEntity

onready var collision_shape := $Area2D/CollisionShape2D
onready var animation := $AnimationPlayer
onready var sprite := $Sprite
onready var tween := $Tween


func setup(_blueprint: BlueprintEntity, location: Vector2) -> void:
    blueprint = _blueprint

    var blueprint_sprite := blueprint.get_node("Sprite")
    sprite.texture = blueprint_sprite.texture
    sprite.region_enabled = blueprint_sprite.region_enabled
    sprite.region_rect = blueprint_sprite.region_rect
    sprite.centered = blueprint_sprite.centered

    global_position = location

    _pop()


func do_pickup(target: KinematicBody2D) -> void:
    var travel_distance := 0.1

    collision_shape.set_deferred("disabled", true)

    while true:
        var distance_to_target := global_position.distance_to(target.global_position)
        if distance_to_target < 5.0:
            break

        global_position = global_position.move_toward(target.global_position, travel_distance)
        travel_distance += 0.1

        yield(get_tree(), "idle_frame")

    queue_free()


func _pop() -> void:
    var direction := Vector2.UP.rotated(rand_range(-PI, PI))

    direction.y /= 2.0
    direction *= rand_range(20, 70)

    var target_position := global_position + direction

    var height_position := global_position + direction * Vector2(0.5, 2 * -sign(direction.y))

    tween.interpolate_property(
        self,
        "global_position",
        global_position,
        height_position,
        0.15,
        Tween.TRANS_SINE,
        Tween.EASE_OUT
    )
    tween.interpolate_property(
        self, "global_position", height_position, target_position, 0.25, 0, Tween.EASE_IN, 0.15
    )
    tween.start()

    yield(tween, "tween_all_completed")
    animation.play("Float")

Player.gd

extends KinematicBody2D

export var movement_speed := 200.0


func _physics_process(_delta: float) -> void:
    var direction := _get_direction()
    move_and_slide(direction * movement_speed)


func _get_direction() -> Vector2:
    return Vector2(
        (Input.get_action_strength("right") - Input.get_action_strength("left")) * 2.0,
        Input.get_action_strength("down") - Input.get_action_strength("up")
    ).normalized()


func _on_PickupRadius_area_entered(area: Area2D) -> void:
    var parent: GroundItem = area.get_parent()
    if parent:
        Events.emit_signal("entered_pickup_area", parent, self)

Events.gd

extends Node

signal entity_placed(entity, cellv)
signal entity_removed(entity, cellv)
signal systems_ticked(delta)
signal entered_pickup_area(entity, player)

EntityPlacer.gd

extends TileMap

const MAXIMUM_WORK_DISTANCE := 275.0
const POSITION_OFFSET := Vector2(0,25)
const DECONSTRUCT_TIME := 0.3

var GroundItemScene := preload("res://Entities/GroundItem.tscn")

var _gui: Control
var _tracker: EntityTracker
var _ground: TileMap
var _flat_entities: Node2D
var _player: KinematicBody2D
var _current_deconstruct_location := Vector2.ZERO

onready var _deconstruct_timer := $Timer


func _unhandled_input(event: InputEvent) -> void:
    var global_mouse_position := get_global_mouse_position()

    var has_placeable_blueprint: bool = _gui.blueprint and _gui.blueprint.placeable
    var is_close_to_player := (
        global_mouse_position.distance_to(_player.global_position)
        < MAXIMUM_WORK_DISTANCE
    )

    var cellv := world_to_map(global_mouse_position)
    var cell_is_occupied := _tracker.is_cell_occupied(cellv)
    var is_on_ground := _ground.get_cellv(cellv) == 0

    if event is InputEventMouseButton:
        _abort_deconstruct()

    if event.is_action_pressed("left_click"):
        if has_placeable_blueprint:
            if not cell_is_occupied and is_close_to_player and is_on_ground:
                _place_entity(cellv)
                _update_neighboring_flat_entities(cellv)

    elif event.is_action_pressed("right_click") and not has_placeable_blueprint:
        if cell_is_occupied and is_close_to_player:
            _deconstruct(global_mouse_position, cellv)

    elif event is InputEventMouseMotion:
        if cellv != _current_deconstruct_location:
            _abort_deconstruct()
        if has_placeable_blueprint:
            _move_blueprint_in_world(cellv)

    elif event.is_action_pressed("rotate_blueprint") and _gui.blueprint:
        _gui.blueprint.rotate_blueprint()

    elif event.is_action_pressed("drop") and _gui.blueprint:
        if is_on_ground:
            _drop_entity(_gui.blueprint, global_mouse_position)
            _gui.blueprint = null


func _process(_delta: float) -> void:
    var has_placeable_blueprint: bool = _gui.blueprint and _gui.blueprint.placeable
    if has_placeable_blueprint and not _gui.mouse_in_gui:
        _move_blueprint_in_world(world_to_map(get_global_mouse_position()))


func setup(gui: Control, tracker: EntityTracker, ground: TileMap, flat_entities: YSort, player: KinematicBody2D) -> void:
    _gui = gui
    _tracker = tracker
    _ground = ground
    _player = player
    _flat_entities = flat_entities

    for child in get_children():
        if child is Entity:
            var map_position := world_to_map(child.global_position)
            _tracker.place_entity(child, map_position)


func _move_blueprint_in_world(cellv: Vector2) -> void:
    _gui.blueprint.display_as_world_entity()
    _gui.blueprint.global_position = get_viewport_transform().xform(
        map_to_world(cellv) + POSITION_OFFSET
    )

    var is_close_to_player := (
        get_global_mouse_position().distance_to(_player.global_position)
        < MAXIMUM_WORK_DISTANCE
    )
    var is_on_ground: bool = _ground.get_cellv(cellv) == 0
    var cell_is_occupied := _tracker.is_cell_occupied(cellv)

    if not cell_is_occupied and is_close_to_player and is_on_ground:
        _gui.blueprint.modulate = Color.white
    else:
        _gui.blueprint.modulate = Color.red

    if _gui.blueprint is WireBlueprint:
        WireBlueprint.set_sprite_for_direction(_gui.blueprint.sprite, _get_powered_neighbors(cellv))


func _place_entity(cellv: Vector2) -> void:
    var entity_name := Library.get_entity_name_from(_gui.blueprint)
    var new_entity: Node2D = Library.entities[entity_name].instance()

    if _gui.blueprint is WireBlueprint:
        var directions := _get_powered_neighbors(cellv)
        _flat_entities.add_child(new_entity)
        WireBlueprint.set_sprite_for_direction(new_entity.sprite, directions)
    else:
        add_child(new_entity)

    new_entity.global_position = map_to_world(cellv) + POSITION_OFFSET
    new_entity._setup(_gui.blueprint)
    _tracker.place_entity(new_entity, cellv)

    if _gui.blueprint.stack_count == 1:
        _gui.destroy_blueprint()
    else:
        _gui.blueprint.stack_count -= 1
        _gui.update_label()


func _deconstruct(event_position: Vector2, cellv: Vector2) -> void:
    _deconstruct_timer.connect(
        "timeout", self, "_finish_deconstruct", [cellv], CONNECT_ONESHOT
    )
    _deconstruct_timer.start(DECONSTRUCT_TIME)
    _current_deconstruct_location = cellv


func _finish_deconstruct(cellv: Vector2) -> void:
    var entity := _tracker.get_entity_at(cellv)

    var entity_name := Library.get_entity_name_from(entity)
    var location := map_to_world(cellv)
    
    if Library.blueprints.has(entity_name):
        var Blueprint: PackedScene = Library.blueprints[entity_name]

        _drop_entity(Blueprint.instance(), location)

    _tracker.remove_entity(cellv)
    _update_neighboring_flat_entities(cellv)


func _drop_entity(entity: BlueprintEntity, location: Vector2) -> void:
    var ground_item := GroundItemScene.instance()
    add_child(ground_item)
    ground_item.setup(entity, location)


func _abort_deconstruct() -> void:
    if _deconstruct_timer.is_connected("timeout", self, "_finish_deconstruct"):
        _deconstruct_timer.disconnect("timeout", self, "_finish_deconstruct")
    _deconstruct_timer.stop()


func _get_powered_neighbors(cellv: Vector2) -> int:
    var direction := 0

    for neighbor in Types.NEIGHBORS.keys():
        var key: Vector2 = cellv + Types.NEIGHBORS[neighbor]

        if _tracker.is_cell_occupied(key):
            var entity: Node = _tracker.get_entity_at(key)
            if (
                entity.is_in_group(Types.POWER_MOVERS)
                or entity.is_in_group(Types.POWER_RECEIVERS)
                or entity.is_in_group(Types.POWER_SOURCES)
            ):
                direction |= neighbor

    return direction


func _update_neighboring_flat_entities(cellv: Vector2) -> void:
    for neighbor in Types.NEIGHBORS.keys():
        var key: Vector2 = cellv + Types.NEIGHBORS[neighbor]
        var object = _tracker.get_entity_at(key)

        if object and object is WireEntity:
            var tile_directions := _get_powered_neighbors(key)
            WireBlueprint.set_sprite_for_direction(object.sprite, tile_directions)