Rotating a Box2D light/body based on mouse input?

I am trying to implement a mechanic you can visualize as a top-down flashlight.

I am using LibGDX and Box2D. I have never done anything using Box2D before, so the PPM scaling and the integration with Sprites, etc, really confuses me. I have made a small game with LibGDX before, you can find it here.

Problems:

  • I feel like I am scaling the camera with PPM (pixels per meter) incorrectly
  • The light does not turn correctly
  • Do not know how to tween between positions

To do this, in the show() method, I have attached a ConeLight to a Box2D body using the following method:


		PolygonShape shape = new PolygonShape();
		shape.setAsBox(5 / PPM, 5 / PPM);

		BodyDef bdef = new BodyDef();
		bdef.position.set(160 / PPM, 200 / PPM);
		bdef.type = BodyType.DynamicBody;
		body = world.createBody(bdef);
		
		FixtureDef fdef = new FixtureDef();
		fdef.shape = shape;
		body.createFixture(fdef);
		
		rayHandler = new RayHandler(world);
		cone = new ConeLigh
        (rayHandler, 40, Color.WHITE, 30, 160 / PPM, 200 / PPM, -90, 40);


Then, again in the show() method, I set up the camera:


		b2dcam = new OrthographicCamera();
		b2dcam.setToOrtho(false, Gdx.graphics.getWidth() / PPM, Gdx.graphics.getHeight() / PPM);

I am rendering it like this in the render() method:


		Gdx.gl20.glClear(GL20.GL_COLOR_BUFFER_BIT);
		world.step(1f/60f, 6, 2);
		b2dr.render(world, b2dcam.combined);
		rayHandler.setCombinedMatrix(b2dcam.combined);
		rayHandler.updateAndRender();

I am handling the input in this fashion:


    public boolean touchDown(int screenX, int screenY, int pointer, int button) {
        
    	if(button == Buttons.LEFT){
            body.setTransform(body.getPosition(), (float) (Math.atan2( (body.getPosition().y - screenY),
            											   (screenX - body.getPosition().x) ) ));
        }
        return false;
    }

The light rotates on mouse click, which means the listener is working, but it does not rotate to the correct point. I assume it has something to do with my math being incorrect, and the scaling done from meters to pixels being wrong.

Can someone help me on both of these issues? The intended behavior is illustrated below:

When the mouse is clicked, the body, and by association the ConeLight, should move to face the direction of the mouse click.

My full code can be viewed here: https://gist.githubusercontent.com/Elsealabs/1afaa812aafb56ecd3c2/raw/5d0959df795516c89fb7e6ab81aecc01dc8cd441/gistfile1.txt
[2]: http://i.stack.imgur.com/S6e5u.png

If you have a camera, you don’t even have to bother with scaling by PPM, and vectors are your friend:

(note, tweening part not tested)


float duration = .5f;


// on click:
float time = 0;
// leverage the camera to do coordinate transforms
Vector3 m3 = camera.unproject(new Vector3(Gdx.input.getX(), Gdx.input.getY(), 0));
Vector2 mouse = new Vector2(m3.x, m3.y); // in world coords
float startAngle = body.getAngle();
float targetAngle = mouse.sub(body.getPosition()).angleRad();


// every frame:
time += delta;
float currentAngle = Interpolation.Swing.apply(startAngle, targetAngle, MathUtils.clamp(time / duration, 0, 1)); // use whatever interpolation function looks good
// set body transform using currentAngle

Thank you so much for the help, but I actually have no idea what is going on in your code with the vectors and the camera unproject, etc. Can you please explain?

I am stuck on things like “mouse.sub(…)” and “.angleRad();” and why one is a Vector3 and one is a Vector2. And what does making the mouse coordinates into a vector do, etc?

How should I set up my camera and world instead? I want the world to scale on resize, like follow an aspect ratio but not stretch.

When attempting to do the interpolation, using the following code, I get the following error.


float currentAngle = Interpolation.Swing.apply(angleStart, angleEnd, MathUtils.clamp(time / .5f, 0, 1));

Cannot make a static reference to the non-static method apply(float, float, float) from the type Interpolation

Sorry for triple posting, but I sort of got it working. Except the interpolation is not working. I am using these lines of code for the interpolation.


time += delta;
float currentAngle = new Interpolation.Swing(1).apply(angleStart, angleEnd, MathUtils.clamp(time / .5f, 0, 1));
body.setTransform(body.getPosition(), currentAngle);

Below is a gif of the program working without interpolation.

Edit:

I also want to get it to where not only does it move from angleA to angleB, but where the position also moves from originalPosition to mouseClick. And not just teleports, but interpolates. I am trying to think of how to do this, but can’t at the moment. If anyone can help that would be greatly appreciated.

Edit #2:

Got it working by setting time to zero at the end of each mouse click.

Edit #3

Here’s a gif of it working:

It messes up and spins in weird directions when traveling long distances sometimes.

IMPORTANT:
Also, I can infer what most of the code I am using is doing, but the two lines that handle the mouse movement are really tripping me up. I have no idea what these two lines are doing, and if someone could explain it to me, it would be really nice.


Vector3 m3 = b2dcam.unproject(new Vector3(Gdx.input.getX(), Gdx.input.getY(), 0));
Vector2 mouse = new Vector2(m3.x, m3.y);

Edit #4

IMPORTANT:
I tried to work on the mechanical where the body moves to the new mouse click as well as the light turning in that direction, but it does not work at all. It jumps all over the place upon click. I think this has a lot to do with me not understanding the aforementioned lines of code.

To do body moving, in the show() method I added the following


Vector3 m3 = b2dcam.unproject(new Vector3(Gdx.input.getX(), Gdx.input.getY(), 0));
prevMouseClick = new Vector2(m3.x, m3.y);
currentMouseClick = new Vector2(m3.x, m3.y);

And I modified the touchDown() method to contain this code to set the previous and current mouse positions:


prevMouseClick = currentMouseClick;
Vector3 m3 = b2dcam.unproject(new Vector3(Gdx.input.getX(), Gdx.input.getY(), 0));
currentMouseClick = new Vector2(m3.x, m3.y);

Then, in the render() method I added


if (moving)
{
    float currentPositionX = new Interpolation.Swing(1).apply(prevMouseClick.x, currentMouseClick.x, MathUtils.clamp(time / 1f,  0, 1));
    float currentPositionY = new Interpolation.Swing(1).apply(prevMouseClick.y, currentMouseClick.y, MathUtils.clamp(time / 1f,  0, 1));
    body.setTransform(new Vector2(currentPositionX, currentPositionY), body.getAngle());
}

Instead of moving as I expected, the body moves sporadically upon click.

Final Edit:

I have no idea what I am doing with scaling the world with PPM. My Box2D bodies seem extremely small, and the background I try to draw is extremely zoomed in and pixelated. Please see this thread. I really need some help. http://www.java-gaming.org/topics/creating-a-box2d-world-of-a-certain-size-whose-viewport-scales/35235/view.html

Yeah I forgot how the Interpolation class works, the actual instances are lowercase.

camera.unproject() transforms a screen-coordinate vector (taken from Gdx.input) into a world-coordinate vector, to be used by box2d.

Here’s some code I actually tested this time, I also fixed the weird angles:


// stored elsewhere:
float duration = .5f;
      
Vector2 startPosition, targetPosition;
float time = 0;
float startAngle, targetAngle;
boolean moving;


// every frame
if (Gdx.input.isButtonPressed(Buttons.LEFT)) {
   time = 0;
   startPosition = body.getPosition();
   Vector3 m3 = camera.unproject(new Vector3(Gdx.input.getX(), Gdx.input.getY(), 0));
   targetPosition = new Vector2(m3.x, m3.y);
   startAngle = body.getAngle();
   targetAngle = targetPosition.cpy().sub(startPosition).angleRad();
            
   // make sure we take smallest angle path, this was the weird angles
   if (Math.abs(targetAngle - startAngle) > MathUtils.PI) {
      if (startAngle < targetAngle) {
         startAngle += MathUtils.PI2;
      } else {
         targetAngle += MathUtils.PI2;
      }
   }
   moving = true;
}
         
if (moving) {
   float alpha = time / duration;
   if (alpha <= 1) {
      // Interpolation.circleOut also looks good I think
      float currentAngle = Interpolation.linear.apply(startAngle, targetAngle, alpha);
      Vector2 currentPosition = startPosition.cpy().lerp(targetPosition, alpha);
      body.setTransform(currentPosition, currentAngle);
               
      time += Gdx.graphics.getDeltaTime();
   } else
      moving = false;
   }
}

It’s pretty buggy. And I want to try to implement it where the user holds down the cursor, as someone would when moving a character on a phone.

It acts like this:

I thought this project was going to be a cool idea, but now I’m wondering if I may be too far in over my head and should just give up.

Well it was intended for single clicks, if you want continuous input that’s starting become paths.
This also isn’t the intended use of Box2D, as setTransform() subverts the physics simulation entirely.

Maybe just applyForce/Torque towards where the user is currently touching down? If that doesn’t look good then you can use impulses, controlled by your own interpolation.

That’s a good idea.

Given a body, how would you apply a force so that it goes in a certain direction. I will attempt to Google this, but if you can give me an answer in psuedo code or something then that would be cool.

I’ll attempt to implement this and post back at this thread with my results, if any.

Well the force is a vector, so the body will be pushed in that direction.

Example:


float forceAmount = 2; // two newtons
// push towards <target>
body.applyForce(target.cpy().sub(body.getPosition()).nor().scl(forceAmount), true);

So since, after a touchDown event, touchDragged() events are fired until touchUp(), I got the delta using this method:


	@Override
	public boolean touchDown(int screenX, int screenY, int pointer, int button)
	{
		if (lastTouch == null) lastTouch = new Vector2(screenX, screenY);
		else lastTouch.set(screenX, screenY);
		return false;
	}
	
	@Override
	public boolean touchDragged(int screenX, int screenY, int pointer)
	{
		Vector2 newTouch = new Vector2(screenX, screenY);
	    Vector2 delta = newTouch.cpy().sub(lastTouch);
	    lastTouch = newTouch;
		return false;
	}

Given that delta vector, would I apply the force like this?


body.applyForce(delta.cpy().sub(body.getPosition()).nor().scl(forceAmount), true);

Something like that, although you will need to unproject it first.

I did this, but it is reacting oddly. It is moving, but in an opposite direction and off the screen, not towards the touchDown.


	@Override
	public boolean touchDragged(int screenX, int screenY, int pointer)
	{
		Vector2 newTouch = new Vector2(screenX, screenY);
	    Vector2 delta = newTouch.cpy().sub(lastTouch);
	    Vector3 undelta = camera.unproject(new Vector3(delta.x, delta.y, 0));
	    
	    body.applyForceToCenter(
	    	new Vector2(undelta.x, undelta.y).cpy().sub(body.getPosition()).nor().scl(10), true
	    );

	    lastTouch = newTouch;
		return false;
	}

Don’t subtract lastTouch, sorry I didn’t see that the first time.
Also delta is a fairly misleading name, it’s just a position, the undelta.sub(…) is the delta (technically displacement)

Also you don’t need to cpy() single-use vectors.

Sorry I don’t think I am getting what you are saying? Where am I subtracting last touch? What should I do instead?

[icode]Vector2 delta = newTouch.cpy().sub(lastTouch);[/icode]

Just past newTouch.x,y to unproject. (or just screenX,Y)

Using this code, no matter what area is pressed, the box moves northeast.


	@Override
	public boolean touchDragged(int screenX, int screenY, int pointer)
	{
		Vector2 newTouch = new Vector2(screenX, screenY);
		System.out.println(newTouch.toString());
		
	    body.applyForceToCenter(
	    	new Vector2(newTouch.x, newTouch.y).cpy().sub(body.getPosition()).nor().scl(10), true
	    );

	    lastTouch = newTouch;
		return false;
	}

This is some of the console output to show that the drag position is indeed changing.

Touch down
Touch down
Touch down
Touch down
[245.0:172.0]
[233.0:173.0]
[224.0:173.0]
[223.0:175.0]
[218.0:175.0]
[217.0:176.0]
...

I’m not really grasping the concepts, so it is hard for me to understand what you are telling me to do. I am going to try to find a tutorial on forces in Box2D, but me googling my issue just gives similar confusing results.

I’m at a point where I’m actually considering offering compensation for an extremely in-depth explanation of the concepts I am attempting to understand, since I can’t find many answers that go in depth when googling. But I guess I’ll keep looking!

You forgot to unproject:


@Override
public boolean touchDragged(int screenX, int screenY, int pointer)
{
   Vector3 newTouch = camera.unproject(new Vector3(screenX, screenY, 0));
      
   body.applyForceToCenter(
      new Vector2(newTouch.x, newTouch.y).sub(body.getPosition()).nor().scl(10), true
   );

   lastTouch = newTouch;
   return false;
}

This is a good source on Box2D, although the code is in C++ it’s quite transferable.
http://www.iforce2d.net/b2dtut/

What it sounds like though is that you really just need to brush up on vectors.
There’s a good video series here: https://www.youtube.com/results?search_query=math+for+game+developers+vector

This worked. Though the mechanic simulated was far more similar to ice skating than it was to walking. I played around with the linear dampening and the density until it acted more like walking. I will now attempt to integrate the light movement with this feature. I will post back when I get that working, but look at how it looks now with a linear dampening of 5 and a density of .1 :

On second thought: I’ll upload the gif when I am able. I am on the road currently, using mobile data on my Surface, so uploading a gif to imgur isn’t going so well!

Yeah, getting the forces right can be tricky, impulses (via applyLinearImpulse) might be the way to go depending on the feel you want. Same with applyAngularImpulse vs. applyTorque