Fish swimming around a planet

Godot Version

latest dev or 4.2

Question

Could anyone help me with a demo project. The challenge is to get a fish to swim (at a single depth) around a small planet (really an Area3D with its own gravity on).

It seems really simple, but I can’t figure it out.

I have a sample project on my Gitlab that you can hack on: Donn Ingle —Dbat / fishy · GitLab

1 Like

Depending on your needs, you may find the topic below helpful:

The solution to that topic is, however, not concerned with the usage of the gravity overrides of Area3D. Instead, it computes the velocity that achieves constant circular motion i.e. gravitational acceleration is not considered. Given that you’re looking to implement a fish “swimming at a single depth”, I reckon the topic will be useful to you. However, in order to modify the “swim-depth” at run-time, the solution presented in the topic must be modified.

Let me know if you got any questions.

I am reading that post of yours. Pretty damn spectacular so far!

Okay, I can change course and go with various static bodies and such, but I still wonder how to do the fish thing with integrate_forces and all the rest of the RigidBody mumbo-jumbo.

I’m not sure what your question is. Could you try and clarify what you are not understanding? “mumbo-jumbo” is rather abstract.

Perhaps it would help if you gave a brief overview of the knowledge you have within this particular topic. That way I can provide an explanation that glosses over the stuff you’re already familiar with.

1 Like

My sample project kind of encapsulates what I have done so far.

I have an Area3D that has gravity going to the centre.
I then have a RigidBody3D (the fish) that starts inside the area.

I am trying to make that fish ‘swim’ ‘forward’ (and keep its belly pointing ‘down’) so that it goes around the entire sphere.

Once I have that I can start to mess with the buoyancy of the fish for the up/down direction.

I am not sure if this is a wise use of the physics in Godot, but it seems like it should be possible to do. I am not familiar with the various methods in the RigidBody3D, hence ‘mumbo jumbo’.

I hope that helps! I am going to hack on the problem today while I can. Will keep an eye on the thread.

This is the kind of thing I am trying. It’s the code for the fish RigidBody3D.

extends RigidBody3D

var fwd := 20 * Vector3.FORWARD

func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:

	# Seems to keep the fish at same depth
	# f = ma
	# Fb is buoyancy force ish
	var Fb := -mass * state.total_gravity

	# Trying to keep the fish somehow belly down...
	# Or at least have 'forward' not be weird
	var local_fwd := fwd * transform.basis
	var y := Fb.normalized()
	var x := -y.cross(local_fwd)
	var z := -local_fwd
	print("old basis:", transform.basis)
	print(" new basis from: x:", x, " y:", y, " z:", z)
	var new_basis := Basis(x, y, z).orthonormalized()

	transform.basis = new_basis
	print(" new basis:", new_basis)

	# calc again cos it's now changed
	local_fwd = fwd * transform.basis

	apply_central_force(Fb + local_fwd)

Here’s the scene (top view):

I am afraid that I am a fish out of water with this one. :disappointed_relieved:

Hmm…

If you insist on using physics for this problem (either because you intend to have the system respond to external forces, or simply because you want to implement a physics-based solution), I’ll try to adhere to that.

Your current approach

From what I can gather, you’re using an Area3D to apply local gravity around a point. In your code, you then seem to be cancelling this gravitational acceleration on your fish via a buoyancy force, Fb.

    # Fb is buoyancy force ish
	var Fb := -mass * state.total_gravity

You didn’t describe what your current code produces in terms of motion. However, I assume that your fish (like in the post I linked previously) moves further and further away from the center of the sphere as time progresses.

With regards to basis manipulation, that looks fine - classic cross product use.

Also, you seem to be handling force-computation just fine. I’m not sure if it’s valid to call apply_central_force directly. I believe the whole point of using integrate_forces is to get access to the state of the body so you can safely manipulate it. In other words, I’m not sure whether the following counts amounts to the same outcome - it might.

    # Your code
    apply_central_force(Fb + local_fwd)
    # Correct usage (?)
    state.apply_central_force(Fb + local_fwd)

take that with a grain of salt

Recommended steps

From your code, it doesn’t look like you considered the solution in the post I linked. The concept that you have to apply is pretty much identical. The only thing that is different (and that is slightly) is the implementation because you’re using a RigidBody3D, not a CharacterBody2D.

Therefore, I would recommend the following steps:

  1. Read through the linked post and note down the concepts or code you don’t fully understand
  2. Look closely at the post’s provided solution and try to determine how you can use it for your problem
  3. If you are still unable to solve the problem at this point, please provide a description (video/gif would be preferred) of what your system is doing and what you’ve tried to fix it.

I’ll try and catch you tomorrow, during the time of your next reply, so we can expedite your issue and find the solution.

I appreciate your help!

I also have wondered about that state param supplied to the func. It looks like it has all the same methods as the RigidBody3D does and I can’t see why, nor what the difference is.

I tried the state.apply_central_force and it does the same thing with and without state.

I must apply myself to your other post and will try again tomorrow.

1 Like

A small delay due to a day without electricity! Yay. Will get back to fishing asap.

1 Like

I have similar project and I currently use said cross math method to change rotators direction ( forward vector ). What I don’t know how process heavy this is or is it a good practise, but seems to work for the small prototype I did.

@dbat No worries. I’ll try to check in daily.

@trier Generally speaking, you should only worry about performance when it becomes a problem. Best practices highly depend on the context so I can’t answer whether your application of cross product(s) is considered best practice. But hey, if it works it works.

Nearest I can tell, that other post has some kind of fixed 2D circle which makes the angle easier to deal with.

In my planet, it’s a 3D sphere and the fish can

  1. swim at various angles, up and down, left and right
  2. the cartesian coordinates are all over the place, so there’s no single nice axis to rotate stuff on.

If I would get the fish to orbit the planet at a fixed depth using apply_central_force that would be a good start.

If code examples help and this may not help unless you can get a VR simulator addon to help if you don’t have an HMD and can grok the files, the XR-tools demos have a planetary level example that keeps the player controller on a spherical world. GitHub - GodotVR/godot-xr-tools: Support scenes for AR and VR in Godot

The problem with calling it orbit, IMO, is that in a true orbital situation, the object is traveling around as fast as it is falling toward, in relation to whatever other movement it’s doing. I think, IMO, you need to simulate buoyancy with intertial drag, a non-gravity body, and adjust the camera and/or player controller’s angle in relation to the sphere’s origin so that the player object’s down vector points at the sphere’s origin/center, keeping a 90-degrees direction the fish is pointing in, staying at a fixed distance based on whatever control adjusts depth (swim bladder simulation). I personally would not rely on physics for this effect, save for maybe dampening of the controller, but would probably move the world around the character and only change distance from the sphere having only collisions for interactions and faking buoyancy with the character body, but that’s probably not what you want.

1 Like

I actually wasn’t considering the fact that you needed your fish to swim at a constant depth in three dimensions. I guess I focused mostly on the illustration from your original post which was in 2D.

With regards to your second point, there’s no need to rotate anything - at least not with regards to the fish’s trajectory. I’m not sure whether this notion is something you’ve concluded yourself or if it’s misinterpreted information from the other post.

Problem formulation

The problem we’re trying to solve is:

Constraining an object and it’s motion to a pre-defined path or shape.

The keyword here is constrain. A constraint is an important concept for modelling and understanding dynamic systems. Constraints provide limits to the dynamic system such that some states are no longer possible.

In this case, we want to implement a constraint that aligns an object with the surface of a sphere. In other words, we want the fish to be neither inside or outside the sphere.

Formulating the constraint

To implement a constraint, we must first describe the information that is available in the system.

Information in this problem

  • Sphere center point (planet center)
  • Sphere radius (target depth)
  • Entity position (fish)

From the previous section we have this constraint description:

A constraint that aligns an object with the surface of a sphere.

This description can be unpacked to produce a more useful description:

A constraint that limits/locks an object’s distance from a point.

Hopefully, it is now more apparent what the solution or approach to the problem is. In any case, we now have multiple ways of viewing the problem.


The position of the object must always be `depth` distance away from our sphere center, and the velocity must only produce such positions.

With this in mind, we can now start building the constraint.

Sidenote: This type of constraint is commonly referred to as a rod or distance constraint. In Blender, the constraint is called Limit Distance.

Building the constraint

As I see it, there a two ways of implementing this constraint: an explicit approach, and an implicit approach.

While explicit methods are straight-forward, implicit methods tend to be more complex and rely heavily on calculus. However, I’m using these terms purely to communicate the nature of the approaches; explicit being after movement has taken place, and implicit being during the movement. I won’t be outlining an actual implicit method and no extensive mathematical foundation will be given.

Explicit approach

image

If the fish is above or below the target depth from the planet center, we modify the position such that the correct depth is achieved.

To do this we simply:

  1. Compute the vector between the fish and the planet center
  2. Constrain the length of that vector
  3. Use the constrained vector to compute the new position for the fish
    pos = planet_center + constrained_vector

Code example

@export var planet_center := Vector3
var depth := 5.0

func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:

    # The new position constrained to the surface of the sphere
    var from_center = tr.origin - planet_center
    state.transform.origin = planet_center + from_center.limit_length(depth)

Depending on the context, we may also need to correct the velocity. However, because you, presumably, are dealing with a user-controlled object, this is not needed as the velocity is always derived from user input and the planet’s surface normal.

Implicit approach

image

For this approach, we want to modify the velocity of the fish such that it complies with our constraint definition. The position-change, as a result of the fish’s velocity, must remain on the sphere’s surface.

The following steps can be used to modify the velocity to guarantee the previous statement:

  1. Compute the next position of the fish, new_pos, and limit this point’s distance from your sphere center. We call this new point new_pos_clamped.
  2. Compute the vector between new_pos_clamped and your fish’s position. This will be our new velocity which complies with our constraint.

Code example

@export var planet_center := Vector3
var depth := 5.0

func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:

    # This is simply a utility variable to make the code more readable
    var tr = state.transform
    # The delta is not provided so we must get it from the node
    var delta = get_physics_process_delta_time()

    # The position of the fish in the next frame
    var new_pos = tr.origin + state.linear_velocity * delta

    # The new position constrained to the surface of the sphere
    var from_center = new_pos - planet_center
    var new_pos_clamped = planet_center + from_center.limit_length(depth)

    # Assign the constrained velocity
    # We divide by delta to achieve the correct unit: m/s
    var constrained_velocity = (new_pos_clamped - tr.origin) / delta
    state.linear_velocity = constrained_velocity

Do note that this example code does not attempt to approximate the correct velocity. If you wish to do this, the new velocity should be the aggregate of several velocity vectors computed in substep-like algorithm.

Final notes

The approaches outlined in the previous sections are not meant to be used in a generic physics context; the constraints described are specialized to achieve the spherical motion that you requested. I wanted to provide you with a more generic constraint implementation but, admittedly, I haven’t spent enough time with physics constraints to do that.

Lastly, I would like to add that this constraint-based approach is just one way of achieving your goal. There are other ways of making your fish orbit around an object.


Hopefully this is of use to you.
1 Like

@Sweatix, you are a great poster and teacher. Thank you for the effort you have put into this.

I did not know about explicit and implicit. I would have to classify myself in the explicit camp, as you said. Implicit is all about math and boy do I wish I had a head for that stuff!

I like the term constraint and will adopt that. Thank you for the clear code examples and diagrams. I also did not know about limit_length!

From my mucking about in the last week, I have learned that the point (the position) of a body within an Area3D gravity is moved but the basis remains what it started as—hence there is no rotation of the body as it moves. That means there’s a static forward direction for each fish and when I apply_impulse they quickly swim out of the gravity well.

I tried to imitate a physicsy ‘nose-down’ effect so that forward would continue to point along the sphere, but my math is not good enough and the apply_torque methods defeated me.

In the end I found some code on Kids Can Code and I’ll reproduce it below.
(CharacterBody3D: Align with Surface :: Godot 4 Recipes)

extends RigidBody3D

## This code goes on my fish scene, which is a RigidBody3D
## The body has Angular X, Y and Z locked in the inspector!

@export var air_bladder_vol : float = 0.01
@export var water_density : float = 1024
@export var band := Vector2(11,15)
@export var noise : NoiseTexture2D
@export var speed : float = 0

var fish_volume : float
var data:PackedByteArray

func _ready() -> void:
	# Make the fish as dense as water + a little bit, so it sinks naturally.
	var fish_density = water_density + (water_density/5.0)
	# Then find the volume via that.
	fish_volume = mass/fish_density
	if noise:
		await noise.changed
		var i : Image = noise.get_image()
		data = i.get_data()

	# Start fish off going forwards. I guess.
	var global_fwd : Vector3 = -global_basis.z
	apply_impulse(speed * global_fwd)


var i:int = 0
## Right now this uses a 1d noise texture to get some
## numbers varying from -n.0 to +n.0 (whatever n is)
## to give the fish some visual up and down floatiness
func get_bouyancy(state) -> Vector3:
	var n : float = 0
	if noise:
		n = data.decode_s8(i) # yay has negative numbers!
		i = wrapi(i + 1, 0, data.size())
		n = remap(n, 0, 255, 0, air_bladder_vol)
		#print(n)

	var fish_total_volume:float = fish_volume + n
	var water_vol_displaced :float = fish_total_volume # archimedes
	var water_weight :float = water_density * water_vol_displaced
	var Fb = -1 * water_weight * state.total_gravity #.normalized()
	return Fb


## For now space will boost the fish forwards
func _input(event: InputEvent) -> void:
	if event is InputEventKey:
		if event.keycode == KEY_SPACE:
			# This is the magic 'go forward' formula:
			var global_fwd : Vector3 = -global_basis.z
			var boost = speed * global_fwd
			print("BOOST:", boost)
			apply_impulse(boost)


func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
	# Fb is buoyancy force
	var Fb := get_bouyancy(state)

	state.apply_central_force(Fb)

	#NEARLY WORKS - but flickers a lot at first
	#In this case we're finding the rotation from the current "up" vector (the Y basis vector) to the surface normal which we want to be the new "up" vector.
	#var b_rotation := Quaternion(transform.basis.y, Fb)
	#transform.basis = Basis(b_rotation * basis.get_rotation_quaternion())

	# Totally works - Thanks to https://kidscancode.org/godot_recipes/4.x/3d/3d_align_surface/index.html
	# I'm guessing _integrate_forces gives one a small peek into the
	# FUTURE of the body. The Fb I calculated here (and applied) is
	# the force that will be applied *after* this func is done.
	# Hence global_transform is NOW and Fb is NEXT
	var xform = align_with_y(global_transform, Fb) # so Fb is a future vector
	global_transform = global_transform.interpolate_with(xform, 12 * state.step)


func align_with_y(xform, new_y):
	xform.basis.y = new_y
	xform.basis.x = -xform.basis.z.cross(new_y)
	xform.basis = xform.basis.orthonormalized()
	return xform

Still not sure why state is passed-in to _integrate_forces exactly.

That’s the general state as of now. Any crits welcome!

Thanks for the kind words.

However, from your reply it doesn’t look like you really care about using constraints. You seem more occupied with trying to create a realistic buoyancy simulation than finding a solution to your initial problem: making a fish swim around a planet.

If you can’t stay focused on the task at hand and answer our questions, you’re wasting the time of myself and others who attempt to help you.

As for why _integrate_forces() has state as a parameter, you can read more about it on Godot Docs.

What is confusing is that RigidBody3D has most of the same methods, so why do we need the state param? It leaves it vague as to whether one does:
state.apply_x or just apply_x (directly on the body).

You’re actually just trolling at this point. Good luck solving your problem - I’m not going to spend my time helping someone who can’t keep their eye on the ball.

I am sorry you have judged me so harshly. Go well.

I took a look at your example project, you seem pretty close to what you are after but the fish tend to bottom out and fly far away. I think tightening up the outer water area might help, but I think the setup is a bit needlessly complicated. I need to ask, which is more important to you for this idea: Accurate to real-life simulations with as much complex physics as possible, tossing KISS out the window and using all system resources to simply making fish float? OR something more KISS friendly that just make sure the fish (game objects for simplicity sake) stay within a sphere of influence and move around that sphere? Keep It Simple, Stupid.

Not directed at you but KISS is one of the foundational things. Also, it may help to understand design documents and how to simplify that concept in to a miniature form you can use to describe what it is you want to do to some one else. Try to complete the basic idea of just the game object. Do the fish eat things? Do they avoid other fish? What is the easiest way to do that. Only focus on getting the basic functionality understood by you and who is helping you or working with you. Don’t worry if people get frustrated, but take it as a sign you were missing something. Maybe they got the impression that you didn’t take time to go over everything they worked hard to give you, which was probably needlessly complicated in and of itself also, but no sweat.

1 Like