Simulating particle collisions and different particle types

Godot Version

4.4.1

Question

I’m new to Godot. I’ve been struggling to do this for over 3 hours and I’m tired of guessing. Please could someone help me?

All I want is for particles to interact based on different particle types. For example: protons, neutrons, electrons.

Protons are red. Neutrons are green. Electrons are blue and smaller than the others.
It doesn’t really matter that they’re subatomic particles, they just need to have different behaviours.
The particles collide or exert forces on each other from a distance.
They’re all spheres. The world script should instantiate the particles at random positions and velocities. The world scene has a bounding sphere to respawn the particles so they don’t drift away forever.

Meshes, collision shapes, signals, I’m so confused. I tried setting up a signal from the particle scene’s Area3D to the world scene’s bounding sphere Area3D but the other doesn’t show up. At this point I just need someone to explain how they would do it, because I’ve gotten nowhere.

Thanks

Create a scene for each type, Proton.tscn, Neutron.tscn, Electron.tscn; Seems like they could use the same script, up to you if you want to try sharing the script between scenes.

I would recommend using RigidBody3D as the root of each type, give them a CollisionShape3D and a CSGSphere for visuals. RigidyBody3D has a useful body_entered signal for collisions with eachother as long as contact monitoring is enabled and reports set greater than 0. When spawned in the main scene you set the position before adding the RigidBody as a child.

There isn’t a “Bounding Sphere” collision shape type, you may have to code distance checks to keep the objects from straying too far.


Sounds like you are pretty new, check out this tutorial, it covers a lot of node types including Areas and RigidBodies.

Thank you very much.

Experience so far
I completed the tutorials for first 2D and 3D games a few weeks ago. I also tried reading much of the documentation but many of the pages were short and didn’t help much so I stopped reading them. Many of the tutorials on YouTube are for outdated versions and made years ago. I tried some demos in the asset library and was able to get 3D camera movement and mouse control working in another project.
Despite this much exposure to Godot I still feel like a novice in the deep end.

Project setup
I considered using RigidBody3D for particles because of the built-in physics behaviours, but in my other attempts to use RigidBody3D I always struggled to set its properties. RigidBody3D would get stuck or not do something simple such as changing its position or scale.
From what I read online in various places, RigidBody3D needs forces applied to it instead of directly setting its properties, because the collision system handles those. So I’m reluctant to use it.
I also tried CharacterBody3D but it doesn’t have the physics behaviour of a RigidBody3D, so both node types have disadvantages, which is why I chose Area3D.

World setup
For a bounding sphere in the world scene I used an Area3D parenting a CollisionShape3D.

world.tscn:

Node3D ('world') (script: world.gd):
	Area3D ('boundingSphere'):
		CollisionShape3D ('collider')
	DirectionalLight3D
	WorldEnvironment
	Camera3D

world.gd:

extends Node3D

const proton = preload("res://proton.tscn")

func _ready() -> void:
	for i in range(50):
		var newProton = proton.instantiate()
		var minVel = -0.01
		var maxVel = 0.01
		var velocity:Vector3
		velocity = Vector3(randf_range(minVel, maxVel), randf_range(minVel, maxVel), randf_range(minVel, maxVel))
		newProton.initialize(velocity)
		newProton.scale = Vector3(0.5, 0.5, 0.5)
		add_child(newProton)

func _process(delta: float) -> void:	
	pass

func _on_bounding_sphere_area_exited(area: Area3D) -> void:
	if area.is_in_group("particleCore"):
		area.get_parent().velocity *= -1

This bounces the protons back as intended but seems inefficient because every process loop needs to check which group the Area3D is in.
What is a more efficient way to simulate a bounding sphere? I guess I could somehow store the IDs of all particles and loop through that array to check if their positions are in the bounding sphere. Such a loop could be more efficient because the program knows everything in that list is a particle, as opposed to checking all Area3Ds intersecting the bounding sphere and checking if those Area3Ds are in the particle group.
I just realised in theory I could optimize collision layers so the only Area3Ds which do collide with the bounding sphere are particles.

Particle setup
For the particles so far I have a Proton scene and a particle script.

proton.tscn:

Node3D ('proton') (script: particle.gd):
	Area3D ('forceArea') (group: protonForceArea):
		CollisionShape3D ('forceCollider')
	
	Area3D ('core') (group: particleCore):
		CollisionShape3D ('coreCollider')
		MeshInstance3D ('mesh')

particle.gd:

extends Node3D

var velocity: Vector3 = Vector3(0, 0, 0)

func _ready() -> void:
	var minVel = -0.01
	var maxVel = 0.01
	velocity = Vector3(randf_range(minVel, maxVel), randf_range(minVel, maxVel), randf_range(minVel, maxVel))
	pass # Replace with function body.

func initialize(newVelocity: Vector3):
	velocity = newVelocity
	pass

func _process(delta: float) -> void:
	position += velocity
	pass

The core is the proton’s ‘solid’ surface and can be used to prevent other particles occupying its exclusive position in space. I guess I could replace the core with a CSGSphere.

Force area setup?
The force area is the radius in which the proton’s force is applied to anything inside the force area’s Area3D.
I’m not sure if this is the best node setup for a particle applying a force over an area.
Ideally the force would be applied to all other particles wherever they are, even if they’re not in a force area. Like how gravity is applied to all objects regardless of distance, with closer objects experiencing stronger force.
I realised there is no signal for something simply being inside the force area; the signals are for entering or exiting the area. So I can’t do something like

if otherParticle is inside this particle's force area
then calculate force

But I want distance to nearby particles to factor into the force strength; closer particles experience stronger force.

I guess I will need to somehow loop through an array of all particles to apply such forces. How would I do that? I think such code would need to be in the world’s script instead of in the particle’s script.

Thank you again. I recall deleting this topic yesterday because I gave up and didn’t want to waste anyone’s time, but later I tried again and this morning noticed your response.

This is a reasonable implementation, checking groups isn’t very taxing; but you could set the collision layer of the particle core to something else and have the bounding sphere only mask that “particle core” layer.

This would be less effecient because it doesn’t use Godot’s internal physics systems, GDScript is always slower than internal C++

Yeah! That’s the stuff!


CSGSphere would only be for visuals, it shouldn’t be used for moving collision. Having a MeshInstance3D is perfect.

Make sure to multiply velocity by delta, this is frame-dependant movement a faster computer will run the game too fast with this code.


You could use your groups again, I suspect this will be a very slow operation though. Area3D does have it’s own “Gravity Space Override” which you could set to combine, along with other settings like gravity_point.

Here’s how you can go through every node in a group, again large operations like this are ill-advised on process/every frame.

# particle.gd

func _process(delta: float) -> void:
	var cores: Array = get_tree().get_nodes_in_group("paticleCore")
	for core: Node3D in cores:
		# do not apply gravity from self to self
		if core != self:
			var difference: Vector3 = self.global_position - core.global_position
			var distance: float = difference.length()

			# change to some formula including distance fall-off
			core.velocity += difference

Hello! Thank you again for your help.
I updated the particle script to multiply velocity by delta.
The particle code you shared has given me a foundation for particle interactions.

Bounding Sphere Area3D versus Manual Code
I was stuck deciding which method to use for keeping particles in the test area.
If I use an Area3D and its area_exited signal then the program will be more efficient. But if i happen to change the spawn radius during my experimentation, some particles could spawn outside the Area3D so they will never be affected by area_exited because they were never inside the Area3D in the beginning.

If I don’t use an Area3D and instead use custom code in the world script’s process function then the program will be able to keep all particles inside the test area regardless of where the particles spawned, but the program will be less efficient because it will check the distances between particle and world origin every process/frame.

I chose the second option for its accuracy.

Inconsistency between collision calculations and rendered result
I have also been troubleshooting a weird problem with calculating the distance between particles, accounting for their radii.

The formula for calculating the distance between 2 spheres a and b is
var distance: Vector3 = (a.position - b.position).length()

But this is the distance between their origins, not the distance between their outer surfaces.
A sphere’s radius is the distance between its origin and its outer surface. Subtracting the radii of both spheres from the distance between their origins will give the true distance:
distance = (a.position - b.position).length() - (a.radius + b.radius)

Here’s the collision code for the particle script:

func _process(delta: float) -> void:
	var protons:Array = get_tree().get_nodes_in_group("proton")
	for proton:Area3D in protons:
		if proton != self:
			var diff = self.global_position - proton.global_position
			#child 0 is CollisionShape3D
			var protonRadius = self.get_child(0).shape.radius
			var distance = diff.length() - protonRadius * 2
			
			if distance <= 0:
				proton.velocity *= 0
				self.velocity *= 0
	
	position += delta * velocity

To test this code I spawned 2 particles, one directly above the other, and made them move toward each other until the distance between them is zero. When the distance is zero they stop moving. It worked. I tested up to 200 particles and they all stop moving as soon as their surfaces touch.

I wanted to try fitting more particles in the same volume. That would be crowded. So I reduced their scale:

func addProton(pos: Vector3, vel: Vector3):
	var newProton = protonScene.instantiate()
	newProton.velocity = vel
	newProton.position = pos
	newProton.scale = Vector3(1.0, 1.0, 1.0) * 0.5
	add_child(newProton)

But the collision code stopped being precise. Particles stopped moving long before their surfaces touched or long after and their meshes overlapped.

I tried changing protonRadius to the CollisionShape3D’s radius or the MeshInstance3D’s radius, or multiplying either of those by the particle’s root Node3D’s scale. But that didn’t fix the problem.

I checked the scales of the particle scene’s CollisionShape3D and MeshInstance3D and the scene’s root Node3D. Their scales are all set to (1, 1, 1), so they can’t be interfering with the collision code.

Is there a more reliable way to calculate the distance between 2 particles which accounts for their scale and mesh?

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.