I am developing an android app which visualize the map of an environment and currently i am using libgdx to draw the map, also like any map application the user should be capable of zoom, rotate and moving the map,
I have developed a GestureHandler class which implements GestureListener interface and interacts with a PerspectiveCamera(since i will use 3d components in the future):
#Override
public boolean pan(float x, float y, float deltaX, float deltaY) {
float tempX = (mapView.getCamera().position.x - deltaX * 0.5f);
float tempY = (mapView.getCamera().position.y + deltaY * 0.5f);
mapView.getCamera().position.set(
MathUtils.lerp(mapView.getCamera().position.x, tempX, mapView.getCamera().fieldOfView / 100),
MathUtils.lerp(mapView.getCamera().position.y, tempY, mapView.getCamera().fieldOfView / 100),
mapView.getCamera().position.z);
mapView.getCamera().update();
return false;
}
float initialDistance = 0;
float initialAngle = 0;
float distance = 0;
private void zoom(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2)
{
initialDistance = initialPointer1.dst(initialPointer2);
float iDeltaX = initialPointer2.x - initialPointer1.x;
float iDeltaY = initialPointer2.y - initialPointer1.y;
initialAngle = (float)Math.atan2((double)iDeltaY,(double)iDeltaX) * MathUtils.radiansToDegrees;
if(initialAngle < 0)
initialAngle = 360 - (-initialAngle);
distance = initialPointer1.dst(pointer2);
float deltaX = pointer2.x - initialPointer1.x;
float deltaY = pointer2.y - initialPointer1.y;
newAngle = (float)Math.atan2((double)deltaY,(double)deltaX) * MathUtils.radiansToDegrees;
if(newAngle < 0)
newAngle = 360 - (-newAngle);
//Log.e("test", distance + " " + initialDistance);
//Log.e("test", newAngle + " " + initialAngle);
float ratio = initialDistance/distance;
mapView.getCamera().fieldOfView = MathUtils.clamp(initialZoomScale * ratio, 1f, 100.0f);
Log.e("zoom", String.valueOf(mapView.getCamera().fieldOfView));
mapView.getCamera().update();
}
#Override
public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2) {
zoom(initialPointer1, initialPointer2, pointer1, pointer2);
float delta1X = pointer2.x - pointer1.x;
float delta1Y = pointer2.y - pointer1.y;
newAngle = (float)Math.atan2((double)delta1Y,(double)delta1X) * MathUtils.radiansToDegrees;
if(newAngle < 0)
newAngle = 360 - (-newAngle);
System.out.println("new "+newAngle);
if(newAngle - currentAngle >= 0.01000f)
{
System.out.println("Increasing");
mapView.getCamera().rotate(0.5f,0,0,1);
}
else if(newAngle - currentAngle <= -0.010000f) {
System.out.println("DEcreasing");
mapView.getCamera().rotate(-0.5f,0,0,1);
}
if(Math.abs(newAngle - currentAngle) >= 0.01000f)
{
currentAngle = newAngle;
}
return true;
}
Everything is fine until as far as i don't rotate the camera, just like this unsolved similar question after rotating the camera, movements will be affected by applied rotation.Any help specially sample codes?
Edit:
After lots of efforts i finally solved it,
As Tenfour04 said in his answer i had to use two separate matrices for transformation and rotations, and finally set the result of their multiplication to view matrix of camera using:
camera.view.set(position).mul(orientation);
Also the most important thing is to set the Transformation Matrix of my batch to camera.view:
batch.setTransformationMatrix(camera.view)
Instead of applying the gestures directly to the camera, apply them to a pair of Matrix4's that you use to store the orientation and position separately. Then in the render method, multiply the two matrices and apply them to your camera's view.
In the render() method:
camera.view.set(orientation).mul(position); //Might need to swap orientation/position--don't remember.
camera.update();
Your zoom method is fine because field of view affects the camera's projection matrix rather than its view matrix.
Related
I am trying to create a pad-like view in android. I got a circle that follows user's gestures and I am using distance to keep the circle of going outside the main circle of the pad control.
My problem is I want the circle to keep following the gesture, but to stay inside of the main circle. I am using the formula for finding a point using angle and radius, but it does some funky stuff.
I am translating the canvas, so that the center of the main circle is at 0, 0.
Here is the code:
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(this.mainRadius, this.mainRadius);
canvas.drawCircle(0, 0, this.mainRadius, this.debugPaint);
canvas.drawCircle(this.handleX, this.handleY, this.handleRadius, this.handlePaint);
}
private void translateHandle(MotionEvent event) {
int x = (int) (event.getX() - this.mainRadius);
int y = (int) (event.getY() - this.mainRadius);
double distance = distanceFromCenter(x, y);
if (distance <= this.maxDistance) {
this.handleX = x;
this.handleY = y;
} else {
float angle = (float) Math.toDegrees(Math.atan2(y, x));
if (angle < 0)
angle += 360;
this.handleX = (int) ((this.mainRadius - this.handleRadius) * Math.cos(angle));
this.handleY = (int) ((this.mainRadius - this.handleRadius) * Math.sin(angle));
}
//onTranslateHandle(distance);
}
And here is the funky stuff in a gif image:
I cannot verify this change into your code snippet but do hope it gives some idea how to proceed further anyway;
private void translateHandle(MotionEvent event) {
float x = event.getX() - this.mainRadius;
float y = event.getY() - this.mainRadius;
double distance = distanceFromCenter(x, y);
if (distance > this.maxDistance) {
// If distance is i.e 2.0 and maxDistance is 1.0 ==> adjust is 0.5
// which repositions x and y making distance 1.0 maintaining direction
double adjust = this.maxDistance / distance;
x = (float)(x * adjust);
y = (float)(y * adjust);
}
this.handleX = (int)x;
this.handleY = (int)y;
}
I can update the answer where needed if this does not give any useful results.
Hello I searched in the forum,but coudn't find a helpful answer.
I'm making a game with AndEngine and I'm stuck for 3 days on shooting from rotating sprite.
That is my code and how I rotate the gun.I tried here to shoot a bullet ,but it shoots from a wrong starting point I would want to shoot a bullet from the end of the gun.
#Override
public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) {
if(pSceneTouchEvent.isActionMove()){
final float dX = pSceneTouchEvent.getX() - machine.getX();
final float dY = pSceneTouchEvent.getY() - machine.getY();
float angle = (float) Math.atan2(dX,dY);
float rotation = MathUtils.radToDeg(angle) + 1;
machine.setRotation(rotation - 90);
Log.d("BUG",machine.getRotation() + "");
if(machine.getRotation() >= 84 ){
machine.setRotation(84);
}
if(machine.getRotation() <= -54 ){
machine.setRotation(-54);
}
final int incrementXValue = 15;
long sElapsed = System.currentTimeMillis() - lastFire;
if(bulletsAmout > 0 && sElapsed > cooldownBetweenShoot * cdModd){
e = new Entity(0,machine.getY());
e.setRotation(getRotation());
SceneManager.getInstance().getCurrentScene().attachChild(e);
float x2 = (float) (machine.getSceneCenterCoordinates()[0] + machine.getWidth() /2 * Math.cos(machine.getRotation()));
float y2 = (float) (machine.getSceneCenterCoordinates()[1] + machine.getWidth() /2 * Math.sin(machine.getRotation()));
float realX = (float) (Math.toRadians(x2) + machine.getWidth());
realY = (float) Math.toRadians(y2);
bullets = new Sprite(realX,realY, resourcesManager.bulletRegion.deepCopy(), vbom){
protected void onManagedUpdate(float pSecondsElapsed) {
float currentX = this.getX();
this.setX(currentX + incrementXValue);
super.onManagedUpdate(pSecondsElapsed);
}
};
bullets.setScale(0.06f);
e.attachChild(bullets);
projectilesToBeAdded.add(bullets);
bulletsAmout--;
lastFire = System.currentTimeMillis();
setBulletsText(bulletsAmout);
resourcesManager.pistolSound.play();
}
return true;
}
return false;
}
Assuming you are using GLES2-AnchorCenter:
You can position the bullet by setting it to the position of the end of the gun that you can get by calling gun.convertLocalToSceneCoordinates(gunMuzzleX, gunMuzzleY).
Then set the bullets rotation to the rotation of the gun.
apply velocity to the bullet. Calculate the speed-vector as follows FloatMath.sin(rotationOfBulletInRadians) * speed and FloatMath.cos(rotationOfBulletInRadians) * speed.
Be aware that you have to pass the rotation in radians to the sin and cos function NOT in degrees!
So I found how to fix that.
The problem is in this line of code :
e = new Entity(0,machine.getY());
Should be :
e = new Entity(machine.getX() - (machine.getHeight() / 2),machine.getY())
I have a SurfaceView that is resposible for drawing a Bitmap as a background and another one that will be used as an overlay. So I've decided to do all transformations using a Matrix that can be used for both bitmaps as it is (I think) one of the fastest ways to do it without using OpenGL.
I've been able to implement panning around and zooming but I have some problems with what I've came with:
I wasn't able to find a way how to focus on the center of the two
fingers while zooming, the image always resets to its initial state
(that is, without panning nor scalling) before the new scale being
applied. Besides looking wrong, that doesn't allow the user to zoom
out to see the whole image and then zoom in on the part that is
important.
After the scalling operation the image won't be at the
same place after the new draw pass because the translation value will
be different.
Is there a way to achieve that using a Matrix or is there another solution?
Code is below (I use a SurfaceHolder in a separate thread do lock the SurfaceView canvas and call its doDraw method):
public class MapSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
public void doDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
canvas.drawBitmap(mBitmap, mTransformationMatrix, mPaintAA);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_DOWN: {
if (event.getPointerCount() == 2) {
mOriginalDistance = MathUtils.distanceBetween(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
mScreenMidpoint = MathUtils.midpoint(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
mImageMidpoint = MathUtils.midpoint((mXPosition+event.getX(0))/mScale, (mXPosition+event.getX(1))/mScale, (mYPosition+event.getY(0))/mScale, (mYPosition+event.getY(1))/mScale);
mOriginalScale = mScale;
}
}
case MotionEvent.ACTION_DOWN: {
mOriginalTouchPoint = new Point((int)event.getX(), (int)event.getY());
mOriginalPosition = new Point(mXPosition, mYPosition);
break;
}
case MotionEvent.ACTION_MOVE: {
if (event.getPointerCount() == 2) {
final double currentDistance = MathUtils.distanceBetween(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
if (mIsZooming || currentDistance - mOriginalDistance > mPinchToZoomTolerance || mOriginalDistance - currentDistance > mPinchToZoomTolerance) {
final float distanceRatio = (float) (currentDistance / mOriginalDistance);
float tempZoom = mOriginalScale * distanceRatio;
mScale = Math.min(10, Math.max(Math.min((float)getHeight()/(float)mBitmap.getHeight(), (float)getWidth()/(float)mBitmap.getWidth()), tempZoom));
mScale = (float) MathUtils.roundToDecimals(mScale, 1);
mIsZooming = true;
mTransformationMatrix = new Matrix();
mTransformationMatrix.setScale(mScale, mScale);//, mImageMidpoint.x, mImageMidpoint.y);
} else {
System.out.println("Dragging");
mIsZooming = false;
final int deltaX = (int) ((int) (mOriginalTouchPoint.x - event.getX()));
final int deltaY = (int) ((int) (mOriginalTouchPoint.y - event.getY()));
mXPosition = mOriginalPosition.x + deltaX;
mYPosition = mOriginalPosition.y + deltaY;
validatePositions();
mTransformationMatrix = new Matrix();
mTransformationMatrix.setScale(mScale, mScale);
mTransformationMatrix.postTranslate(-mXPosition, -mYPosition);
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
mIsZooming = false;
validatePositions();
mTransformationMatrix = new Matrix();
mTransformationMatrix.setScale(mScale, mScale);
mTransformationMatrix.postTranslate(-mXPosition, -mYPosition);
}
}
return true;
}
private void validatePositions() {
// Lower right corner
mXPosition = Math.min(mXPosition, (int)((mBitmap.getWidth() * mScale)-getWidth()));
mYPosition = Math.min(mYPosition, (int)((mBitmap.getHeight() * mScale)-getHeight()));
// Upper left corner
mXPosition = Math.max(mXPosition, 0);
mYPosition = Math.max(mYPosition, 0);
// Image smaller than the container, should center it
if (mBitmap.getWidth() * mScale <= getWidth()) {
mXPosition = (int) -((getWidth() - (mBitmap.getWidth() * mScale))/2);
}
if (mBitmap.getHeight() * mScale <= getHeight()) {
mYPosition = (int) -((getHeight() - (mBitmap.getHeight() * mScale))/2);
}
}
}
Instead of resetting the transformation matrix every time using new Matrix(), try updating it using post*(). This way, you do only operations relative to the screen. It is easier to think in terms: "zoom to this point on the screen".
Now some code. Having calculated mScale in zooming part:
...
mScale = (float) MathUtils.roundToDecimals(mScale, 1);
float ratio = mScale / mOriginalScale;
mTransformationMatrix.postScale(ratio, ratio, mScreenMidpoint.x, mScreenMidpoint.y);
It might be even better to recalculate mScreenMidpoint on each zooming touch event. This would allow user to change the focus point a bit while zooming. For me, it is more natural than having the focus point frozen after first two finger touch.
During dragging, you translate using deltaX and deltaY instead of absolute points:
mTransformationMatrix.postTranslate(-deltaX, -deltaY);
Of course now you have to change your validatePositions() method to:
ensure deltaX and deltaY do not make image move too much, or
use transformation matrix to check if image is off screen and then move it to counter that
I will describe the second method, as it is more flexible and allows to validate zooming as well.
We calculate how much image is off screen and then move it using those values:
void validate() {
mTransformationMatrix.mapRect(new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()));
float height = rect.height();
float width = rect.width();
float deltaX = 0, deltaY = 0;
// Vertical delta
if (height < mScreenHeight) {
deltaY = (mScreenHeight - height) / 2 - rect.top;
} else if (rect.top > 0) {
deltaY = -rect.top;
} else if (rect.bottom < mScreenHeight) {
deltaY = mScreenHeight - rect.bottom;
}
// Horziontal delta
if (width < mScreenWidth) {
deltaX = (mScreenWidth - width) / 2 - rect.left;
} else if (rect.left > 0) {
deltaX = -rect.left;
} else if (rect.right < mScreenWidth) {
deltaX = mScreenWidth - rect.right;
}
mTransformationMatrix.postTranslate(deltaX, deltaY)
}
So I have an ImageView using a Matrix to scale the Bitmap I'm displaying. I can double-tap to zoom to full-size, and my ScaleAnimation handles animating the zoom-in, it all works fine.
Now I want to double-tap again to zoom out, but when I animate this with ScaleAnimation, the ImageView does not draw the newly exposed areas of the image (as the current viewport shrinks), instead you see the portion of visible image shrinking in. I have tried using ViewGroup.setClipChildren(false), but this only leaves the last-drawn artifacts from the previous frame - leading to an trippy telescoping effect, but not quite what I was after.
I know there are many zoom-related questions, but none cover my situation - specifically animating the zoom-out operation. I do have the mechanics working - ie aside from the zoom-out animation, double-tapping to zoom in and out works fine.
Any suggestions?
In the end I decided to stop using the Animation classes offered by Android, because the ScaleAnimation applies a scale to the ImageView as a whole which then combines with the scale of the ImageView's image Matrix, making it complicated to work with (aside from the clipping issues I was having).
Since all I really need is to animate the changes made to the ImageView's Matrix, I implemented the OnDoubleTapListener (at the end of this post - I leave it as an "exercise to the reader" to add the missing fields and methods - I use a few PointF and Matrix fields to avoid excess garbage creation). Basically the animation itself is implemented by using View.post to keep posting a Runnable that incrementally changes the ImageView's image Matrix:
public boolean onDoubleTap(MotionEvent e) {
final float x = e.getX();
final float y = e.getY();
matrix.reset();
matrix.set(imageView.getImageMatrix());
matrix.getValues(matrixValues);
matrix.invert(inverseMatrix);
doubleTapImagePoint[0] = x;
doubleTapImagePoint[1] = y;
inverseMatrix.mapPoints(doubleTapImagePoint);
final float scale = matrixValues[Matrix.MSCALE_X];
final float targetScale = scale < 1.0f ? 1.0f : calculateFitToScreenScale();
final float finalX;
final float finalY;
// assumption: if targetScale is less than 1, we're zooming out to fit the screen
if (targetScale < 1.0f) {
// scaling the image to fit the screen, we want the resulting image to be centred. We need to take
// into account the shift that is applied to zoom on the tapped point, easiest way is to reuse
// the transformation matrix.
RectF imageBounds = new RectF(imageView.getDrawable().getBounds());
// set up matrix for target
matrix.reset();
matrix.postTranslate(-doubleTapImagePoint[0], -doubleTapImagePoint[1]);
matrix.postScale(targetScale, targetScale);
matrix.mapRect(imageBounds);
finalX = ((imageView.getWidth() - imageBounds.width()) / 2.0f) - imageBounds.left;
finalY = ((imageView.getHeight() - imageBounds.height()) / 2.0f) - imageBounds.top;
}
// else zoom around the double-tap point
else {
finalX = x;
finalY = y;
}
final Interpolator interpolator = new AccelerateDecelerateInterpolator();
final long startTime = System.currentTimeMillis();
final long duration = 800;
imageView.post(new Runnable() {
#Override
public void run() {
float t = (float) (System.currentTimeMillis() - startTime) / duration;
t = t > 1.0f ? 1.0f : t;
float interpolatedRatio = interpolator.getInterpolation(t);
float tempScale = scale + interpolatedRatio * (targetScale - scale);
float tempX = x + interpolatedRatio * (finalX - x);
float tempY = y + interpolatedRatio * (finalY - y);
matrix.reset();
// translate initialPoint to 0,0 before applying zoom
matrix.postTranslate(-doubleTapImagePoint[0], -doubleTapImagePoint[1]);
// zoom
matrix.postScale(tempScale, tempScale);
// translate back to equivalent point
matrix.postTranslate(tempX, tempY);
imageView.setImageMatrix(matrix);
if (t < 1f) {
imageView.post(this);
}
}
});
return false;
}
I'm working on a game which will use projectiles. So I've made a Projectile class and a new instance is created when the user touches the screen:
#Override
public boolean onTouch(View v, MotionEvent e){
float touch_x = e.getX();
float touch_y = e.getY();
new Projectile(touch_x, touch_y);
}
And the Projectile class:
public class Projectile{
float target_x;
float target_y;
Path line;
public Projectile(float x, float y){
target_x = x;
target_y = y;
line = new Path();
line.moveTo(MyGame.mPlayerXPos, MyGame.mPlayerYPos);
line.lineTo(target_x, target_y);
}
}
So this makes a Path with 2 points, the player's position and and touch coords. My question is - How can you access points on this line? For example, if I wanted to get the x,y coords of the Projectile at the half point of the line, or the point the Projectile would be at after 100 ticks (moving at a speed of X pixels/tick)?
I also need the Projectile to continue moving after it reaches the final point.. do I need to use line.addPath(line) to keep extending the Path?
EDIT
I managed to get the Projectiles moving in a straight line, but they're going in strange directions. I had to fudge some code up:
private void moveProjectiles(){
ListIterator<Projectile> it = Registry.proj.listIterator();
while ( it.hasNext() ){
Projectile p = it.next();
p.TimeAlive++;
double dist = p.TimeAlive * p.Speed;
float dx = (float) (Math.cos(p.Angle) * dist);
float dy = (float) (Math.sin(p.Angle) * dist);
p.xPos += dx;
p.yPos += -dy;
}
}
The Angle must be the problem.. I'm using this method, which works perfectly:
private double getDegreesFromTouchEvent(float x, float y){
double delta_x = x - mCanvasWidth/2;
double delta_y = mCanvasHeight/2 - y;
double radians = Math.atan2(delta_y, delta_x);
return Math.toDegrees(radians);
}
However, it returns 0-180 for touches above the center of the screen, and 0 to -180 for touches below. Is this a problem?
The best way to model this is with parametric equations. No need to use trig functions.
class Path {
private final float x1,y1,x2,y2,distance;
public Path( float x1, float y1, float x2, float y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.distance = Math.sqrt( (x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
}
public Point position( float t) {
return new Point( (1-t)*x1 + t*x2,
(1-t)*y1 + t*y2);
}
public Point position( float ticks, float speed) {
float t = ticks * speed / distance;
return position( t);
}
}
Path p = new Path(...);
// get halfway point
p.position( 0.5);
// get position after 100 ticks at 1.5 pixels per tick
p.position( 100, 1.5);
From geometry, if it's a straight line you can calculate any point on it by using polar coordinates.
If you find the angle of the line:
ang = arctan((target_y - player_y) / (target_x - player_x))
Then any point on the line can be found using trig:
x = cos(ang) * dist_along_line
y = sin(ang) * dist_along_line
If you wanted the midpoint, then you just take dist_along_line to be half the length of the line:
dist_along_line = line_length / 2 = (sqrt((target_y - player_y)^2 + (target_x - player_x)^2)) / 2
If you wanted to consider the point after 100 ticks, moving at a speed of X pixels / tick:
dist_along_line = 100 * X
Hopefully someone can comment on a way to do this more directly using the android libs.
First of all, the Path class is to be used for drawing, not for calculation of the projectile location.
So your Projectile class could have the following attributes:
float positionX;
float positionY;
float velocityX;
float velocityY;
The velocity is calculated from the targetX, targetY, playerX and playerY like so:
float distance = sqrt(pow(targetX - playerX, 2)+pow(targetY - playerY, 2))
velocityX = (targetX - playerX) * speed / distance;
velocityY = (targetY - playerY) * speed / distance;
Your position after 20 ticks is
x = positionX + 20 * velocityX;
y = positionY + 20 * velocityY;
The time it takes to reach terget is
ticksToTarget = distance / velocity;
Location of halp way point is
halfWayX = positionX + velocityX * (tickToTarget / 2);
halfWayY = positionY + velocityY * (tickToTarget / 2);