Some spawned objects freeze with very high velocity, while others behave normally

Godot Version 4.6

v4.6.dev1.mono.official [8d8041bd4]

Question

I have a setup, where jumping on objects calls a behavior_drop_loot component, that in turn spawns the drops, with an initial velocity to have a effect of items exploding out of the parent. But a lot of times it doesn’t work, and the spawned objects are static on screen.

When inspecting these bugged objects in the runtime inspector, none of them are frozen or sleeping, and they do have velocity, sometimes very high (like 4k px/s), which wouldn’t be possible valuewise given how I set this initial velocity.

I spent the last 5 hours slamming my head against Gemini, Claude and ChatGPT, without avail.

I use resources to initialize generic Object Container that hold platforms, items and all other objects.

The container has an area2d used to trigger behaviours. Before I had it so that the objectdata resource for a big rock would contain the logic for how it’d be destroyed and spawns stones, but I wanted to refactor this and switch to a composition based system, where I now have behaviour scripts, that get loaded and attached to dynamically created nodes in the container tree at runtime.

I use an ObjectSpawner for spawning containers, which then calls the init_object() function on the container, which also initializes the components from the behaviour scripts I assigned.

The container then simply loops though the components and executes their on_collision_behavior().

Sounds straight forward enough, but for some reason it broke my system that was working before I put the logic into components

The fact that the velocity values are so crazy while the objects aren’t moving at all is a mystery to me. I also don’t have any process functions that would hard-override the position… no custom integrator, or anything like that.

Here’s the function in the behaviour component that’s called by the container of a rock to spawn stones:

func on_collision_behavior(body: Node2D) -> void:
	if not body.is_in_group("Player"):
		return	
	var inventory:InventoryComponent = body.find_child("InventoryComponent",true)
	
	var failed := false
	for tool in required_tools:
		if not inventory.has_item_type(tool):
			failed = true
	if failed:
		print("Missing tools")
		pass #cancel the function, nothing happens?
	else:
		call_deferred("deferred_spawn")

		
		#implement calls to ObjectSpawner, in order to spawn small rocks when you break a big one

func deferred_spawn() -> void:
		var resource_node_state = parent_container.state as ObjectStateResourceNode
		
		resource_node_state.hits += 1
		
		if resource_node_state.hits >= required_hits:
			

			var count = randi_range(child_spawn_range.x,child_spawn_range.y)
			var rng = RandomNumberGenerator.new()
			for i in range(count):
		
				
				var rnd_x = rng.randf_range(-1.0,1.0)
				var rnd_y = rng.randf_range(0.5,1.0)
				var rnd_vec := Vector2(rnd_x,rnd_y).normalized()*velocity_multiplier
				"""ToDo: Right now just a random pick is done. Should also be handled via spawntable eventually"""
				var newcont = ObjectSpawner.spawn_world(parent_container.get_parent(),child_spawns.pick_random(),1,parent_container.to_global(parent_container.position),Vector2(1,0))
				await get_tree().physics_frame
			parent_container.destroy()

And here’s the spawn_world function of the ObjectSpawner

static func spawn_world(parent:Node, object_data:ObjectData, count:int, position:Vector2 = Vector2(0,0), velocity:Vector2 = Vector2(0,0), container:PackedScene = container_scene ) -> ObjectContainer:
	
	var new_state_inst = create_new_object_state(object_data,count,container)
	
	#retrieve objectData from state
	#var obj_data = new_state_inst.object_data
	
	#if parent is another container
	if parent.get_script().get_global_name() == "ObjectContainer":
		#jetzt die data und checken ob es eine platform ist
		var parent_platform_size
		var parent_cast = parent as ObjectContainer
		var parent_obj_data = parent_cast.data
		var parent_obj_data_cast = parent_obj_data as ObjectDataPlatforms
		print(parent_obj_data_cast.platform_size)

		if parent_obj_data_cast: #if it's really a platform
			parent_platform_size = parent_obj_data_cast.platform_size # store its size
			if parent_platform_size == ObjectDataPlatforms.PlatformSize.SMALL:
				#now cast the object_data argument to a ResourceNode, as this class has the can_spawn_on_small var
				var obj_data_cast = object_data as ObjectDataResourceNodes
				if obj_data_cast:
					if obj_data_cast.spawn_on_small == false:
						#don't spawn!
						return null
				
		
	
	
	
	
	var cont:ObjectContainer = new_state_inst.container_scene.instantiate()

	# if a velocity is provided, unfreeze the container
	if velocity:
		cont.freeze = false
		cont.sleeping = false
		#cont.apply_central_impulse(velocity * cont.mass)
		
	else:
		cont.freeze = true
		
	if parent is Node2D:
		cont.position = parent.to_local(position)
	else:

		cont.position = position

	
	parent.add_child(cont)
	cont.init_object(new_state_inst) 
	
	#initializes the container using the state (and its ref to ObjectData)
	#set position
	
	

	#as spawn_world creates a real object in the world via the ObjectContainer, it's state needs to be "realized", i.E. the current 
 	#container is stored in the state... right now it just can be an ObjectContainer, which is just the realworld representation,
	#but maybe it could also be inventory?
	new_state_inst.realize_instance(position,cont) #sets position as it was spawned and the container it was spawned "in"
	#this way, the container can be deleted along with the state instance if an object is removed from the world
	
	cont.state = new_state_inst # attach unique state copy to the container

	return cont

Here’s the ObjectContainer:

extends RigidBody2D
class_name ObjectContainer
@onready var sprite_2d: Sprite2D = $Sprite2D
@onready var collision_shape_container: CollisionShape2D = $CollisionShape2D
@onready var collision_shape_area2d: CollisionShape2D = $Area2D/CollisionShape2D_Area
@onready var area_2d: Area2D = $Area2D
@onready var light_occluder = $LightOccluder2D

@export var data: ObjectData
#@export var pickup_enabled:bool = false
##should get initialized by the spawner. The spawner spawns the Container, and attaches the unique state to it, to be fetched
##later when the object is picked up
@export var state: ObjectState 
##If true, an item will get attracted by the player after the follow_player_delay (const)
@export var attracted_by_player:bool = false

@onready var player:CharacterBody2D = get_tree().get_first_node_in_group("Player")

var behaviour_components:Array[Node]


#init an object in the world, needs the particular state
func init_object(state:ObjectState):
	var platform_data = data as ObjectDataPlatforms
	if platform_data:
		light_occluder.occluder.polygon = platform_data.light_occluder
		

	freeze_mode =RigidBody2D.FREEZE_MODE_KINEMATIC #platforms, items etc. should always be animatable
	data = state.object_data
	assert(data!=null, String(self.name)+" init object failed as ObjectData = null")
	
	sprite_2d.texture = data.texture
	sprite_2d.scale *= data.scale
	collision_shape_container.scale *= data.scale
	collision_shape_area2d.scale *= data.scale
	
	
	#Collision Configuration
	self.collision_layer = 0
	self.set_collision_layer_value( data.col_layer,true) #sets the container to the respective collision layer set in the ObjectData
	area_2d.collision_layer = 0
	area_2d.set_collision_layer_value(data.col_layer,true)#same for the area2d
	
	#Physics Collision Setup
	#setup shape
	collision_shape_container.shape = data.collision_shape
	collision_shape_container.position += data.collision_offset
	#setup masks and layers
	if not data.col_switch:
		collision_shape_container.disabled = true
	
	else: #if collision is activated
		if not data.col_player:
			#var player = get_tree().get_first_node_in_group("Player")
			self.add_collision_exception_with(player)
	
	#Area2D collision setup
	collision_shape_area2d.shape = data.collision_shape
	collision_shape_area2d.position = data.collision_offset
	
	self.data = data
	self.state = state
	
	#var pickup_data = data as ObjectDataPickups
	#if pickup_data:
		#await get_tree().create_timer(pickup_data.pickup_delay).timeout
		#pickup_enabled = true
	
	##Setting up the Behaviour Manager	
	var behaviour_manager := self.get_node("BehaviourComponentManager") as BehaviourComponentManager
	if behaviour_manager:
		behaviour_components = behaviour_manager.init_behaviours(data.behaviour_scripts,state.dynamic_behaviours) 
func destroy() -> void:
	self.queue_free()



func _process(delta: float) -> void:
	##ToDo: This stuff should be outsourced to a component as well.
	#var pickup_data = data as ObjectDataPickups
	#if pickup_data:
		#if attracted_by_player:
			#if _following:
				#pickup_enabled = true #override the timer state
				#var vector:Vector2 = player.global_position - self.global_position
				#self.linear_velocity = vector.normalized()*follow_player_speed
				#var thresh:float = 50
				#if vector.length() < thresh:
					#data.on_collision_behavior(player,self)
					#print("express pickup")
		
	pass

func _on_area_2d_body_entered(body: Node2D) -> void:
	for component in behaviour_components:
		component.on_collision_behavior(body)
		

Please help :frowning:

Frankly, I can’t expect anyone to have an answer to my issue. I restored submit and am halfway through rebuilding the component system. Painful, but faster and less nervewrecking.

If you have a suggestion for what that could’ve been regardless of that, I’d still be happy to hear it.