What's an efficient method for burnable objects that can spread?

Godot Version

4.3

Question

Hi,

I’m fairly new to all this, so there’s a great chance I’m doing things in a roundabout way–feel free to suggest improvements, it just may go over my head for a bit. With that out of the way:

I’m trying to set up a destructible object system where objects and enemies can either break apart (easy enough), freeze solid (still easy enough, in theory), or catch fire and spread fire to other burnable objects within a range. I’ve worked out an error-prone solution using one attachable scene as a burning mechanic, and one base scene as a burnable item. It’s pretty error-prone and I think attaching the new scene is causing weird issues that I’m not grasping yet, but the biggest issue is that it really flogs performance once 5 or so items catch fire–and I’d like to be able to have a patch of grass or a vine catch fire over time.

The other method I’ve been resisting is to have each object/enemy have a seperate burn-state object that would swap in, and trap all the mechanics in there, which would mean every WoodenBox also has a corresponding WoodenBox_burning object, etc. This seems a bit bloated to my mind, but open to it if it’s the best route.

What is the most effecient way to handle destructible/burnable objects, in way that doesn’t smash performance?

Here’s the burning mechanic:

extends Area2D

@onready var burn_check: Timer = $BurnCheck
@onready var burn_check_radius: CollisionShape2D = $BurnCheck_radius
@onready var burnable: Area2D = $"."

var burn_target = null

func _on_burn_check_timeout() -> void:
	burn_check.set_wait_time(randf_range(0.4, 0.8))
	check_burnable()
	burn_check.start
	print("timer restarting")
	#burnable.monitoring = true

func check_burnable():
	print("should be burn checking")
	
	if burn_target != null:
		if burn_target.has_method("take_burn_damage"):
			burn_target.take_burn_damage()
			print("checked and burning")
	elif burn_target == null:
		print("cant burn")
	#burnable.monitoring = false
	
func _on_area_entered(area: Area2D) -> void:
	burn_target = area.get_parent()

And here’s the burnable test object:

extends RigidBody2D

enum States {NOTBURNING, BURNING}

const BURNINGSCENE = preload("res://scenes/burning.tscn")
@onready var burntimer: Timer = $burntimer
@onready var burn_check: Timer = $Burning/BurnCheck2
@onready var burn_check_radius: CollisionShape2D = $Burning/BurnCheck_radius2

# This variable keeps track of the character's current state.
var state: States = States.NOTBURNING
var burn_health = 15

func _process(delta: float) -> void:
	if burn_health <= 0:
		state = States.BURNING
		burntimer.start()
		
	if state == States.BURNING:
		modulate = Color(randf_range(0.1, 0.99), randf_range(0.1, 0.99), randf_range(0.1, 0.99))
		var instancedscene = BURNINGSCENE.instantiate()
		add_child(instancedscene)

func take_burn_damage():
	if state == States.NOTBURNING:
		burn_health -= 8
		print(burn_health)
	else:
		return

func _on_burntimer_timeout() -> void:
	queue_free()

Many thanks for any ideas.

Seems like here you are maybe instantiating many BURNINGSCENE over time, unless you are setting the state back to NOTBURNING after you instantiate the BURNINGSCENE the first time.

Oh, I think you’re right–modulate was firing off every frame and I didn’t connect that the scene would be instantiated every frame as well, which would explain the performance hit. Thanks!

Would still love to work out a better spreadable fire system though, if anyone has ideas or there’s a common method that I haven’t been able to find.

What kind of gameplay elements are involved in the burning?

Performance wise I think the biggest hitch is how the burning objects ignite other objects, how are you doing that right now? I see some variables but not sure if the functionality for the spreading is shown in the scripts pasted.

Sorry for the delay, I only get to do this stuff on the weekend.

To answer your question, I wound up just going the route of instantiate an entirely new burning object in place of the non-burning object:

const BURNINGSCENE = preload("res://scenes/burning_test_object.tscn")
const GRAV = 80

var burn_health = 15

func _process(delta: float) -> void:
	if burn_health <= 0:
		var burning_object = BURNINGSCENE.instantiate()
		get_tree().get_root().add_child(burning_object)
		burning_object.position = position
		queue_free()
		
	apply_central_impulse(Vector2.DOWN * GRAV)

func take_burn_damage():
	burn_health -= 8
	print(burn_health)

And then the burning object:

extends Area2D

@onready var burn_check: Timer = $BurnCheck
@onready var burn_check_radius: CollisionShape2D = $BurnCheck_radius
@onready var burnable: Area2D = $"."



var burn_target = null


func _on_burn_check_timeout() -> void:
	burn_check.set_wait_time(randf_range(0.4, 0.8))
	check_burnable()
	burn_check.start


func check_burnable():
	
	if burn_target != null:
		if burn_target.has_method("take_burn_damage"):
			burn_target.take_burn_damage()
	elif burn_target == null:
		return
	
func _on_area_entered(area: Area2D) -> void:
	burn_target = area.get_parent()

So it checks for overlaps within a circular burn-checking radius every half second. This is working okay, but pretty error prone–odd physics collisions with objects instantiating in while colliding with each other, and often in a line of burnable objects, the spreading fire will skip one or two objects and they’ll eventually catch later, which makes me wonder if multiple overlapping collision checks cause weird issues. If there’s a simple distance check, that may work better, but this is the process I’m most familiar with at the moment.

Any thoughts?
2024-11-27 14-07-04

1 Like

Why do you instantiate a whole new object? Can’t you just swap the visuals instead? This way your physics would be in sync the whole time.

Partly because there was a fair number of behaviors that needed to be added in that I didn’t want triggering in the background like timers and collision masks for flamability, which was causing me problems when I tried to instantiate them in over top of the base object–realizing now that I could probably solve most of that with a state machine. And also because I have the same setup for enemies, where their CharacterBodies while alive and switch to RigidBodies on death so they can drop and roll around. It’s gotten ungainly though, for sure.

Happy to hear ideas for a more efficient method though! I think there’s probably a much better way to set up with either class or composition, but I haven’t read up on it enough yet.