I'm building an isometric game with LibGDX. I'm trying to figure out pan limits, so the user can't pan off of the isometric map on the screen. The limits need to be dependent on zoom as well.
I found the following code for a orthogonal top-down game for pan limits:
float pxWidth =((TiledMapTileLayer) tiledMap.getLayers().get("Background")).getWidth() * ((TiledMapTileLayer) tiledMap.getLayers().get("Background")).getTileWidth();
float pxHeight = ((TiledMapTileLayer) tiledMap.getLayers().get("Background")).getHeight() * ((TiledMapTileLayer) tiledMap.getLayers().get("Background")).getTileHeight();
float scaledViewportWidthHalfExtent = cam.viewportWidth * cam.zoom * 0.5f;
float scaledViewportHeightHalfExtent = cam.viewportHeight * cam.zoom * 0.5f;
float xmax = pxWidth;
float ymax = pxHeight;
// Horizontal
if (cam.position.x < scaledViewportWidthHalfExtent)
cam.position.x = scaledViewportWidthHalfExtent;
else if (cam.position.x > xmax - scaledViewportWidthHalfExtent)
cam.position.x = xmax - scaledViewportWidthHalfExtent;
// Vertical
if (cam.position.y < scaledViewportHeightHalfExtent) {
cam.position.y = scaledViewportHeightHalfExtent;
}
else if (cam.position.y > ymax - scaledViewportHeightHalfExtent)
cam.position.y = ymax - scaledViewportHeightHalfExtent;
cam.update();
It works perfectly for the horizontal direction, but not for the vertical direction. The vertical direction seems to be offset by a half map size. Here's a gif that outlines the issue:
How do I convert the orthogonal vertical limit to an isometric vertical limit?
Also, it appears as though the y=0 position for the camera is in the center of the screen.
Other possibly relevant info:
Tiles are 512 x 256 pixels
Tiles rendered using IsometricTiledMapRenderer
ExtendViewport used with viewport set to screen pixels
OrthographicCamera used for panning, zooming, etc.
The problem with the y limits is that y=0 is the center of the screen. This means that the vertical map edges are not 0 and ymax, but they are -ymax / 2 and ymax / 2. So your final code would be this:
if (cam.position.y < -ymax + scaledViewportHeightHalfExtent) cam.position.y = -ymax + scaledViewportHeightHalfExtent;
else if (cam.position.y > ymax - scaledViewportHeightHalfExtent) cam.position.y = ymax - scaledViewportHeightHalfExtent;
Related
A Tiled map object hat a position x, y in pixels and a rotation in degrees.
I am loading the coordinates and the rotation from the map and trying to assign them to a box2d Body. There are a couple of differences between the location models, for example Tiled object rotation is in degrees and box2d body angle is in radians.
How do I convert the location to the BodyDef coordinates x, y and angle so that the body will be created at the correct position?
Background:
Using the code:
float angle = -rotation * MathUtils.degreesToRadians;
bodyDef.angle = angle;
bodyDef.position.set(x, y);
Works when the rotation is 0, but the body is not positioned correctly when rotation is different than 0.
I found a couple of hints here:
http://www.tutorialsface.com/2015/12/qu ... dx-solved/
and here:
https://github.com/libgdx/libgdx/issues/2742
That seem to tackle this exact problem, however neither solution worked for me, the body objects are still positioned wrong after applying those transformations. By positioned wrong I mean that the body is positioned in the area of the map where it should be but slightly off depending on its rotation.
I feel that it should be pretty simple but I do not know how to mediate the differences between Tiled and box2d locations.
For reference these are the two solutions I tried from the links above (after transforming the values x, y, width, height from pixels to world units):
float angle = rotation * MathUtils.degreesToRadians;
bodyDef.angle = -angle;
Vector2 correctionPosition = new Vector2(
height * MathUtils.cosDeg(-rotation - 90),
height + height * MathUtils.sinDeg(-rotation - 90));
bodyDef.position.set(x, y).add(correctionPosition);
and
float angle = rotation * MathUtils.degreesToRadians;
bodyDef.angle = -angle;
// Top left corner of object
Vector2 correctedPosition = new Vector2(x, y + height);
// half of diagonal for rectangular object
float radius = (float)Math.sqrt(width * width + height * height) / 2.0f;
// Angle at diagonal of rectangular object
float theta = (float)Math.tanh(height / width) * MathUtils.degreesToRadians;
// Finding new position if rotation was with respect to top-left corner of object.
// X=x+radius*cos(theta-angle)+(h/2)cos(90+angle)
// Y=y+radius*sin(theta-angle)-(h/2)sin(90+angle)
correctedPosition = correctedPosition
.add(
radius * MathUtils.cos(theta - angle),
radius * MathUtils.sin(theta - angle))
.add(
((height / 2) * MathUtils.cos(MathUtils.PI2 + angle)),
(-(height / 2) * MathUtils.sin(MathUtils.PI2 + angle)));
bodyDef.position.set(correctedPosition);
Any hint would be highly welcomed.
Found the correct solution, lost about 1 day of my life :)
The information from above links is incorrect and/or outdated. Currently Tiled saves the object position depending on it's type. For an image is relative to bottom-left position.
Box2d doesn't really have an "origin" point, but you can consider is its center and the shapes of the fixtures attached to the body should be positioned relative to (0,0).
Step 1: Read tiled properties
float rotation = textureMapObject.getRotation();
float x = textureMapObject.getX();
float y = textureMapObject.getY();
float width = textureMapObject.getProperties()
.get("width", Float.class).floatValue();
float height = textureMapObject.getProperties()
.get("height", Float.class).floatValue();
Step 2: Scale these acording to your box2d world size, for example x = x * 1/25; etc.
Step 3: Create a body without any position or angle.
Step 4: Transform body position and angle with:
private void applyTiledLocationToBody(Body body,
float x, float y,
float width, float height,
float rotation) {
// set body position taking into consideration the center position
body.setTransform(x + width / 2, y + height / 2, 0);
// bottom left position in local coordinates
Vector2 localPosition = new Vector2(-width / 2, -height / 2);
// save world position before rotation
Vector2 positionBefore = body.getWorldPoint(localPosition).cpy();
// calculate angle in radians
float angle = -rotation * MathUtils.degreesToRadians;
// set new angle
body.setTransform(body.getPosition(), angle);
// save world position after rotation
Vector2 positionAfter = body.getWorldPoint(localPosition).cpy();
// adjust position with the difference (before - after)
// so that the bottom left position remains unchanged
Vector2 newPosition = body.getPosition()
.add(positionBefore)
.sub(positionAfter);
body.setTransform(newPosition, angle);
}
Hope it helps.
I am trying to implement a simple "zoom" function in a map presentation type app. The user interacts with a NumericStepper to dial in a scale value and I then use that value to set the scaleX and scaleY properties of my map sprite. The parent of the map sprite has a scrollRect defined so the map is cropped as it scales. That all seems to work fine.
Naturally when I change the scale, the visible content shifts as the sprite becomes larger or smaller. I would like to keep the content in relatively the same screen location. I've taken a first pass at it below but it's not quite right.
Question: Am I on the right track here thinking that I can determine how much to shift the x/y by comparing the change in the width/height of the sprite after scaling? (as I write this I am thinking I can determine the center of the sprite before scaling, then reposition it so it stays centered over that point. Hmm. . .).
protected function scaleStepper_changeHandler(event:Event):void
{
var cX:Number = wrapper.x + (wrapper.width /2);
var cY:Number = wrapper.y + (wrapper.height /2);
wrapper.scaleX = scaleStepper.value;
wrapper.scaleY = scaleStepper.value;
wrapper.x = cX - (wrapper.width /2);
wrapper.y = cY - (wrapper.height /2);
}
You are on the right track, but for a better solution you should use a matrix to transform your Sprite. Use the following code below to achieve what you need:
private var originalMatrix:Matrix;
private function scaleAroundPoint(target:Sprite, scalePoint:Point, scaleFactor:Number):void
{
if(originalMatrix == null)
originalMatrix = target.transform.matrix;
var matrix:Matrix = originalMatrix.clone();
matrix.translate(-scalePoint.x, -scalePoint.y);
matrix.scale(scaleFactor, scaleFactor);
matrix.translate(scalePoint.x, scalePoint.y);
target.transform.matrix = matrix;
}
You can call this method like this:
scaleAroundPoint(wrapper, new Point(yourWidth/2, yourHeight/2), scaleStepper.value);
Hope this helps and solves your problem.
Am I on the right track here thinking that I can determine how much to shift the x/y by comparing the change in the width/height of the sprite after scaling?
Yes. As all values are known, you don't really have to "test" after scaling. You basically want to distribute the movement of the bounding box borders evenly.
Here's an example in one dimension, scaling factor 2, X is the registration point, | a boundary:
before scaling |--X--|
after scaling |----X----|
No problem there. Now what if the registration point is not in the middle?
before scaling |-X---|
after scaling |--X------|
As a last example, the edge case with the registration point on the boundary:
before scaling |X----|
after scaling |X--------|
Note how the boundaries of all 3 examples are equal before scaling and within each example, the registration point remains constant.
The problem is clearly identified. Now how to solve this?
We do know how much the width changes
before scaling width
after scaling width * scaleFactor
and from the first example we can determine where the left boundary should be after scaling (assuming that the registration point is at 0, so the object is centered):
before scaling -width * 0.5
after scaling -width * 0.5 * scaleFactor
This value depends on where the registration point of course is within the display object relative to the left boarder. To circumvent this dependency, subtract the values from each other to know how much the left boundary is moved to the left after scaling while keeping the object centered:
boundary shift width * 0.5 * (scaleFactor - 1)
Comparing before and after scaling, the left boundary should be further to the left by that amount and the right boundary should be further to the right by that amount.
The problem is that you cannot really set the left or right boundary directly.
You have to set the registration point, which will influence where the boundaries are. To know how far you should move the registration point, imagine both edge cases:
before scaling |X----|
after scaling |X--------|
corrected, |X--------|
before scaling |----X|
after scaling |--------X|
corrected, |--------X|
In both cases, the registration point has to be moved by the amount which the boundary should move, because essentially, the registration point is on the boundary and thus behaves the same way.
Any value in between can be found by linearly interpolating between both cases:
-[width * 0.5 * (scaleFactor - 1)] <= value <= +[width * 0.5 * (scaleFactor - 1)]
-[width * 0.5 * (scaleFactor - 1)] * (1-t) + [width * 0.5 * (scaleFactor - 1)] * t
To find the interpolation value t, which is 0 if X is on the left and 1 when on the right:
t = (X - L) / width
Add -[width * 0.5 * (scaleFactor - 1)] * (1-t) + [width * 0.5 * (scaleFactor - 1)] * t to the x position of the registration point and the scale the object.
Do the same for y in a similar fashion.
I am currently playing a bit around with a jave game engine. ( Developed by a Friend using LWJGL ). This Engine uses Vector3f Positions to draw a picture on the screen. I want to draw Pictures with an exact pixel position... so I created a Vector3f and, somehow, have to convert the x and y values to a pixel position now. But how?
Assuming the components of the Vector3f use OpenGL screen coordinates (origin in the center of the screen, x goes right, y goes up and the axes have values of -1 to 1). The resulting pixel coordinate uses LWJGL coordinate system, i.e. origin is in bottom left corner.
import org.lwjgl.opengl.Display;
import org.lwjgl.util.Point;
import org.lwjgl.util.vector.Vector3f;
public static Point toPixelPos(Vector3f v) {
int width = Display.getWidth();
int height = Display.getHeight();
int x = (int) (width * (v.x + 1) / 2);
int y = (int) (height * (v.y + 1) / 2);
return new Point(x, y);
}
I'm working on my own tile bliting engine, this one is using hexagonal tiles - but I think it doesn't differ much from regular tiles.
I have huge x,y array of tiles and they have their x,y coordinates for rendering on canvas, I iterate only the ones that should be visible on canvas in current camera position.
So I'm stuck with scaling and cant resolve this on my own.
Here is my code for drawing tiles on canvas:
public function draw():Void{
clearCanvas(); //Clear canvas (bitmapData)
var _m:Matrix;
iterateTiles(function(_tile:HexTile):Void{ // loop every tile that is visible on screen
_m = new Matrix();
_m.translate(_tile.x + cameraPoint.x,_tile.y + cameraPoint.y);//Get pre calculated tile x,y and add camera x,y
_m.scale(matrixScale, matrixScale);
drawToCanvas(_tile,_m);//Send to draw tile on canvas using Matrix
},true);
}
This works nice and fast but only problem is it scales tiles from left top corner (like regular scale would work)
Before scale
After scale
My question is how to transform tiles to always scale from center. So if tile 10:10 is in center of screen before scaling, then it should
stay there after scaling.
Sorry, I misunderstood the question, but I think I've got it now:
// Scale the distance from the original point to the center of the canvas
var xDistance:Number = ((_tile.x + cameraPoint.x) - xCenter) * matrixScale;
var yDistance:Number = ((_tile.y + cameraPoint.y) - yCenter) * matrixScale;
// Add the distances to the center of the canvas. This is where you want the tile
// to appear.
var x:Number = xCenter + xDistance;
var y:Number = yCenter + yDistance;
// Because the coordinate is going to be scaled, you need to increase it first.
x = (1 / matrixScale) * x;
y = (1 / matrixScale) * y;
_m.translate(x, y);
I have not tested this, I've just drawn it out on graph paper. Let me know if it works.
i have a darkBlueRect rectangle sprite and a copy of the same sprite with a larger scale and a lighter color - lightBlueRect.
i'm attempting to shift the location of the lightBlueRect according to the mouse.x location when the mouse is moving over the darkBlueRect. this way if the mouse is on the right of the darkBlueRect than the location of the scaled up lightBlueRect will be on the opposite side and shifted towards the left proportionate to the mouse position and scale. in addition, the lightBlueRect must appear "locked" to the darkBlueRect so lightBlueRect.x must never be more than darkBlueRect.x and lightBlueRect.x + lightBlueRect.width must never be less than darkBlueRect.x + darkBlueRect.width.
the image below depicts 3 states of what i'm attempting to accomplish:
State A: mouse.x is over darkBlueRect.x = 1 and both sprites are aligned to the left.
State B: mouse.x is in the middle of darkBlueRect and both sprites are aligned to the middle.
State C: mouse.x is on the last pixel of darkBlueRect and both sprites are aligned to the right.
for this example, the darkBlueRect.width is equal to 170 and the lightBlueRect.width is equal to 320, or a scale of 1.89.
each time the mouse changes it's x position over darkBlueRect the following is called. however, while my current code works for the most part, it's not exactly correct. when the mouse.x is over darkBlueRect.x = 1, as shown in State A, the lightBlueRect.x is not property aligned with darkBlueRect and appears less than darkBlueRect.x.
var scale:Number = 1.89;
lightBlueRect.x = darkBlueRect.x - Math.round((mouse.x * scale) / darkBlueRect.width * (lightBlueRect.width - darkBlueRect.width));
what equation can i use so that no matter the scale of the lightBlueRect it's first position (mouse over first pixel) and last position (mouse over last pixel) will result in the 2 sprites being aligned as well as property proportionate positioning in between?
[EDIT] the coordinates of the darkBlueRect is {0, 0}, so when the lightBlueRect moves towards the left it is moving into the negative. i could have simply written my code (what doesn't work) like this instead:
var scale:Number = 1.89;
lightBlueRect.x = 0 - Math.round((mouse.x * scale) / darkBlueRect.width * (lightBlueRect.width - darkBlueRect.width));
[EDIT 2]
when the display objects are small, the problem is difficult to notice. however, when they are large the problem becomes move obvious. the problem, here, being that the objects on the left side are misaligned.
also the problem is probably exasperated by the fact that both the lightBlueRect and darkBlueRect are scalable. darkBlueRect is scaled down and lightBlueRect is scaled up.
here is a link to the test displaying the problem. mousing over the shape quickly will obviously result in inaccurate alignment since it's based on frame rate speed, but this is not my concern. still, when you slowly mouse over the shape it will not align correctly on the left side when the mouse is over the first pixel of darkBlueRect: http://www.geoffreymattie.com/test/test.html
[SWF(width = "1000", height = "600", backgroundColor = "0xCCCCCC")]
import flash.display.Sprite;
import flash.events.MouseEvent;
var downScale:Number = 0.48;
var upScale:Number = 2.64;
var darkBlueRect:Sprite = createSprite();
darkBlueRect.scaleX = darkBlueRect.scaleY = downScale;
darkBlueRect.x = stage.stageWidth / 2 - darkBlueRect.width / 2;
darkBlueRect.y = stage.stageHeight / 2 - darkBlueRect.height / 2;
addChild(darkBlueRect);
var lightBlueRect:Sprite = createSprite();
lightBlueRect.scaleX = lightBlueRect.scaleY = upScale;
lightBlueRect.y = stage.stageHeight / 2 - lightBlueRect.height / 2;
lightBlueRect.x = stage.stageWidth;
lightBlueRect.mouseEnabled = false;
addChild(lightBlueRect);
darkBlueRect.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveEventHandler);
function mouseMoveEventHandler(evt:MouseEvent):void
{
lightBlueRect.x = darkBlueRect.x + Math.max(0.0, Math.min(darkBlueRect.mouseX / darkBlueRect.width * downScale, 1.0)) * (darkBlueRect.width - lightBlueRect.width);
}
function createSprite():Sprite
{
var result:Sprite = new Sprite();
result.graphics.beginFill(0x0000FF, 0.5);
result.graphics.drawRect(0, 0, 700, 200);
result.graphics.endFill();
return result;
}
i believe the problem is that the scaling of the shapes.
Assuming you have a Clamp function handy, and that width is floating-point so that division works as expected:
lBR.x = dBR.x + Clamp((mouse.x - dBR.x) / dBR.width, 0, 1) * (dBR.width - lBR.width);
(You can define Clamp(x, m, M) = min(max(x, m), M) if you don't have one.)