🔗LBJT: Mouse Joint 2️⃣.
LibGDX Box2d Joints Tutorials.
Text separators are set using chatGPT.
This article is about the Mouse Joint, its settings, and how to make it work.
To make it easier to understand what is happening and how to perform practical tasks, start with the article: 🔗LBJT: Сommon in Joints 1️⃣.
And so the… Mouse Joint
Mouse joint — the joint is designed to move dynamic bodies by the user.
First, we need to set up the joint. For this, each joint has its own implementation of JointDef. You can read about this in the article: 🔗LBJT: Common in Joints 1️⃣. And now let’s talk about the MouseJointDef.
MouseJointDef settings:
(Mandatory)
- bodyA — first joint body. (StaticBody)
- bodyB — second joint body. (DynamicBody)
- collideConnected — <boolean> Specifies whether bodyA should collide with bodyB.
(Optional)
- target — The <Vector2> target point that matches the user’s touch coordinates will cause bodyA to move relative to that point.
- maxForce — the force required to move bodyB. Usually expressed as (multiplier * mass * gravity) = (1000 * bodyB.mass * world.gravity ). I personally usually do this for tests (1000 * bodyB.mass).
(Note: ) Why 1000? Because if the force is too small or the body is too heavy, we won’t be able to move it. A force 1000 times the mass of bodyB is sufficient to move it.
- frequencyHz — also known as the response rate, works like a spring between the user’s finger (target) and bodyB.
Value < 1: With values less than 1, such as 0.5 or 0.7, the joint will be strongly stretched, resembling a soft spring.
Value > 1: With values greater than 1, such as 2, 5, or 10, the joint will stretch very little or not at all, similar to a stiff spring.
- dampingRatio — suppresses vibrations and takes values from 0 to 1.
0: does not suppress.
1: almost absolute suppression.
But values greater than 1 are possible, although in such cases the body will move very slowly. On the other hand, values less than 0 will make the body highly active and behave erratically, similar to wild behavior. However, it is generally not recommended to use values outside the range of 0 to 1.
Practice. How to create a Mouse Joint?
Our example will consist of a static platform and a dynamic ball on it. We need to make it so that the user can touch the ball and move it.
1️⃣Create 2 bodies: a static platform and a dynamic ball.
val platform = StaticBody
val ball = DynamicBody
2️⃣Add an InputListener to the stage on which the UI is drawn, and redefine the touchDown, touchDragged, and touchUp methods.
stageUI.addListener(object: InputListener() {
override fun touchDown()
override fun touchDragged()
override fun touchUp()
})
3️⃣Сreate reference fields for:
- MouseJoint (because it will be created and destroyed in different methods);
- The body that will move;
- Click coordinates in Box2d dimensions;
- QueryCallback to determine if the user clicked on the body they want to move.
/*1*/ val mouseJoint: MouseJoint? = null
/*2*/ val hitBody : Body? = null
/*3*/ val touchPoint: Vecto2 = Vector2()
/*4*/ val callback : QueryCallback = QueryCallback { fixture ->
if (fixture.testPoint(touchPoint)) {
hitBody = fixture.body
return@QueryCallback false
}
return@QueryCallback true
}
(Note: ) QueryCallback — should return a boolean value, what does that mean?
- true — returns something to CONTINUE searching for body fixtures.
- false — returns something to STOP searching for body fixtures.
4️⃣In the touchDown method:
- For touchPoint set click coordinates in Box2d sizes;
- For hitBody set null;
- Call QueryAABB with implementation callback and coordinates touchPoint + 0.01;
- We check if the callback has determined that the user clicked within the coordinates of the body fixture. If this condition is met, we set the corresponding body as the hitBody. Then, we create a MouseJoint between the platform and the hitBody and assign this connection to the variable mouseJoint;
- We set the target in the coordinates of the touchPoint;
- We set the maxForce (1000 * bodyB.mass).
override fun touchDown(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int): Boolean {
/*1*/ touchPoint.set(Vector2(x, y).toBox2d)
/*2*/ hotBody = null
/*3*/ world.QueryAABB(callback,
touchPoint.x - 0.01f, touchPoint.x + 0.01f,
touchPoint.y - 0.01f, touchPoint.y + 0.01f)
/*4*/ hitBody?.let { hitB ->
world.createJoint(MouseJointDef().apply {
bodyA = platform
bodyB = hitB
collideConnected = true
/*5*/ target.set(touchPoint)
/*6*/ maxForce = 1000 * bodyB.mass
})
}
return true
}
(Note: ) Vectore2().toBox2d — Converts sizes from UI to Box2d thanks to the meter constant. If you do not know how to convert sizes from UI to Box2d, read the article: LBJT: Meter Constant.
5️⃣In the touchDragged method, change the coordinates of the mouseJoint to the coordinates of the user converted to Box2d:
override fun touchDragged(event: InputEvent?, x: Float, y: Float, pointer: Int) {
mouseJoint?.target = Vector2(x, y).toBox2d
}
6️⃣In the touchUp method, destroy the mouseJoint:
override fun touchUp(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int) {
world.destroyJoint(mouseJoint)
}
(Note: ) The destruction in the touchUp method is only for demonstration purposes. In a real example, you would need to destroy bodies and joints after updating the world. The process of destroying bodies and joints is described in Destroy the joints 💥. It is important to perform the destruction step after updating the world to ensure proper synchronization and avoid any potential issues.
All code:
val platform = StaticBody
val ball = DynamicBody
stageUI.addListener(object: InputListener() {
val mouseJoint: MouseJoint? = null
val hitBody : Body? = null
val touchPoint: Vecto2 = Vector2()
val callback : QueryCallback = QueryCallback { fixture ->
if (fixture.testPoint(touchPoint)) {
hitBody = fixture.body
return@QueryCallback false
}
return@QueryCallback true
}
override fun touchDown(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int): Boolean {
touchPoint.set(Vector2(x, y).toBox2d)
hotBody = null
world.QueryAABB(callback,
touchPoint.x - 0.01f, touchPoint.x + 0.01f,
touchPoint.y - 0.01f, touchPoint.y + 0.01f)
hitBody?.let { hitB ->
world.createJoint(MouseJointDef().apply {
bodyA = platform
bodyB = hitB
collideConnected = true
target.set(touchPoint)
maxForce = 1000 * bodyB.mass
})
}
return true
}
override fun touchDragged(event: InputEvent?, x: Float, y: Float, pointer: Int) {
mouseJoint?.target = Vector2(x, y).toBox2d
}
override fun touchUp(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int) {
world.destroyJoint(mouseJoint)
}
})
That’s all. However, in most practical examples, it is common to include an additional check in the QueryCallback to verify if the found body is dynamic. This is because kinematic or static bodies are not movable and, therefore, not relevant for certain interactions.
A few more examples (cool effects)
(Note: ) For these examples, the previous code will be used, it just needs to be changed a little.
1️⃣Let’s do another example without QueryCallback, don’t care where the user clicks the joint will be created between the ball and the target (user click):
- In the touchDown method set bodyB like a ball;
- In the touchDown method (Delete / Comment out / Leave as is / Don’t care) hitBody and QueryAABB.
override fun touchDown(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int): Boolean {
touchPoint.set(Vector2(x, y).toBox2d)
world.createJoint(MouseJointDef().apply {
bodyA = platform
/*1*/ bodyB = ball
collideConnected = true
target.set(touchPoint)
maxForce = 1000 * bodyB.mass
})
return true
}
2️⃣Another cool effect is if you set the target as bodyB.position in the touchDown method to the previous example:
world.createJoint(MouseJointDef().apply {
bodyA = platform
bodyB = ball
collideConnected = true
/* UPDATE */ target.set(bodyB.position)
maxForce = 1000 * bodyB.mass
})
The body is attracted to the user’s touch like a magnet, isn’t it a miracle? How cool is that!) 😎
PS. Vel_daN: Love what You DO 💚.