Mobile game - how do you make this physic in Godot?

Godot Version

4.7.2

Question

Currently I’m looking into games which I consider as good time killer, and something about this mechanic in game make me wonder:->

How did they achieved this effect?

I mean specifically this objects how it’s rolling around, are stacked on top of each other.

I mean hopefully it’s not stacked one by one, but how could I achieve nice stack of fruit, and then have this rolling mechanic?

Here is short play from mobile device, it’s one of many games for Nectar points.

You would place the objects, either by hand or with a custom tool script. They would likely be RigidBody3Ds with “Sleeping” enabled on start.

So for hole itself, is it cutting out floor or its smart overlap, maybe decal?

Probably not cutting a hole in the floor, I could see a World Boundry being used for the floor, let’s say on layer 1, then the hole can use an Area3D to disable any overlapping RigidBody’s mask 1. To catch objects on the sides an extra ring-shaped collision(s) can be added on layer 2.

As I see it this would make the objects fall regardless of their size. What about objects that get stuck because they don’t fit the whole yet? One would need to wiggle them out of the hole (using some sort of realistic collision) in order to free the hole up again.

I think in addition to the fact that the floor lets stuff fall through a gap one would need an actual ring-shaped physical body which synchronously to the floor gap grows in size and can push things around a bit.

As for the visuals, one can create a shader material for the ground which gives the illusion of a whole at the right spot through transparency and moves synchronously with the ring-shaped physics object through shader parameters.

I tried to build it, but failed.

Looks like as fairly simple thing, but even collision shapes for RigidBody3D is a bit tricky.

I found this guide

Interesting video.

I heard it was better to avoid CSG during production builds. Not sure about that tho.

Also, if using the CSG in the first place, why not just subtracting a CSG cylinder from a CSG surface? Instead he goes through this lengthy setup with stencil mask settings. Interesting, but I don’t understand why.

Until minute 4:30 it is about the visuals, only after it starts go be about collision – the important part for the right game feel. The trick that he mentions is kind of what we described earlier.

I only don’t get what is cyan doughnut was for. I have the feeling the entire setup should work without it, but I might have misunderstood the purpose.

not sure as well.

Very similar approach with Area3D, but overall I think the idea is make it bit less jittery the objects on contact with hole, also object which doesn’t fall still affects other objects.

The original studio is “MoonActive” - I believe they used Unity in this case, but principles should be same in Godot.

I see some people made it before - GitHub - mbMayer/Godot-Hole.io: Template for Hole.io style games based on CSG nodes · GitHub

As for the visuals of the hole I would probably make the hole thing a shader like so:

shader_type spatial;

uniform vec3 hole_world_position;
uniform float hole_radius : hint_range(0.0, 5.0) = 1.0;
uniform vec4 albedo_color : source_color = vec4(1.0);

varying vec3 world_position;

void vertex() {
    world_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
}

void fragment() {
    float distance_to_hole = distance(world_position, hole_world_position);
    if (distance_to_hole < hole_radius) {
        discard;
    }
    ALBEDO = albedo_color.rgb;
}

Then update the shader data like so:

extends StaticBody3D
class_name MyGround

@onready var ground_mesh: MeshInstance3D = %ground_mesh
@export var target: Node3D

func _physics_process(_delta: float) -> void:
	var shader_material: ShaderMaterial = ground_mesh.material_override as ShaderMaterial
	if shader_material and target:
		shader_material.set_shader_parameter('hole_world_position', target.global_position)

I was trying to get a minimal setup working for hole mechanics myself: godot_the-hole on my Github.

I think I implemented it with much less, at least there is no non straight-forward setup. So far I think everything is quite simple.

The one exception is the hole scene itself – that is what makes you feel the physics when dropping stuff inside (these shapes are on layer 2, which is always on).

This can probably be done nicer and cleaner. For example I didn’t need a cyan doughnut. No CSG meshes.

These two signals (shown below) toggle if a fruit can fall into the hole or not; and if a fruit moves the right way it stays on top of the ground, while fruits always look for collisions on layer 2.

func _on_area_3d_body_entered(body: Node3D) -> void:
	if body is MyFruit:
		print('hi fruit')
		body.sleeping = false
		body.set_meta('over_hole', true)
		body.set_collision_mask_value(1, false)

func _on_area_3d_body_exited(body: Node3D) -> void:
	if body is MyFruit:
		print('bye fruit')
		body.set_collision_mask_value(1, true)
		body.set_meta('over_hole', false)

Time to try some fruit ?

Beautiful fruits and a nice resource too!

I have tried your project , but it’s failing to keep stacked the fruit.

That is because I didn’t focus on the fruit shapes yet, their colliders are just ball shapes and therefore hard to stack. But that shouldn’t be hard to add in.

My main focus was about to get the pushy hole mechanics going. Next I would probably make sure that the colliders are a better approximation to a circular shape than what I did before.

First would make sure here that the approximation of the circle is smooth enough to not cause any game feel disturbances. Second I would make the hole grow in size depending on some parameter.

The rigidbody not like much trimesh or convex.

But I see option freeze as well.

update current look

I have played with shapes a bit, and collision layers.

With basic collision shape like Capsule it reacts much better, ( trimesh completely was ignoring shapes and pears falls into other pears)

Also instead can sleep, I use freeze and unfreeze to make it more logical.

func _on_area_3d_body_entered(body: Node3D) -> void:
	if body is MyFruit:
		print('hi fruit')
		body.freeze = false
		body.set_meta('over_hole', true)
		body.set_collision_mask_value(1, false)

Unfrozen Donut, is fun but frozen make it more like in original example.

Looking pretty good!

I’m looking on how “hole” and “ground” making size of visible hole.

What would be a best approach to scale it along with donut?

I made fork with changes I added - GitHub - artemisiagodot-beep/godot_the-hole: Implementing a hole in Godot. · GitHub

Got maybe a better solution

using stencil and trimesh for staticbody3d

Your video looks already much smoother regarding the whole hole game feel – except maybe that the fruits still stack on a collider outside the visible mesh, but that is trivial to fix.

Here is some code which arranges the collision shapes (called pillars) in a ring shape and adjusts to the size of the hole. Two things about it tho: I don’t know if it is a good idea to have so many overlapping collision shapes – probably not; and somehow the algorithm should also account for how many shapes are actually needed approximate the ring shape for bigger sizes. But I think both issues are probably best fixed together. So for now that’s a quick and dirty implementation.

Also the initial hole state should be updated in the ground shader and there is some bug with waking up the fruits from the sleep state. Sometimes they just don’t wake up, I didn’t investigate, but my first guess would be the hole’s top collider is too low on the y-axis.

Below the code (and update on my Github). Maybe that helps.

func _on_area_3d_2_body_exited(body: Node3D) -> void:
	#body.owner.queue_free() # TODO: remove the fruits.
	_grow_hole()

func _ready() -> void:
	_grow_hole()
	
func _grow_hole() -> void:
	print('+1UP')
	var shape: CylinderShape3D = ring.shape
	shape.radius += GROWTH_INCREMENT
	var cylinder_mesh: CylinderMesh = mesh as CylinderMesh
	cylinder_mesh.top_radius = shape.radius
	cylinder_mesh.bottom_radius = shape.radius
	grows_in_radius.emit(shape.radius)
	_update_pilars()

func _update_pilars() -> void:
	var cylinder: CylinderShape3D = ring.shape
	create_collision_ring(static_body, cylinder, 30)

func create_collision_ring(
	parent: Node3D,
	cylinder_shape: CylinderShape3D,
	shape_count: int,
) -> void:
	for child: Node in parent.get_children():
		child.queue_free()
	var pilar_depth: float = 6.0
	var pilar_dimension: float = 0.5
	var radius: float = cylinder_shape.radius
	for i in range(shape_count):
		var angle: float = TAU * float(i) / float(shape_count)
		var collision: CollisionShape3D = CollisionShape3D.new()
		var shape: BoxShape3D = BoxShape3D.new()
		shape.size = Vector3(pilar_dimension, pilar_depth, pilar_dimension)
		collision.shape = shape
		var pos_x: float = cos(angle) * radius
		var pos_z: float = sin(angle) * radius
		collision.position = Vector3(pos_x, -pilar_depth/2.0, pos_z)
		collision.rotation.y = PI - angle
		parent.add_child(collision)