Motivation

I recently started development on a solo project which is a cooking game where you and your friends are making burgers on a food truck. A big inspiration was the chaotic nature of Overcooked, but I wanted to make it even more chaotic. To achieve that I wanted to implement a physical interaction system like in Amnesia. This system would then be used to drive the truck for example and interact with machinery. Only after researching it did I found out that there were no tutorials available on this. So this blog will somewhat serve as a guide on what to use to achieve said result and also my journey to discovering how.
What do I mean by a physical interaction system?
The closest example I can provide is the interaction system that is in all of the FrictionalGames’s games. Most popular being Amnesia. The player can interact with levers and wheels as they would in real life, by pulling or dragging them.

For example in their game SOMA, when the player interacts with something like a door/handle the camera stays still and the mouse motions are used to drive the forces on the interaction object. I avoided this method as I thought it would be too easy.
What I got from their demonstration video (FrictionalGames, 2010) and further research into their blogs (Let’s Not Forget About Physics – Frictional Games, 2010) and source code (FrictionalGames, 2020) is that they had a difficulty implementing this mechanic. They used PID Controllers and joints to achieve their final goal (Amnesia Source Code Summary – ChatGPT, 2023). I will try to use similar techniques and see how it goes. It is good to remember that they have been developing this system ever since their first game Penumbra. When Amnesia: The Dark Descent was released they had been working on it for 5 years (FrictionalGames, 2010). Since then it’s been another 10 years! Let’s see if I am able to achieve a similar mechanic in 5 weeks.
The Godot Engine
My game engine of choice is Godot. This comes after all of the drama with Unity’s new pricing policy after which some Unity developers switched over to Godot, me included. Besides, Godot has been on the down low for most of its time since releasing in 2014. Most notable games are Cruelty Squad and Brotato.


I have used it before, but never anything serious. Just some prototyping here and there. By choosing this engine I hope to expand my programming knowledge and to have a better understand of the engine itself as I hope to use it for my future solo-projects.
Beginning
I started out by building a foundation that I could continue on. After finding a suitable tutorial (Abra, 2022) for my needs I was able to carry objects around like in the video.
This grabbing method wasn’t what I was aiming for though. I want a system where the player can pick up the object from whatever point and then that point becomes the anchor point of the object. It will rotate/sway around that point until let go.

After some research I came to the conclusion that joints provided by Godot’s physics engine were the best solution for my problem. They provide a lot of customisability right out of the box, which would save me time. Amnesia developers also used joints for their system although their grabbing is way simpler.
PID Controller
While dissecting the code of Amnesia: The Dark Descent, I learned of them utilising something called “PID controllers” for their grabbing system (Amnesia Source Code Summary – ChatGPT, 2023). I did research and found another user using it for their interaction system as-well (GnAmez, 2023). This mechanism is used quite frequently in the real-world. Mostly for cruise control for cars and keeping ships on course (Wikipedia contributors, 2023b).
This is the formula for a PID Controller:


This is simple explanation to how PID controllers work:
Imagine you’re playing a flying game and you’re trying to keep yourself in place to drop a box onto a button for a puzzle. You would correct you movement until completion, every time you overshoot you move back to counteract it. A PID controller is exactly this, but automated.
Let’s break down the logic:
- P (Proportional): This is like you correcting your flight position in the bug game. The further you are from the target, the more you have to move.
- I (Integral): This is like while playing you notice the flying object always tilts a little right. The next time this happens you know it will happen so you tilt left to counteract this. This is what the Integral part does. It looks for repeated errors and adjusts them.
- D (Derivative): This is like correcting your movement in the flying game. If you see that you are going too far from the target you start correcting your path to reorient yourself back.
It’s basically an autonomous controller that tries to keep things in the right place with it’s set parameters.
Here is what it looked like when implemented into the grab script:

(green – 2kg • orange – 5kg • red – 8kg • black – 12kg)
Realistic Grabbing
When you grab something in real-life, the way that it’s being grabbed affects the object’s physics. For example is it being held only by the index finger and thumb:

or is it held entirely in the palm of your hand:

These are the factors to consider when making a system like this.
The PID Controller was a step in the right direction, but the only “fault” with it is that it’s grabbing the object from the middle/origin point. I need the grabbed object to tangle around.
I had two options to achieve this, first would be the joint method. I would implement it like I did before, but now also include the PID controller. The second method would be just changing the position where the force of the PID equation is applied. I went and tried both.
Code method results:

As you can see it didn’t turn out so well. The objects started glitching and flicking around when grabbed. I instead decided to go with the Joint method after getting affirmations from a post on the Godot forums (Holding a Rigidbody Object – Godot Forums, 2021).
func grab_anywhere():
var weight_influence = clamp(mass / g.player_strength, 0.1, 1.0)
var grabbed = self
var desired_position = i.hand.global_transform.origin
var force = pid.compute(desired_position, grabbed.global_transform.origin)
force /= weight_influence
grabbed.apply_impulse(grab_point - grabbed.global_transform.origin, force)
Joint method results:

The joint would be like a separate object/rigidbody that is always on the player, but also attaches itself to the grabbed object.
The PID Controller moves the joint object towards the hand position when an object is being grabbed.
func Action():
action_called = true
print("wassap I got grabbed")
i.grabbed_object = self
start_pin()
func Release():
action_called = false
print("wassap I got un-grabbed")
i.grabbed_object = null
reset_pin()
func grab_anywhere():
var weight_influence = clamp(mass / g.player_strength, 0.1, 1.0)
var grabbed = self
var desired_position = i.hand.global_transform.origin
var force = pid.compute(desired_position, i.pin_body.global_transform.origin)
force /= weight_influence
i.pin_body.apply_central_force(force)
func reset_pin():
i.pin.node_a = NodePath()
i.pin_body.freeze = true
i.pin_body.global_transform.origin = i.hand.global_transform.origin
func start_pin():
i.pin.node_a = i.grabbed_object.get_path()
i.pin_body.freeze = false
I ended up using the joint method as it was the closest to my goal. Also the math approach would’ve required me to use my brain more so I decided against it.
Precision Drag
The joint method was good, but for the next task I did need to use some math. I came across a video(CptFurball, 2022) that helped get some insight into the matter of developing a good drag system.
I took the code from the video and modified it to my needs.
extends RigidBody3D
var action_called : bool
var player
var camera
var door_plane_normal: Vector3 = Vector3(0, 1, 0)
@export var door_hinge : HingeJoint3D
var door_hinge_position: Vector3
var mouse_position : Vector2
@export var lerp_speed: float = 0.1
func _ready():
door_hinge_position = door_hinge.global_transform.origin
camera = g.player_camera
player = g.player
func _input(event):
if event is InputEventMouseMotion and action_called:
var x = event.relative.x * cos(-player.rotation.y) - event.relative.y * sin(-player.rotation.y)
var y = event.relative.x * sin(-player.rotation.y) + event.relative.y * cos(-player.rotation.y)
global_translate(Vector3(x * 0.001, 0, y * 0.001))
func _physics_process(delta):
if action_called:
var target_angle = calculate_door_rotation_angle()
var current_angle = rotation.y
var new_angle = lerp(current_angle, target_angle, lerp_speed * delta)
rotation.y = new_angle
func Action():
action_called = true
print("wassap you grabbed a door")
func Release():
action_called = false
print("wassap you un-grabbed a door")
var move_door_tolerance: float = 0.1
var detach_distance: float = 1.0
func move_object(_delta):
var direction: Vector3 = get_parent().handle.global_transform.origin.direction_to(global_transform.origin).normalized()
var distance: float = get_parent().handle.global_transform.origin.distance_to(global_transform.origin)
if distance > move_door_tolerance:
apply_force(direction, global_transform.origin - get_parent().handle.global_transform.origin)
func calculate_door_rotation_angle() -> float:
var ray_origin = i.hand.global_transform.origin
var ray_dir = (i.ray.target_position - i.hand.global_transform.origin).normalized()
# i.debug_sphere.global_transform.origin = intersect_ray_with_plane(ray_origin, ray_dir)
var intersection = intersect_ray_with_plane(ray_origin, ray_dir)
var reference_vector = Vector3(1, 0, 0)
var hinge_to_intersection = (intersection - door_hinge_position).normalized()
i.debug_sphere.global_transform.origin = intersect_ray_with_plane(ray_origin, ray_dir)
var dot = reference_vector.dot(hinge_to_intersection)
var det = reference_vector.cross(hinge_to_intersection).y
var angle = atan2(det, dot)
angle = clamp(angle, -80 , 80)
return angle
func intersect_ray_with_plane(ray_origin: Vector3, ray_dir: Vector3) -> Vector3:
var d = -door_hinge_position.dot(door_plane_normal)
var t = -(ray_origin.dot(door_plane_normal) + d) / ray_dir.dot(door_plane_normal)
return ray_origin + t * ray_dir

Next I moved onto the wheel functionality. I looked at the script that was available in the Amnesia open source files and tried to convert that over to Godot. This was the result…

The wheel didn’t want to rotate at all. I had to resort to another method for my rotation needs. I tried PID controllers, joints and reusing scripts that were for grabbing objects and doors. Nothing was working. So I had an idea to try a method that would definitely work, but not exactly the way I want it to. This method would use the mouse’s motion to rotate the wheel.
This method I did get working as it wasn’t that difficult to implement compared to the other methods which required more math calculations.

As you can see it doesn’t visually rotate the wheel and neither does it use physics. Something that proves that it works is the debug rotation text at the top-left. I wasn’t satisfied with this result either.
After some crying I was able to clear my mind and start over again. This time going back to a method I tried in the beginning, but was too lazy to fix. Turns out… that was the solution! THIS METHOD WAS THE ANSWER ALL ALONG!

func _physics_process(delta):
if interact:
var mouse_pos = get_viewport().get_mouse_position()
var ray = camera.project_ray_normal(mouse_pos)
var from = camera.project_ray_origin(mouse_pos)
var to = from + ray * 1000
var ray_params = PhysicsRayQueryParameters3D.create(from,to)
var space_state = get_world_3d().direct_space_state
var result = space_state.intersect_ray(ray_params)
if result:
var center_to_mouse = result.position - global_transform.origin
angle = atan2(center_to_mouse.z, center_to_mouse.x)
rotation.y = -angle
total_rotation = rad_to_deg(angle)
This script takes the 2D viewport position of the mouse and converts it to 3D coordinates using a raycast. If this raycast hits a suitable object (the wheel in this case) then it gets the angle between the center of the wheel and the mouse position. This angle is then applied to the wheel’s rotation.
This was a big motivator for me as I now had more time to work on other aspects of the project. One of those being the lever, which was the final mechanic to be implemented. This didn’t take much time as it’s just refactored code of the wheel. Two birds with one stone.

func _physics_process(delta):
if interact:
var mouse_pos = get_viewport().get_mouse_position()
var ray = camera.project_ray_normal(mouse_pos)
var from = camera.project_ray_origin(mouse_pos)
var to = from + ray * 1000
var ray_params = PhysicsRayQueryParameters3D.create(from,to)
var space_state = get_world_3d().direct_space_state
var result = space_state.intersect_ray(ray_params)
if result:
var center_to_mouse = result.position - global_transform.origin
var angle = atan2(center_to_mouse.y, center_to_mouse.z)
angle = clamp(angle, deg_to_rad(min_rotation), deg_to_rad(max_rotation))
rotation.x = -angle
total_rotation = rad_to_deg(angle)
The code is pretty much the same just some axis changes and clamping going on to prevent the lever from acting out of line.
Playtest
For the playtest I designed a short level for the testers to try out all of the features. Nothing too complex. This is what it looked like:

Each of the rooms would showcase the three different mechanics.
The first room showcases the object grabbing mechanic by having the player drag an object to a pressure plate to keep a gate open.

For the second room I wanted to have a ball-in-a-maze game with the wheels as the controls.
“The object of the game is to try to tilt the playfield to guide the marble to the end of the maze, without letting it fall into any of the holes.”
(Wikipedia contributors, 2023)
That sadly fell flat after it exposed some big bugs in my system with the wheel and lever. I first tried to use the lever for the puzzle, but I noticed that if you rotate the lever full 90 degrees it breaks. It stays broken even if you change the axis of rotation in the code.
So I decided to use the wheel instead. The wheel showed another bug in the ray cast by making the rotations be super sudden. I did have value clamping going on in the code and on the HingeJoints, but neither of those stopped the wheel from going past it’s limits. Seems like a Godot problem. In the end I scrapped the idea and opted for a more simple idea. A gate that will be raised by a wheel.

The third room showcases the lever mechanic by having the player fill one tank of water with another tank of water.

After that another door opens into a bright room. When the player walks inside the level is restarted and the test concludes.
One thing you won’t see in this playtest is the normal swinging door that I showed before. That is because I encountered this bug with it.

Playtest results
The playtest went alright. Testers did experience some new bugs and old bugs like the wheel’s raycast bug. For that I implement the possibility to skip the level as I wanted the players to experience all of the mechanics. Regardless, most of the feedback was positive. Biggest criticism were for the wheel and lever which I already knew would be a problem.









Improvements
The wheel and lever were the biggest generators for bugs. I always discovered new bugs with them and so did the testers.
The grabbing also had bugs as seen in the results gif and here:

This problem seems to stem from the joint rigidbody being set frozen when it’s not grabbing an object. Thus it strays further from the center(green cube) and when the player grabs again the rigidbody quickly accelerates towards the raycast hit position because of the PID controller. This causes the little bump/jump when grabbing objects again after moving or looking around. It isn’t really a big problem, but I am sure that it can scale to cause bigger bumps. I tried setting the rigidbody’s position multiple times in different places in the code, but nothing worked. It to cause the bigger bumps I was talking about.
In short, things in Godot seem to have a mind of their own.
Conclusion
This was a fun journey to embark on for 5 weeks. I had attempted to make this system before, but was unsuccessful as I wasn’t determined enough. With the weekly checkups from the guild leader, the task had more pressure. I tried giving my best each week based on how much time I had free. Most of my time went into researching and trying out methods to use and because I am not a programmer this took way longer than it should. This caused me to not always have something new to show for the guild meetings.
In the end, I was able to improve at programming, learned about PID controllers, joints and of course Godot itself. It is a powerful game engine if in the hands of the correct people. The correct people being programmers who have time to fix everything. The major problem that the engine suffers from is the absence of documentation, which I hope will be improved upon now that some Unity developers have started using it.
About the project itself, it was fun seeing stuff work after hours of trial and error. The rush of dopamine was so satisfying. One big conclusion that I came to at the end was that I don’t think I will be using Godot for my future projects as it still lacks some features needed for a newbie like me to use it. Maybe for simpler games only.
Resources
Physical Interaction | Physics | Unity Asset Store. (2022, January 1). Unity Asset Store. https://assetstore.unity.com/packages/tools/physics/physical-interaction-207611
FrictionalGames. (2020, October 12). GitHub – FrictionalGames/AmnesiaTheDarkDescent. GitHub. https://github.com/FrictionalGames/AmnesiaTheDarkDescent
Abra. (2022, August 6). Advanced Object Picking in Godot 3.4 – Tutorial Part 1 [Video]. YouTube. https://www.youtube.com/watch?v=jLIe1_xvOXU
FrictionalGames. (2010, September 3). Amnesia: TDD – Developer Walkthrough Physics Interaction [Video]. YouTube. https://www.youtube.com/watch?v=2ve0eVwjv5k
Let’s not forget about Physics – FrictionalGames. (2010, September 3). https://frictionalgames.com/2010-09-lets-not-forget-about-physics/
Amnesia Source Code Summary – ChatGPT. (2023, October 30). ChatGPT. https://chat.openai.com/share/1a7d959c-5bc7-489e-9a33-17381417b3e3
Explained, D.-. P. (2020b, July 23). PID controller explained. PID Explained. https://pidexplained.com/pid-controller-explained/
Wikipedia contributors. (2023b, October 27). Proportional–integral–derivative controller. Wikipedia. https://en.wikipedia.org/wiki/Proportional%E2%80%93integral%E2%80%93derivative_controller
CptFurball. (2022, January 9). Amnesia’s Precision Door Control and WTF is PMC? [Video]. YouTube. https://www.youtube.com/watch?v=DvdB5UxbndQ
GnAmez. (2023, April 10). Reddit – Dive into anything. https://www.reddit.com/r/godot/comments/12hkzki/physics_playground/?context=3
Holding a Rigidbody object – Godot Forums. (2021, November 6). https://godotforums.org/d/27925-holding-a-rigidbody-object
Wikipedia contributors. (2023, September 23). Labyrinth (marble game). Wikipedia. https://en.wikipedia.org/wiki/Labyrinth_(marble_game)
How To Get 3D Position Of The Mouse Cursor. (2018, April 26). Godot Engine – Q&A. https://ask.godotengine.org/25922/how-to-get-3d-position-of-the-mouse-cursor
Godotengine. (2020, September 8). Camera.project_ray_origin always returns the origin of the camera when in perspective projection. · Issue #41872 · godotengine/godot. GitHub. https://github.com/godotengine/godot/issues/41872
Lumenwrites. (2020, April 9). Reddit – Dive into anything. https://www.reddit.com/r/godot/comments/fxwq9d/a_question_about_projecting_mouse_cursor_onto_3d/
