I am trying to set restrictions on the movement of a camera in JavaFX such that when it moves, it does not allow the user to move in such a way that only the content of the subscene is visible. Currently, my movement code looks as follows and has no checks to prevent this, I have tried limiting the movement of the camera by checking its coordinates and approximating if it will or will not show the content of the subscene, but that has problems in that it is purely an approximation and when zooming. TLDR, the problem involves 1 detecting when the camera moves away from the content of it, and 2 preventing a transformation from occurring if it will result in the camera moving away from the content.
mapView.addEventFilter(MouseEvent.MOUSE_PRESSED, e->{
startX = e.getX();
startY = e.getY();
});
mapView.addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
camera.setTranslateX(camera.getTranslateX() + (startX - e.getX()));
camera.setTranslateY(camera.getTranslateY() + (startY - e.getY()));
});
mapView is a MeshView if that is relevant.
If you would like me to clarify anything or need further information I will provide it. Thanks for the help and good day.
The camera has a viewport that you can imagine as a movable overlay above the contents (with some background being displayed in areas where no contents are placed). For the sake of simplicity, I would separate scrolling (i.e. moving the viewport) from content transformations (e.g. zooming).
Based on this mental model, you can define the scrollable bounds to be the bounds of your contents as well as a possibly empty portion of the current viewport (e.g. in case of contents smaller than viewport). The scrollable bounds needs to be recomputed after every scroll operation (increasing/reducing empty space within the current viewport) or content manipulation (transformations and bounds changes). If you restrict scrolling to the scrollable bounds, then you can ensure that empty space within the viewport is never increased by a scroll operation.
You can create an ObjectBinding scrollableBounds that is bound to the contents' bounds-in-local and local-to-parent-transform properties, as well the viewport-bounds. Then you can create a scollableBoundsProperty that is bound to the binding. That property can be accessed when scrolling to restrict the translation before applying it, thus preventing an increase of empty space within the viewport.
ObjectBinding<Bounds> scrollableBoundsBinding = new ObjectBinding<>() {
{
// TODO: bind to dependencies: viewport bounds and content bounds
// TODO: (transformed to the same coordinate system)
bind(camera.boundsInParentProperty(),
contentPane.boundsInLocalProperty(),
contentPane.localToParentTransformProperty());
}
#Override protected Bounds computeValue() {
// TODO: compute union of viewport and content bounds
return unionBounds(viewportBounds, contentBounds);
}
};
ObjectProperty<Bounds> scrollableBoundsProperty = new SimpleObjectProperty<>(
scrollableBoundsBinding);
// ...
// on mouse drag:
// dx, dy: relative mouse movement
// tx, ty: current scrolling
// mintx, maxtx, minty, maxty: translation range
// (taken from scrollable bounds and viewport size)
if (dx < 0) { tx = max(mintx, tx + dx); }
else { tx = min(maxtx, tx + dx); }
if (dy < 0) { ty = max(minty, ty + dy); }
else { ty = min(maxty, ty + dy); }
You might want to further restrict scrolling when the contents fully fit within the viewport, e.g. by placing the contents at the top left corner. You could also restrict the minimal zoom level in that case so that the contents are displayed as big as possible.
Note on usability: As already pointed out by another answer, you might want to consider allowing to drag over the contents by a bit, possibly with decreasing efficiency the further away one tries to scroll from the contents, comparable to the behavior of scrolling via touchpad in Safari. Then, when the interaction finishes, you could transition back instead of snapping in order to restrict the viewport to the contents again.
that's pretty common: just move and after you moved check if you're out of bounds... in that case go back into scene... this usually feels natural as when you try to pan an image on your phone.. it doesn't just block: it appears as it's making resistance and when you end your gesture it goes back... that's the simplest thing to do
Related
I've got a JPanel which renders an SVGDiagram, set as the viewport view of a JScrollPane. The scrollpane seems to work OK, and I can zoom in and out of the diagram, too; and given a MouseEvent, I can find nearby SVGElements using the "pick" method.
I'd like to be able to center the scrollpane on a given ShapeElement, but I'm having trouble. I'd thought that the center of the bounding box of the element would be the center of the shape in the diagram's coordinate system, but this appears to be wrong:
ShapeElement c;
Rectangle2D bounds = c.getBoundingBox();
Point center = new Point((int) bounds.getCenterX(), (int) bounds.getCenterY());
The returned center appears to (a) offset the Y-coordinate by more than the diagram's height (elements near the bottom of the diagram appear to have "centers" with small negative Y values, elements near the top of the diagram appear to have "centers" with large negative Y values), and (b) scale the X-coordinate (with no zoom, elements near the left edge of the diagram appear to have centers with X values near zero, while elements near the right edge of the diagram appear to have centers with X values roughly near 1/2 the diagram's width).
Obviously, when svgSalamander renders the Shapes to the screen, it's putting them in "the right place" -- but whatever transforms it uses are available only when it is rendering to the screen, which doesn't help me to find the coordinates of the center of an element when it's not on the screen.
Has anyone had any luck trying to center a given ShapeElement?
Once again, just taking the time to write the question pushed me in the direction of the answer.
The key is that "pick" also has to relate the diagram's coordinate frame to the coordinate frame of each Shape. Following the code, SVGRoot.pick creates an identity AffineTransform, then concatenates to it, in turn, the viewXForm of the SVGRoot, then the xform of each TransformableElement it encounters until it gets to a ShapeElement; there, ShapeElement.pick checks whether the given Rectangle intersects with its Shape as transformed by the concatenation of AffineTransforms.
So, to find the center of a given ShapeElement, one need only walk up its parents, collecting any TransformableElement.getXForm() results, append the viewXForm of the root element, create the concatenation of all those transforms from that of the root down, and apply that transform to the center of the target ShapeElement's bounding box:
synchronized Point2D getCenter(ShapeElement e) throws SVGException {
List<AffineTransform> ats = new ArrayList<>();
for (SVGElement el = e; el != null; el = el.getParent())
if (el instanceof TransformableElement) {
AffineTransform a = ((TransformableElement) el).getXForm();
if (a != null)
ats.add(a);
}
ats.add(diagram.getRoot().getViewXform());
AffineTransform xform = new AffineTransform();
for (AffineTransform a : <an Iterable that iterates over ats in reverse> )
xform.concatenate(a);
return xform.transform(
new Point2D.Double(
e.getBoundingBox().getCenterX(),
e.getBoundingBox().getCenterY()
), null);
}
I did have to add (a trivial) SVGRoot.getViewXform(), but that's all.
I've spent several frustrating hours trying to implement (what I thought would be) a simple FontActor class.
The idea is to just draw text at a specific position using a provided BitmapFont. That much, I've managed to accomplish. However, I'm struggling to compute my actor's width/height based on the rendered text.
(Using a FitViewport for testing)
open class FontActor<T : BitmapFont>(val font: T, var text: CharSequence = "") : GameActor() {
val layout = Pools.obtain(GlyphLayout::class.java)!!
companion object {
val identity4 = Matrix4().idt()
val distanceFieldShader: ShaderProgram = DistanceFieldFont.createDistanceFieldShader()
}
override fun draw(batch: Batch?, parentAlpha: Float) {
if (batch == null) return
batch.end()
// grab ui camera and backup current projection
val uiCamera = Game.context.inject<OrthographicCamera>()
val prevTransform = batch.transformMatrix
val prevProjection = batch.projectionMatrix
batch.transformMatrix = identity4
batch.projectionMatrix = uiCamera.combined
if (font is DistanceFieldFont) batch.shader = distanceFieldShader
// the actor has pos = x,y in local coords, but we need UI coords
// start by getting group -> stage coords (world)
val coords = Vector3(localToStageCoordinates(Vector2(0f, 0f)), 0f)
// world coordinate destination -> screen coords
stage.viewport.project(coords)
// screen coords -> font camera world coords
uiCamera.unproject(coords,
stage.viewport.screenX.toFloat(),
stage.viewport.screenY.toFloat(),
stage.viewport.screenWidth.toFloat(),
stage.viewport.screenHeight.toFloat())
// adjust position by cap height so that bottom left of text aligns with x, y
coords.y = uiCamera.viewportHeight - coords.y + font.capHeight
/// TODO: use BitmapFontCache to prevent this call on every frame and allow for offline bounds calculation
batch.begin()
layout.setText(font, text)
font.draw(batch, layout, coords.x, coords.y)
batch.end()
// viewport screen coordinates -> world coordinates
setSize((layout.width / stage.viewport.screenWidth) * stage.width,
(layout.height / stage.viewport.screenHeight) * stage.height)
// restore camera
if (font is DistanceFieldFont) batch.shader = null
batch.projectionMatrix = prevProjection
batch.transformMatrix = prevTransform
batch.begin()
}
}
And in my parent Screen class implementation, I rescale my fonts on every window resize so that they don't become "smooshed" or stretched:
override fun resize(width: Int, height: Int) {
stage.viewport.update(width, height)
context.inject<OrthographicCamera>().setToOrtho(false, width.toFloat(), height.toFloat())
// rescale fonts
scaleX = width.toFloat() / Config.screenWidth
scaleY = height.toFloat() / Config.screenHeight
val scale = minOf(scaleX, scaleY)
gdxArrayOf<BitmapFont>().apply {
Game.assets.getAll(BitmapFont::class.java, this)
forEach { it.data.setScale(scale) }
}
gdxArrayOf<DistanceFieldFont>().apply {
Game.assets.getAll(DistanceFieldFont::class.java, this)
forEach { it.data.setScale(scale) }
}
}
This works and looks great until you resize your window.
After a resize, the fonts look fine and automatically adjust with the relative size of the window, but the FontActor has the wrong size, because my call to setSize is wrong.
Initial window:
After making window horizontally larger:
For example, if I then scale my window horizontally (which has no effect on the world size, because I'm using a FitViewport), the font looks correct, just as intended. However, the layout.width value coming back from the draw() changes, even though the text size hasn't changed on-screen. After investigation, I realized this is due to my use of setScale, but simply dividing the width by the x-scaling factor doesn't correct the error. And again, if I remove my setScale calls, the numbers make sense, but the font is now squished!
Another strategy I tried was converting the width/height into screen coordinates, then using the relevant project/unproject methods to get the width and height in world coordinates. This suffers from the same issue shown in the images.
How can I fix my math?
Or, is there a smarter/easier way to implement all of this? (No, I don't want Label, I just want a text actor.)
One problem was my scaling code.
The fix was to change the camera update as follows:
context.inject<OrthographicCamera>().setToOrtho(false, stage.viewport.screenWidth.toFloat(), stage.viewport.screenHeight.toFloat())
Which causes my text camera to match the world viewport camera. I was using the entire screen for my calculations, hence the stretching.
My scaleX/Y calculations were wrong for the same reason. After correcting both of those miscalculations, I have a nicely scaling FontActor with correct bounds in world coordinates.
I'm trying to make a simple bit of code that will detect whether a model was clicked on. So far the best method I've seen is to create some sort of rectangle around the mesh and detect with Gdx.input.justTouched() to get the x,y coordinates, and then check if the rectangle contains the coordinates returned by justTouched().
I have no idea if there's a better way to do this, some kind of mesh onClick listener or something that LibGDX has in place that I'm unaware of (I've been scouring Google and the javadocs but I can't seem to find anything). I don't really need to deal with the z-axis coordinate, at least I don't think so. I only have the one PerspectiveCamera and it's not going to be moving around that much (not sure if this matters?)
Anyways, in my render() method I have:
if (Gdx.input.justTouched()) {
//this returns the correct values relative to the screen size
Vector2 pos = new Vector2(Gdx.input.getX(), Gdx.input.getY());
//I'm not sure how to get the correct rectangle to see what the
//width and height are for the model relative to the screen?
Rectangle modelBounds = new Rectangle(<<not sure what to put here>>);
if (modelBounds.contains(pos.x, pos.y) {
System.out.println("Model is being touched at: " + pos.x + ", " + pos.y);
}
}
I'm really not sure if this is the correct way to do this. I can get the position of the model with:
modelInstance.getNode("Node1").globalTransform.getTranslation(new Vector3());
but I'm not sure how to get the width and height as a rectangle relative to the screen size, if it's even possible.
I'm also unsure if this would cause massive lag, as I'm going to have about 7 nodes total that I need to detect if they're clicked on or not.
Is there a better way to do this? If not, is there a way to get the model width & height relative to the screensize (or camera, maybe)?
EDIT: Read about using Bounding Boxes, seems like what I need. Not quite sure how to implement it properly, however. I've changed my code to such:
public ModelInstance modelInstance;
public BoundingBox modelBounds;
#Override
public void create() {
...
//omitted irrelevant bits of code
modelInstance = new ModelInstance(heatExchangerModel);
modelBounds = modelInstance.calculateBoundingBox(new BoundingBox());
}
#Override
public void render() {
...
if (Gdx.input.justTouched()) {
Vector3 pos = new Vector3(Gdx.input.getX(), Gdx.input.getY(), 0);
System.out.println(pos);
if (modelBounds.contains(pos)) {
System.out.println("Touching the model");
}
}
}
I'm not really sure what the output of BoundingBox is supposed to be, or how the numbers it gives me correlates to the position in a 2d space. Hmm..
EDIT2: Think I'm getting closer.. Read about Rays and the .getPickRay method for my PerspectiveCamera. .getPickRay seems to return completely unusable numbers though, like really tiny numbers. I think I need to do something like:
if (Gdx.input.justTouched()) {
Vector3 intersection = new Vector3();
Ray pickRay = perspectiveCamera.getPickRay(Gdx.input.getX(), Gdx.input.getY());
Intersector.intersectRayBounds(pickRay, modelBounds, intersection);
}
and then intersection should give me the point where they overlap. It appears to be not working, however, giving me really small numbers like (4.8066642E-5, 2.9180354E-5, 1.0) .. hmmm..
Im trying to make a Vortex effect on a Circle Body that is a Sensor.
I've been looking for this and all examples i look for are in C++ or Objective C and i dont seem to translate them well.
when my objects collition, it calls beginContact(..) and it sets a flag so that i can call bodyToUpdate.applyForce(...);
public void beginContact(Contact contact) {
setColliding(true);
}
//updating collition every frame
public void act(){
if (colliding) {
ball.getBody().applyForce(....);
}
how to calculate the amount of force to apply every frame to make it a vortex?
Edit:
so i now have the object going straight to the center of the vortex, but no "spin"
public void act() {
if (colliding) {
ball.getBody().setLinearVelocity(0, 0);
ball.getBody().applyForce((portal.getBody().getPosition().x - ball.getBody().getPosition().x) * i,
(portal.getBody().getPosition().y - ball.getBody().getPosition().y) * i,
ball.getBody().getPosition().x, ball.getBody().getPosition().y, true);
i++;
} else
i = 10;
}
If by "spin" you mean that the falling object would move along a curve or a spiral, rather then changing the direction of movement immediately towards the black hole, there is an easy fix for that.
ball.getBody().setLinearVelocity(0, 0);
This completely stops the current movement of the body. I would start by removing that line. Also, for better realistic behaviour, you can follow the proper formula to compute attractive force, which goes something like this:
force = mass1 * mass2 * [some constant] / (distance ^ 2)
When you have the vector from your body towards the black hole (computed as black hole position - body position), the length of the vector is the distance, and after normalizing and multiplying by the force, you have the desired forceX and forceY force vector that needs to be applied to the body each update, as long as it stays in range of the hole.
However this formula will cause the force to grow to infinity as body moves closer to the hole, so you could try changing to linear conversion (closest = 1, farest = 0) if that causes any trouble.
force = mass1 * mass2 * [some constant] * ( (maxDistance - distance) / maxDistance )
You want to implement a tangential force with a magnitude that increases towards the center of the vortex.
Here's some pseudocode.
radialVector = objectPosition - vortexPosition;
tangentialVector = radialVector.perpendicularVector();
if (radialVector.length() < vortexRadius) {
// Swirl faster when near the center of the vortex.
// Max tangential force when distance from center is 0.
// Min tangential force when distance from center is vortexRadius.
forceMagnitude = map(radialVector.length(), vortexRadius, 0, minTangentialForce, maxTangentialForce);
force = forceMagnitude * tangentialVector.normalize();
object.applyForce(force);
}
Here's an image that shows the vector components:
To create a whirlpool effect there should be increasing radial (Fr) and tangential (Ft) forces as the object moves closer to the center.
I'm having a little problem with figuring something out (Obviously).
I'm creating a 2D Top-down mmorpg, and in this game I wish the player to move around a tiled map similar to the way the game Pokemon worked, if anyone has ever played it.
If you have not, picture this: I need to load various areas, constructing them from tiles which contain an image and a location (x, y) and objects (players, items) but the player can only see a portion of it at a time, namely a 20 by 15 tile-wide area, which can be 100s of tiles tall/wide. I want the "camera" to follow the player, keeping him in the center, unless the player reaches the edge of the loaded area.
I don't need code necessarily, just a design plan. I have no idea how to go about this kind of thing.
I was thinking of possibly splitting up the entire loaded area into 10x10 tile pieces, called "Blocks" and loading them, but I'm still not sure how to load pieces off screen and only show them when the player is in range.
The picture should describe it:
Any ideas?
My solution:
The way I solved this problem was through the wonderful world of JScrollPanes and JPanels.
I added a 3x3 block of JPanels inside of a JScrollPane, added a couple scrolling and "goto" methods for centering/moving the JScrollPane around, and voila, I had my camera.
While the answer I chose was a little more generic to people wanting to do 2d camera stuff, the way I did it actually helped me visualize what I was doing a little better since I actually had a physical "Camera" (JScrollPane) to move around my "World" (3x3 Grid of JPanels)
Just thought I would post this here in case anyone was googling for an answer and this came up. :)
For a 2D game, it's quite easy to figure out which tiles fall within a view rectangle, if the tiles are rectangular. Basically, picture a "viewport" rectangle inside the larger world rectangle. By dividing the view offsets by the tile sizes you can easily determine the starting tile, and then just render the tiles in that fit inside the view.
First off, you're working in three coordinate systems: view, world, and map. The view coordinates are essentially mouse offsets from the upper left corner of the view. World coordinates are pixels distances from the upper left corner of tile 0, 0. I'm assuming your world starts in the upper left corner. And map cooridnates are x, y indices into the map array.
You'll need to convert between these in order to do "fancy" things like scrolling, figuring out which tile is under the mouse, and drawing world objects at the correct coordinates in the view. So, you'll need some functions to convert between these systems:
// I haven't touched Java in years, but JavaScript should be easy enough to convey the point
var TileWidth = 40,
TileHeight = 40;
function View() {
this.viewOrigin = [0, 0]; // scroll offset
this.viewSize = [600, 400];
this.map = null;
this.worldSize = [0, 0];
}
View.prototype.viewToWorld = function(v, w) {
w[0] = v[0] + this.viewOrigin[0];
w[1] = v[1] + this.viewOrigin[1];
};
View.prototype.worldToMap = function(w, m) {
m[0] = Math.floor(w[0] / TileWidth);
m[1] = Math.floor(w[1] / TileHeight);
}
View.prototype.mapToWorld = function(m, w) {
w[0] = m[0] * TileWidth;
w[1] = m[1] * TileHeight;
};
View.prototype.worldToView = function(w, v) {
v[0] = w[0] - this.viewOrigin[0];
v[1] = w[1] - this.viewOrigin[1];
}
Armed with these functions we can now render the visible portion of the map...
View.prototype.draw = function() {
var mapStartPos = [0, 0],
worldStartPos = [0, 0],
viewStartPos = [0, 0];
mx, my, // map coordinates of current tile
vx, vy; // view coordinates of current tile
this.worldToMap(this.viewOrigin, mapStartPos); // which tile is closest to the view origin?
this.mapToWorld(mapStartPos, worldStartPos); // round world position to tile corner...
this.worldToView(worldStartPos, viewStartPos); // ... and then convert to view coordinates. this allows per-pixel scrolling
mx = mapStartPos[0];
my = mapStartPos[y];
for (vy = viewStartPos[1]; vy < this.viewSize[1]; vy += TileHeight) {
for (vx = viewStartPos[0]; vx < this.viewSize[0]; vy += TileWidth) {
var tile = this.map.get(mx++, my);
this.drawTile(tile, vx, vy);
}
mx = mapStartPos[0];
my++;
vy += TileHeight;
}
};
That should work. I didn't have time to put together a working demo webpage, but I hope you get the idea.
By changing viewOrigin you can scroll around. To get the world, and map coordinates under the mouse, use the viewToWorld and worldToMap functions.
If you're planning on an isometric view i.e. Diablo, then things get considerably trickier.
Good luck!
The way I would do such a thing is to keep a variable called cameraPosition or something. Then, in the draw method of all objects, use cameraPosition to offset the locations of everything.
For example: A rock is at [100,50], while the camera is at [75,75]. This means the rock should be drawn at [25,-25] (the result of [100,50] - [75,75]).
You might have to tweak this a bit to make it work (for example maybe you have to compensate for window size). Note that you should also do a bit of culling - if something wants to be drawn at [2460,-830], you probably don't want to bother drawing it.
One approach is along the lines of double buffering ( Java Double Buffering ) and blitting ( http://download.oracle.com/javase/tutorial/extra/fullscreen/doublebuf.html ). There is even a design pattern associated with it ( http://www.javalobby.org/forums/thread.jspa?threadID=16867&tstart=0 ).