Anyone can dig up a bit of soil and pick up some twigs off the ground, or scoop up a rock. But when faced with a boulder or an entire tree, you can’t take those down with your bare hands.
In this lesson, we’ll add a way to require tools like axes and pickaxes to deconstruct certain entities like boulders and trees.
We can start this off by creating some tools so we have items to work with. Let’s start with an axe.
Create a new scene with a BlueprintEntity
named AxeBlueprint as the root node. Add a Sprite
node as a child of it and and assign it the blueprints.svg
texture.
Set its region to be the pristine metal axe by turning on Region -> Enabled and selecting the right slice in the TextureRegion bottom panel.
To make it a tool and be able to recognize it in code, we can add a separate script that extends the functionality we already have on blueprints.
Assign a new script, ToolEntity.gd
, to the AxeBlueprint. You can save it in the res://Entities/Blueprints/
directory.
We’ll re-use this script for every tool.
class_name ToolEntity extends BlueprintEntity # This property is a multiplier for the deconstruct. With it, bad tools can be # slower, and high-quality tools can work faster. # `1.0` is the default speed. export var tool_speed := 1.0 # The category the tool belongs to, like "Axe" or "Pickaxe". This lets us # identify what kind of tool this is. # For example, an axe can cut trees but not break boulders. export var tool_name := ""
With the script assigned, you can configure the AxeBlueprint.tscn
scene as an unplaceable tool with the “Axe” tool name and a tool speed of 1.
If you go back to the main Simulation.tscn
scene and use the GUI
’s node debug dictionary to give yourself an axe, upon playing the game, you may notice something odd with the sprite.
Because we are using auto-slice in the TextureRegion tool panel, we do not have an exact 100x100 pixel sprite, and that changes what counts as its center.
Like we did with the Stirling engine and battery, we have to offset it by 25
or so pixels to make up the difference.
You can now repeat the process to create a pickaxe tool for stone and ore.
To do this efficiently, you can duplicate the AxeBlueprint.tscn
file and edit the new copy. Be sure to name the root node PickaxeBlueprint and to set its Tool Name property to “Pickaxe.”
In the final demo, I repeated the process to create a “Crude Axe” and a “Crude Pickaxe”.
They still use the “Axe” and “Pickaxe” values for the Tool Name property, but the player creates them with stone instead of metal. They should be less efficient: I set with 0.1
for their Tool Speed property.
If you do the same, you should end up with an AxeBlueprint.tscn
, PickaxeBlueprint.tscn
, CrudeAxeBlueprint.tscn
, and CrudePickaxeBlueprint.tscn
.
You can see the sprites for the “crude” versions of the tools on the left in the image below.
While we’re making entities, we should create an object that requires a tool to mine.
We’ll go with the boulder. Like the rocks we made in the previous chapter, it will drop stone, except it will drop more than one.
You will never hold a boulder in your inventory, so we don’t have to create a blueprint for it.
We create a new scene with a StaticBody2D
named BoulderEntity at its root node. It should have a Sprite
and, for collision, a CollisionPolygon2D
.
The sprite has its texture assigned to the tileset.svg
texture with its regions data set to one of the boulders.
With the sprite displayed on the screen, we can use it as a reference to build an appropriate polygon on the CollisionPolygon2D
node.
Notice how I moved the sprite up a bit below to align its base with the scene’s origin.
You’ll have to test it in-game to see how it feels when the player walks around it.
Note that you can edit a collision shape while the game is running; it will update live. To do so, ensure you have Debug -> Synchronize Scene Changes and Debug -> Visible Collision Shapes turned on.
Like with rocks and twigs, we have more than one possible sprite, so we can use a script to pick one at random when the boulder is first created. Add a new script, BoulderEntity.gd
, to the root node. It should extend Entity
as a class.
extends Entity ## These are the three regions in `tileset.svg` for the boulder sprites. const REGIONS := [ Rect2(10, 780, 100, 100), Rect2(120, 780, 100, 100), Rect2(230, 780, 100, 100) ] func _ready() -> void: ## We set the sprite region to a random region. var index := int(rand_range(0, REGIONS.size() - 1)) $Sprite.region_rect = REGIONS[index]
We have an issue in that the boulders’ sprites don’t have the same shape for where they meet the ground.
We can create three collision polygon nodes and disable the ones we’re not using.
Create two more CollisionPolygon2D
nodes and for each, change the Sprite
‘s’ region to one of the two remaining boulders and draw a collision polygon for that boulder.
Make sure that the order they appear in the scene tree is in the same as they are in the REGIONS
array. That is, from the leftmost to the rightmost sprite.
Hide them and set their Disabled property to true
so those we keep hidden don’t interfere with collisions.
Then head back to _ready()
, and we can finish the function and enable the right collision polygon.
func _ready() -> void: #... ## Enable the appropriate collision polygon. Sprite is child 0, ## so `index` + 1 should be the correct child. var collision: CollisionPolygon2D = get_child(index + 1) collision.disabled = false collision.show() ## We randomly flip the entire boulder on the X axis for more visual variety. scale.x = 1 if rand_range(0, 10) < 5 else -1
At the moment, with the way we have our Library set up, if we deconstructed this boulder, it would try to spawn a boulder entity.
Not only does it not exist, but we also don’t want it to spawn a boulder: we want it to drop a bunch of stone. But we already covered that eventuality.
In our Library.gd
autoload, we already check if the entity has a special get_entity_name()
function. So when the deconstruct code executes, we can tell it the name is “Stone” so it drops stone instead of boulders.
## Tells the Library what name it should pretend to wear instead of its scene name. func get_entity_name() -> String: return "Stone"
That means it will display the name of the entity as “Stone” when hovered over with the mouse because of how we have our information label set up.
If you’d like to separate the two, I leave it as an exercise to you, dear reader.
To specify the number of stones to drop, you can override the _get_pickup_count()
getter. I went with 10
stones, but you could also return a random amount if you’d like.
func _get_pickup_count() -> int: return 10
Before we move on, make sure to select the BoulderEntity
root node and set its Deconstruct Filter property to “Pickaxe”. This will only make it possible to deconstruct it with a pickaxe.
This is what this lesson is all about, after all.
The code we wrote in the previous chapters doesn’t take the amount returned by _get_pickup_count()
into account, so only one stone will drop.
To produce more, we need to update EntityPlacer._finish_deconstruct()
.
We update the call to drop_entity()
by making it in a loop.
Open EntityPlacer.gd
and move the line that says _drop_entity(Blueprint.instance(), location)
inside a loop.
func _finish_deconstruct(cellv: Vector2) -> void: #... #if Library.blueprints.has(entity_name): #... for _i in entity.pickup_count: _drop_entity(Blueprint.instance(), location) #...
You can now mine the boulder and get a dozen units of stone.
Except you’re still doing it with your bare hands.
Hands might be a handy tool, they’re not the right one for this job.
We have a pickaxe, let’s make it so we have to use it.
While we’re in EntityPlacer.gd
, in _deconstruct()
, we can check for tools there.
We can change the amount of time it takes while we’re at it if we’re using a crude tool, too.
Be mindful of the commented lines of code below. I included them so you can see where to add the new code.
func _deconstruct(event_position: Vector2, cellv: Vector2) -> void: # Get the blueprint in the player's hand currently. var blueprint: BlueprintEntity = _gui.blueprint var blueprint_name := "" # If it's a tool, get its custom tool name, otherwise just get its normal # name from the library. if blueprint and blueprint is ToolEntity: blueprint_name = blueprint.tool_name elif blueprint: blueprint_name = Library.get_entity_name_from(blueprint) # Check the entity we're trying to decosntruct. var entity := _tracker.get_entity_at(cellv) # If it has a deconstruct filter property, and the blueprint we're holding # in our hand is part of the filter, then we can proceed. Otherwise, return # so we don't deconstruct it. 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 #_deconstruct_timer.start(DECONSTRUCT_TIME * modifier) #_current_deconstruct_location = cellv
That should make is so you need to select the pickaxe with your mouse before you can deconstruct a boulder.
In the next lesson, we’ll add lumbers and ore boulders, and polish the deconstruction mechanic.
ToolEntity.gd
class_name ToolEntity extends BlueprintEntity export var tool_speed := 1.0 export var tool_name := ""
BoulderEntity.gd
extends Entity const REGIONS := [ Rect2(10, 780, 100, 100), Rect2(120, 780, 100, 100), Rect2(230, 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 else -1 func get_entity_name() -> String: return "Stone" func _get_pickup_count() -> int: return 10
EntityPlacer.gd
’s _deconstruct()
and _finish_deconstruct()
functions.
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 _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] for _i in entity.pickup_count: _drop_entity(Blueprint.instance(), location) _tracker.remove_entity(cellv) _update_neighboring_flat_entities(cellv)