I'm making a grid based 2d puzzle game where you move each character once, and then end your turn. When your turn ends, water spreads to all surounding free grids next to the current water blocks. If water enters an area inhabited by a character, the character drowns. I can get the water to spread the first round, but the created water blocks don't continue to spread
extends Area2D
signal next_turn
@onready var check_up = $CollisionShape2D/ray_up
@onready var check_down = $CollisionShape2D/ray_down
@onready var check_left = $CollisionShape2D/ray_left
@onready var check_right = $CollisionShape2D/ray_right
var water_block = ["res://water.tscn", ]
var water_block_count = 1
# Called when the node enters the scene tree for the first time.
func _ready():
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
pass
func _on_hud_end_turn():
var count = 0
var new_water = 0
while count != water_block_count:
var water_scene = load(water_block[count])
if check_up && check_down && check_left && check_right:
water_block.remove_at(count)
new_water -= 1
if not check_up.is_colliding():
var water = water_scene.instantiate()
water.position += Vector2(0, -64)
add_child(water)
count += 1
new_water += 1
water_block[count] = get_node(water)
if not check_down.is_colliding():
var water = water_scene.instantiate()
water.position += Vector2(0, 64)
add_child(water)
count += 1
new_water += 1
water_block[count] = get_node(water)
if not check_left.is_colliding():
var water = water_scene.instantiate()
water.position += Vector2(-64, 0)
add_child(water)
count += 1
new_water += 1
water_block[count] = get_node(water)
if not check_right.is_colliding():
var water = water_scene.instantiate()
water.position += Vector2(64, 0)
add_child(water)
count += 1
new_water += 1
water_block[count] = get_node(water)
water_block_count += new_water
next_turn.emit()
This is my current code block... it isnt working at all. I first tried to have my water be a packed scene in inself. This allowed me to instantiate the new water blocks being created. The problem was it would only check from the packed scene. This was my attempt to fix that by saving the water child node created into an array so i could call that array to create spread at that node. I also added a part in the beginning to delete nodes that are no long spreading from the array. I get "Invalid type in function 'get_node' in base 'Area2D (Water.gd)'. Cannot convert argument 1 from Object to NodePath"
I'll go over this quickly, as I believe the design is wrong, and so fixing the errors will not result in the correct behavior.
We will begin by looking at this code:
water_block[count] = get_node(water)
First, the method get_node
takes a NodePath
return a Node
or fails. In your code it is failing, because you are passing water
to get_node
... And water
is set to the result of instantiate
, which means that water
is already a Node
. Making the call to get_node
pointless.
Second, you have a water_block
defined as an Array
containing a String
(a scene path). The code expects water_block
to contain scene paths, since you call load
on its members. Meaning that if you add a Node
to it (either water
or the result from get_node
) then load
will fail. In fact, the only relevant scene path is "res://water.tscn"
so you do not need an array for them at all.
Third, if you are always loading the same scene, ideally you would preload it:
const water_scene = preload("res://water.tscn")
But since the script at hand is attached to that scene, we have a scene that depends on itself. And that makes me question the design. The issues being that:
RayCast2D
for all the instances in the array.Turns
Before we deal with water, you have a turn system.
Ideally, anything that needs to run when the turn ends should be notified of the end of turn. I do not have details on how it currently works, but I'm suggesting to change it...
Three options come to mind:
For your case, I'm going to suggest node groups. This is the plan:
Water and an anything else that needs to run when the turn ends, will add themselves to a node group. I'm calling it on_turn
, you can call it whatever you want to. Set the group in the scene from the editor, see Groups.
We will also declare a method that will be called when the turn ends. I'll name it on_hud_end_turn
based on the code you have. Again, it can be anything. Whatever name you pick, stick to it.
func on_hud_end_turn() -> void:
# CODE HERE
pass
When the turn ends, you will not have a signal, instead you will call that method on the nodes in the node group. There is no check that the method name matches (you will not get a warning telling you the name you wrote does not match), so make sure it is the same.
get_tree().call_group(&"on_turn", &"on_hud_end_turn")
Water
If we have adding itself to the group to get end of turn notifications, all water instances can spread... However, for water to spread, it requires coordination between instances to avoid multiple instances of water can spawn water on the same location.
If water does not check for water:
00000 00000 00100
00000 00100 02220
00100 -> 01110 -> 12521 -> …
00000 00100 02220
00000 00000 00100
Each number is how many instances of water are there.
If water checks for water:
00000 00000 00100
00000 00100 02120
00100 -> 01110 -> 11111 -> …
00000 00100 02120
00000 00000 00100
Each number is how many instances of water are there.
To orchestrate the instances, we will use an static
Dictionary
:
static
so all the instance share the same one.Dictionary
so we can query it with a key, in this case the key will be the position.static var _instances:Dictionary = {}
Here I'm following the naming convention to use _
as prefix for members that should not be acceded from outside.
Actually, let us initialize it with an empty Dictionary
:
static var _instances := {}
This variable is still statically typed as Dictionary
, but now the type is implicit.
The code will be like this:
extends Area2D
const grid_size := Vector2(64, 64)
@onready var _check_up:RayCast2D = $CollisionShape2D/ray_up
@onready var _check_down:RayCast2D = $CollisionShape2D/ray_down
@onready var _check_left:RayCast2D = $CollisionShape2D/ray_left
@onready var _check_right:RayCast2D = $CollisionShape2D/ray_right
static water_scene:PacketScene = load("res://water.tscn")
static var _instances := {}
func on_hud_end_turn():
var grid_position := Vector2i(
position.x / grid_size.x,
position.y / grid_size.y
)
if not _check_up.is_colliding():
_ensure_water_at(grid_position + Vector2i(0, -1))
if not _check_down.is_colliding():
_ensure_water_at(grid_position + Vector2i(0, 1))
if not _check_left.is_colliding():
_ensure_water_at(grid_position + Vector2i(-1, 0))
if not _check_right.is_colliding():
_ensure_water_at(grid_position + Vector2i(1, 0))
func _ensure_water_at(new_grid_position:Vector2i) -> void:
var instance:Area2D = _instances.get(new_grid_position, null)
if not is_instance_valid(instance):
instance = water_scene.instantiate()
instance.position = Vector2(
grid_size.x * new_grid_position.x,
grid_size.y * new_grid_position.y,
)
add_sibling(instance)
_instances[new_grid_position] = instance
Let us go over the code.
Note: we might add a name to this class, although I have little incentive to do it, it would be like this class_name Water
.
The script is to be attached to an Area2D
, I do not have a reason to change that:
extends Area2D
This is the size of the grid:
const grid_size := Vector2(64, 64)
We want it to be able to use Vector2i
as keys for the Dictionary
, and so avoiding floating point comparisons.
Then we have are the RayCast2D
, they are the same you had them, except:
_
, following naming convention.@onready var _check_up:RayCast2D = $CollisionShape2D/ray_up
@onready var _check_down:RayCast2D = $CollisionShape2D/ray_down
@onready var _check_left:RayCast2D = $CollisionShape2D/ray_left
@onready var _check_right:RayCast2D = $CollisionShape2D/ray_right
These lines should fail if no Node
is present at the given path, and by specifying the type explicitly, they should also fail if the the Node
at those paths are not of the correct type... If you get either of those errors, you know you need to fix the scene.
This is the scene we will be instantiating (I have changed it from a const preload
):
static water_scene:PacketScene = load("res://water.tscn")
We want it to be able to create new instances.
This is the Dictionary
with our instances:
static var _instances := {}
Then we have on_hud_end_turn
:
func on_hud_end_turn():
var grid_position := Vector2i(
position.x / grid_size.x,
position.y / grid_size.y
)
if not _check_up.is_colliding():
_ensure_water_at(grid_position + Vector2i(0, -1))
if not _check_down.is_colliding():
_ensure_water_at(grid_position + Vector2i(0, 1))
if not _check_left.is_colliding():
_ensure_water_at(grid_position + Vector2i(-1, 0))
if not _check_right.is_colliding():
_ensure_water_at(grid_position + Vector2i(1, 0))
Notes:
_ready
) see Groups.call_group
instead of the signal you have._ensure_water_at
with the position were we want to create water.Vector2i
, as mentioned earlier, this is to avoid floating point comparisons.This will make more sense by looking at _ensure_water_at
. The method _ensure_water_at
will check if a position already has water, and if not, it will create it. We will look at it part by part.
Note that _ensure_water_at
is declared to take a Vector2i
called new_grid_position
which is where the new instance should be placed if necesary:
func _ensure_water_at(new_grid_position:Vector2i) -> void:
First we will get whatever instance is in that position:
var instance:Area2D = _instances.get(new_grid_position, null)
Here get
allows us to specify a default value (null
) which we get in case there is nothing in that position in the Dictionary
.
Thus instance
can either be null
or an instance... However, that instance might be invalid (e.g. it was removed with queue_free
but not removed from the dictionary). We check both things at once with is_instance_valid
:
if not is_instance_valid(instance):
So now we know that either we got a null
or an invalid instance, in either case we will create a new instance:
instance = water_scene.instantiate()
We need to set its position, which we get by multiplication the new_grid_position
by the grid_size
:
instance.position = Vector2(
grid_size.x * new_grid_position.x,
grid_size.y * new_grid_position.y,
)
And we need to add it to the scene tree:
add_sibling(instance)
I'm using add_sibling
so the new instance is not a child of the current one.
And, of course, we have to update the Dictionary
:
_instances[new_grid_position] = instance
As final note, I want to point out that I removed next_turn
from this code. Although I do not know how the turn code works in general, I know that in the old code nothing would have been listening to next_turn
. With the use of call_group
you can write code right after. I recommend to use a deferred call to go to the next turn to allow a frame in between.
If I'm not skipping over something, this should work.
Dictionary
in _exit_tree
. In the version of the code in this answer, you would compute the key form the position as it is done in on_hud_end_turn
and use it to call erase
on the Dictionary
.preload
the scene on the script that is attached to the same scene, because that is cyclic dependency between the script and the scene, and Godot is not handling it correctly (at least not at the time of writing).pack
on the scene solved problems for OP (it is not clear to me why). You can create a PackedScene
then call pack
on it passing a Node
to store it (and all its children that has it as owner
) in the PackedScene
. Then you could use the PackedScene
make more instances, or to save it with ResourceSaver
. I think this is a roundabout way to duplicate
the Node
s... So for anybody finding this answer: consider using duplicate
instead of using an PackedScene
(this would also work around the cyclic dependency mentioned above).