Adding trees, ore, and mining feedback

We can now wield tools and limit what the user can deconstruct without them. It would be a good time to add some more entities to take advantage of this, like a tree that drops some lumber.

The user experience is also a little lacking when deconstructing.

There’s no way to tell if it’s working until the entity pops open like a pinata, so we’ll address that too by creating a circular progress bar.

Tree and lumber

Chopping a big tree down and getting twigs would be a little unusual, so let’s add a new blueprint entity for lumber.

There’s nothing you haven’t done already here.

Create a new blueprint scene, LumberBlueprint.tscn, with a BlueprintEntity node named LumberBlueprint as its root.

Give it a Sprite as a child.

Set the sprite’s Texture to blueprints.svg and assign it the region for the lumber log.

I set its Stack Size to 25 and turned off the Placeable property.

You can save the scene in the Entities/Blueprints/ directory, like the others.

That’s the lumber set. Now we’ll create a tree.

Creating the trees

To have some flexibility with the trees’ design, I split them into three parts: foliage, the trunk, and roots.

Make a new entity scene, TreeEntity.tscn, with a StaticBody2D named TreeEntity as its root node.

Add three sprites as its child for the root, trunk, and foliage.

You should add the Roots first, the Trunk second, and the Foliage third, so they draw in that order.

Assign the tileset.svg texture to each one and set their regions to their respective tree parts.

Place the Roots so its origin point is where the ‘floor’ of the tree should be (I found it to be about at 0,-12), and assemble the trunks (about 0, -60) and foliage (about 0, -125) accordingly from there.

So the player can’t walk through the tree, add a CollisionPolygon2D and build its collision polygon around the trunk.

Your scene should look like this.

You might have noticed there are several variations for the foliage.

We’ll randomize the sprites using a new script, TreeEntity.gd, which we attach to the root node.

Like boulders, its main job is to randomize the foliage with the extra sprites and report itself as lumber the player can chop down.

extends Entity

## Each of the sprite regions with foliage.
const REGIONS := [
    Rect2(10, 560, 210, 210),
    Rect2(230, 560, 210, 210),
    Rect2(450, 560, 210, 210),
    Rect2(670, 560, 210, 210),
]


func _ready() -> void:
    # Assign random foliage as the region for the sprite.
    $Foliage.region_rect = REGIONS[int(rand_range(0, REGIONS.size() - 1))]
    # And flip it horizontally at random for extra variety.
    $Foliage.flip_h = rand_range(0, 10) < 5.5


## Returns the name "Lumber" so deconstructing a tree drops lumber.
func get_entity_name() -> String:
    return "Lumber"


# Harvesting a tree yield five lumbers.
func _get_pickup_count() -> int:
    return 5

Don’t forget to set the node’s Deconstruct Filter so the player needs to use an “Axe” to chop the tree down.

If you add tree instances to the Simulation scene’s EntityPlacer, you should be able to chop them down with the axe.

Ore boulders

The last entities we should add before we get into machining would be a way to mine ore.

We need an ore blueprint to turn into an ingot, and an ore entity to mine this ore from, like the boulders.

The process is the same as with lumber and the boulder we made in the previous lesson.

For the ore blueprint, duplicate the StoneBlueprint.tscn file, name it OreBlueprint.tscn, and rename its root node before editing its properties and texture.

I set its Stack Size to 50.

And assigned the ore texture to the Sprite, a vertical stone with white lines.

The ore boulder entity

For the ore boulder entity, it’s a repeat of what we did with the boulder.

You can start by duplicating BoulderEntity.tscn as OreBoulderEntity.tscn. Rename its root node and set its sprite to the ore boulder, as illustrated below.

You can remove the existing CollisionPolygon2D nodes and add two new ones.

Build their polygons to match both of the metal-streaked ore boulders.

Hide them and set their Disabled property to true.

Your scene tree should look like this.

Here’s how I set the collision shape for both my boulders.

Finally, remove the existing script from the root node and attach a new one to it, OreBoulderEntity.gd.

We use it to randomly assign sprite, collision data, orientation, and make it drop more than one ore blueprint.

There again, it’s a repeat of what we saw so far.

extends Entity

## Both regions that represent an ore boulder in the sprite sheet
const REGIONS := [
    Rect2(340, 780, 100, 100),
    Rect2(450, 780, 100, 100),
]


func _ready() -> void:
    # Get a random index and set the sprite and enable the correct collision
    var index := int(rand_range(0, REGIONS.size() - 1))
    $Sprite.region_rect = REGIONS[index]
    
    var collision: CollisionPolygon2D = get_child(index + 1)
    collision.disabled = false
    collision.show()
    
    # Randomly flip the entity for more variety
    scale.x = 1 if rand_range(0, 10) < 5.5 else -1


# We override the name to drop metal ore.
func get_entity_name() -> String:
    return "Ore"


func _get_pickup_count() -> int:
    return 10

Which leaves setting its Deconstruct Filter to “Pickaxe” to require that tool to break this down.

I hope by now, after a tree, lumber, ore, two kinds of boulders, and four tools, I’ve sufficiently emphasized how efficient it makes creating new entities when you keep them decoupled and naive.

The less they have to know and do, the less you have to do.

Deconstruction user feedback

As noted above, it’s currently impossible to tell if the deconstruction is working until the item is deconstructed.

Let’s add a progress bar to the GUI that fills up as you break something.

Create a new scene, with a TextureProgress node named DeconstructProgressBar as its root.

Set its Fill Mode to Clockwise to make the bar radial and assign the progress_circle.png texture to its Progress property.

Below, I also set its Value to 100 to see the bar full in the editor.

You can save the scene inside the GUI/ directory.

Then you can assign a new script, DeconstructProgressBar.gd, to the node.

Like DragPreview and InformationLabel, its job is to appear near the user’s mouse, so we don’t want it to get controlled by the GUI containers.

We need to set it as top-level so it controls its own position and size.

tool
extends TextureProgress


func _ready() -> void:
    set_as_toplevel(true)

Go into GUI.tscn and instance this new scene as a child of GUI.

We only want it to show up when we are deconstructing an entity, so we hide it by default.

Inside of GUI.gd, we can grab a reference to it to access it from anything that has a reference to the GUI.

onready var deconstruct_bar := $DeconstructProgressBar

To animate it, we can use a Tween node assigned to the EntityPlacer node.

So head back to Simulation.tscn and add a new Tween node as a child of EntityPlacer.

Then, in EntityPlacer.gd, we can get a reference to it and change _deconstruct() to make the progress bar appear under the mouse and start filling up.

Once again, I included existing lines of code inside comments to help you see where to add the new code.

onready var _deconstruct_tween := $Tween

func _deconstruct(event_position: Vector2, cellv: Vector2) -> void:
    #...
    #var modifier := 1.0 if not blueprint is ToolEntity else 1.0 / blueprint.tool_speed

    var deconstruct_bar: TextureProgress = _gui.deconstruct_bar

    # Just like we did it with the DragPreview, we need to transform the mouse's
    # position to translate it from the GUI canvas layer.
    deconstruct_bar.rect_global_position = (
        get_viewport_transform().xform(event_position)
        + POSITION_OFFSET
    )
    deconstruct_bar.show()
    
    _deconstruct_tween.interpolate_property(
        deconstruct_bar, "value", 0, 100, DECONSTRUCT_TIME * modifier
    )
    _deconstruct_tween.start()

    _deconstruct_timer.start(DECONSTRUCT_TIME * modifier)
    #_current_deconstruct_location = cellv

func _finish_deconstruct(cellv: Vector2) -> void:
    #...
    _gui.deconstruct_bar.hide()


func _abort_deconstruct() -> void:
    #...
    _gui.deconstruct_bar.hide()

Which leaves you with a nice, circular progress bar to tell the player how much progress they’ve made deconstructing an object.

If you find it a little big, you can open the DeconstructProgressBar scene and change the node’s Rect Scale property.

Code reference

TreeEntity.gd

extends Entity

const REGIONS := [
    Rect2(10, 560, 210, 210),
    Rect2(230, 560, 210, 210),
    Rect2(450, 560, 210, 210),
    Rect2(670, 560, 210, 210),
]


func _ready() -> void:
    $Foliage.region_rect = REGIONS[int(rand_range(0, REGIONS.size() - 1))]
    $Foliage.flip_h = rand_range(0, 10) < 5.5


func get_entity_name() -> String:
    return "Lumber"


func _get_pickup_count() -> int:
    return 5

OreBoulderEntity.gd

extends Entity

const REGIONS := [
    Rect2(340, 780, 100, 100),
    Rect2(450, 780, 100, 100),
]


func _ready() -> void:
    var index := int(rand_range(0, REGIONS.size() - 1))
    $Sprite.region_rect = REGIONS[index]
    
    var collision: CollisionPolygon2D = get_child(index + 1)
    collision.disabled = false
    collision.show()
    
    scale.x = 1 if rand_range(0, 10) < 5.5 else -1


func get_entity_name() -> String:
    return "Ore"


func _get_pickup_count() -> int:
    return 10

DeconstructProgressBar.gd

tool
extends TextureProgress


func _ready() -> void:
    set_as_toplevel(true)

The modified functions in EntityPlacer.gd are _deconstruct(), _finish_deconstruct(), and _abort_deconstruct().

onready var _deconstruct_tween := $Tween


func _deconstruct(event_position: Vector2, cellv: Vector2) -> void:
    var blueprint: BlueprintEntity = _gui.blueprint
    var blueprint_name := ""
    
    if blueprint and blueprint is ToolEntity:
        blueprint_name = blueprint.tool_name
    elif blueprint:
        blueprint_name = Library.get_entity_name_from(blueprint)

    var entity := _tracker.get_entity_at(cellv)
    
    if (
        not entity.deconstruct_filter.empty()
        and (not blueprint or not blueprint_name in entity.deconstruct_filter)
    ):
        return

    _deconstruct_timer.connect(
        "timeout", self, "_finish_deconstruct", [cellv], CONNECT_ONESHOT
    )

    var modifier := 1.0 if not blueprint is ToolEntity else 1.0 / blueprint.tool_speed

    var deconstruct_bar: TextureProgress = _gui.deconstruct_bar

    deconstruct_bar.rect_global_position = (
        get_viewport_transform().xform(event_position)
        + POSITION_OFFSET
    )
    deconstruct_bar.show()
    
    _deconstruct_tween.interpolate_property(
        deconstruct_bar, "value", 0, 100, DECONSTRUCT_TIME * modifier
    )
    _deconstruct_tween.start()

    _deconstruct_timer.start(DECONSTRUCT_TIME * modifier)
    _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]

        for _i in entity.pickup_count:
            _drop_entity(Blueprint.instance(), location)

    
    _tracker.remove_entity(cellv)
    _update_neighboring_flat_entities(cellv)
    _gui.deconstruct_bar.hide()


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