Whether it’s to gather resources, or because they placed an object by mistake, the player needs more controls than only placing entities.
They should be able to pick them back up and put them somewhere else, disassemble machinery to re-do their power setup, or to make space for new machines.
In this lesson, we’ll add this feature by adding deconstruction methods to EntityPlacer.gd
.
If the player tries to deconstruct a machine but they click on the one next to it by accident, it’d be frustrating if the machine instantly went away. It could also interrupt important work in the player’s assembly line!
That’s why we’ll make the deconstruction process take a short while. You hold the right mouse button down, a bar fills up, and if you’re still holding the button on the target by the end of it, the entity breaks down and gets dropped to the ground. You can release the button anytime to cancel the operation.
For now, we’ll only implement the feature without displaying an animated progress bar, which we’ll cover in the next chapter, when we get to the user interface.
We need a dedicated Timer node as a child of EntityPlacer we’ll use to time the deconstruction input.
We can store a reference to it in an onready
variable. Then, in _unhandled_input()
, we trigger the deconstruction process when the player right-clicks on a valid entity.
Open the EntityPlacer.gd
script to add the following code.
#... ## Base time in seconds it takes to deconstruct an item. const DECONSTRUCT_TIME := 0.3 #... ## The variable below keeps track of the current deconstruction target cell. If the mouse moves ## to another cell, we can abort the operation by checking against this value. var _current_deconstruct_location := Vector2.ZERO #... onready var _deconstruct_timer := $Timer func _unhandled_input(event: InputEvent) -> void: #... #if event.is_action_pressed("left_click"): #... # When right clicking... elif event.is_action_pressed("right_click") and not has_placeable_blueprint: # ...onto a tile within range that has an entity in it, if cell_is_occupied and is_close_to_player: # we remove that entity. _deconstruct(global_mouse_position, cellv)
Above, we call a _deconstruct()
function we have yet to define. Let’s add it at the bottom of the script.
The _deconstruct()
method does not instantly take out the item. Instead, it starts the process of deconstructing it. It connects to the timer with the appropriate time and targeted cell coordinates, which we can store to check if we should later abort the operation.
## Begin the deconstruction process at the current cell func _deconstruct(event_position: Vector2, cellv: Vector2) -> void: # We connect to the timer's `timeout` signal. We pass in the targeted tile as a # bind argument and make sure that the signal disconnects after emitting once # using the CONNECT_ONESHOT flag. This is because once the signal has triggered, # we do not want to have to disconnect manually. Once the timer ends, the deconstruct # operation ends. _deconstruct_timer.connect( # We call the `_finish_deconstruct()` function when the timer times out. We'll code it next. "timeout", self, "_finish_deconstruct", [cellv], CONNECT_ONESHOT ) # We then start the timer and store the cell we're targeting, which allows us to cancel # the operation if the player's mouse moves to another cell. _deconstruct_timer.start(DECONSTRUCT_TIME) _current_deconstruct_location = cellv
Once the timer times out, that means it’s time to deconstruct the entity. If the player deconstructs an inventory item, we want it to drop on the floor and become an item to pick up. We’ll code that part later. Until we get there, we will just ask the EntityTracker
to remove the entity.
## Finish the deconstruction and delete the entity from the game world. func _finish_deconstruct(cellv: Vector2) -> void: # This function will drop the deconstructed entity as a pickup item, # but we haven't implemented an inventory yet, so we only remove the entity. var entity := _tracker.get_entity_at(cellv) _tracker.remove_entity(cellv)
The player should be able to abort the deconstruction process.
To do so, we follow a couple of rules:
First, let’s write a function to abort the deconstruction process. We’ll code the function in such a way we can safely call it anytime. If the player is deconstructing an entity, the timer is connected to the node and running. In that case, we disconnect the signal and stop the timer.
## Disconnect from the timer if connected, and stop it from continuing, to prevent ## deconstruction from completing. func _abort_deconstruct() -> void: if _deconstruct_timer.is_connected("timeout", self, "_finish_deconstruct"): _deconstruct_timer.disconnect("timeout", self, "_finish_deconstruct") _deconstruct_timer.stop()
We can then call the function in _unhandled_input()
and implement the rules we listed above. You’ll have to insert some code at the start of the function, and some more below the line elif event is InputEventMouseMotion
, where we update the placement preview.
func _unhandled_input(event: InputEvent) -> void: # If the user releases right-click or clicks another mouse button, we can abort. # We catch all mouse button inputs here because the `_abort_deconstruct()` function # below will safely disconnect the timer and stop it. if event is InputEventMouseButton: _abort_deconstruct() #... #elif event.is_action_pressed("right_click") and not has_placeable_blueprint: #... elif event is InputEventMouseMotion: # If the mouse moves and slips off the target tile, then we can abort # the deconstruction process. if cellv != _current_deconstruct_location: _abort_deconstruct() #if has_placeable_blueprint: #...
With that code, you should be able to remove entities from the game board by doing a Right-click on them and holding the button. We don’t have a visual indicator yet for the player; we’ll add that in a future lesson, when working on the game’s user interface.
In the next few lessons, we’ll tackle powering machines and transferring energy between them using wires.
Here are is the complete EntityPlacer.gd
script with the additions from this lesson.
EntityPlacer.gd
extends TileMap const MAXIMUM_WORK_DISTANCE := 275.0 const POSITION_OFFSET := Vector2(0,25) const DECONSTRUCT_TIME := 0.3 var _blueprint: BlueprintEntity var _tracker: EntityTracker var _ground: TileMap var _player: KinematicBody2D var _current_deconstruct_location := Vector2.ZERO onready var Library := { "StirlingEngine": preload("res://Entities/Blueprints/StirlingEngineBlueprint.tscn").instance(), } onready var _deconstruct_timer := $Timer func _ready() -> void: Library[Library.StirlingEngine] = preload("res://Entities/Entities/StirlingEngineEntity.tscn") func _exit_tree() -> void: Library.StirlingEngine.queue_free() func _unhandled_input(event: InputEvent) -> void: var global_mouse_position := get_global_mouse_position() var has_placeable_blueprint: bool = _blueprint and _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) 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("drop") and _blueprint: remove_child(_blueprint) _blueprint = null elif event.is_action_pressed("quickbar_1"): if _blueprint: remove_child(_blueprint) _blueprint = Library.StirlingEngine add_child(_blueprint) _move_blueprint_in_world(cellv) func _process(_delta: float) -> void: var has_placeable_blueprint: bool = _blueprint and _blueprint.placeable if has_placeable_blueprint: _move_blueprint_in_world(world_to_map(get_global_mouse_position())) func setup(tracker: EntityTracker, ground: TileMap, player: KinematicBody2D) -> void: _tracker = tracker _ground = ground _player = player 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: _blueprint.global_position = 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: _blueprint.modulate = Color.white else: _blueprint.modulate = Color.red func _place_entity(cellv: Vector2) -> void: var new_entity: Node2D = Library[_blueprint].instance() add_child(new_entity) new_entity.global_position = map_to_world(cellv) + POSITION_OFFSET new_entity._setup(_blueprint) _tracker.place_entity(new_entity, cellv) 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) _tracker.remove_entity(cellv) func _abort_deconstruct() -> void: if _deconstruct_timer.is_connected("timeout", self, "_finish_deconstruct"): _deconstruct_timer.disconnect("timeout", self, "_finish_deconstruct") _deconstruct_timer.stop()