Fireballs - is there way to make burned/smoked area?

I’m currently quite happy how fireball turned out with sound and impact blast, but missing one thing to make smoked/burned area which this fireball hits.

Some idea how to make it ? some decals or other way to do it ?

1 Like

Maybe a combination of decals and some particles? Though with decals you tend to hit a limit pretty quickly I think, have to look into counting/ despawning them procedurally or on a timer

1 Like

Use the normal of the surface you hit to place a decal

1 Like

What role the particles would play? I thought about smoke but for fantasy it’s unnecessary.

Yes the timer is good idea, maybe some other mechanic which stop from being layered in one spot.

1 Like

Do you mind elaborating a bit on normals? Do you mean get collisions normal or normal as resource for decal?

For now only albedo texture and global_position being used for it

Obviously there is no effect on houses or tree’s .

@Captain_Euphoria is certainly talking about the collision’s normal. If you are only using global_position the decal might not project properly on angled surfaces.

1 Like

my current code looks like this

extends RayCast3D

@export var speed := 50.0
@export var explosion_scene: PackedScene
@export var decal_scene: PackedScene	
@onready var remote_transform = RemoteTransform3D.new()
@onready var audio_stream_player_3d: AudioStreamPlayer3D = $AudioStreamPlayer3D

func _ready() -> void:
	audio_stream_player_3d.play()

func _physics_process(delta: float) -> void:
	position += global_basis * Vector3.FORWARD * delta * speed
	target_position = Vector3.FORWARD * delta * speed
	force_raycast_update()
	var collider = get_collider()
	if is_colliding():
		audio_stream_player_3d.stop()
		global_position = get_collision_point()
		set_physics_process(false)
		collider.add_child(remote_transform)
		remote_transform.global_transform = global_transform
		remote_transform.global_position = global_position
		remote_transform.remote_path = remote_transform.get_path_to(self)
		remote_transform.tree_exited.connect(cleanup)
		_explode()
			
func cleanup() -> void:
	queue_free()
	print("cleaned")

func _explode() -> void:
	if explosion_scene:
		var explosion := explosion_scene.instantiate()
		var decal := decal_scene.instantiate()
		get_tree().current_scene.add_child(explosion)
		get_tree().current_scene.add_child(decal)
		explosion.global_position = global_position
		decal.global_position = global_position
		explosion.blast()
		queue_free()

decal script itself

extends Node3D

var decals_amount
@export var max_decals_amount: int = 10

func _physics_process(delta: float) -> void:
	decals_amount = get_tree().get_nodes_in_group("Decals").size()
	if decals_amount >= max_decals_amount:
		print("decals removed")
		queue_free()
		

func _on_timer_timeout() -> void:
	queue_free()

Current looks of decals, I noticed some square look on house .

How would I implement the get_collision_normal() ?

Your raycast can get normals.

var collider = get_collider()
if is_colliding():
    var normal := get_collision_normal()

but then it seems like you are using remote transforms to copy this positional data instead of moving your _explode function under this if is_colliding() statement

if is_colliding():
	audio_stream_player_3d.stop()
	var point := get_collision_point()
	var normal := get_collision_normal() # get normal
	var rotate_up := Quaternion(Vector3.UP, normal) # use normal to rotate

	var explosion := explosion_scene.instantiate()
	var decal := decal_scene.instantiate()
	explosion.position = point
	decal.position = point
	decal.quaternion = rotate_up # assign rotation

	get_tree().current_scene.add_child(explosion)
	get_tree().current_scene.add_child(decal)
	explosion.blast()
	queue_free()

using physics process to get_nodes_in_group is pretty bad; and every other decal will be doing this too. Your decals script could track the total decals with a static variable instead. A static variable is shared between every instance of the script, so increasing it on ready, and decrementing on _exit_tree works as cheap way to count every living instance.

static var decals_amount: int = 0
const MAX_DECALS_AMOUNT: int = 10

func _ready() -> void:
	decals_amount += 1
	if decals_amount > MAX_DECALS_AMOUNT:
		var all_decals := get_tree().get_nodes_in_group("Decals")
		# a better solution would find the oldest
		all_decals.pick_random().queue_free()

func _exit_tree() -> void:
	decals_amount -= 1

func _on_timer_timeout() -> void:
	queue_free()
1 Like

There is a decals exanple in the godot demo projects repository.

1 Like

Thank you so much, code is so neat :ok_hand:.

I have tested it and had no idea what this normals was doing, when I tried to print it was in format of Vector3.

to find oldest decal I have added:

		all_decals.pop_at(0).queue_free()

Had no idea I can use static variable in GDScript.

this is something new as well, might be worth to use this trick more often.

Idea came from arrow sticking to moving object, and it becomes independent of parent node via top level.

current look