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.
Related
In my game (created using LibGDX) I have a gameworld filled with a lot of circles changing their size continiously. Because there are so many circles I want to maximize their rendering-performance: I've heard of the ShapeRenderer, but it seems like that it is not the best in case of performance. The PixMap is also no solution because my circles should be vector-based.
Is their another faster solution too? And is the ShapeRenderer really that slow?
PS: I'm already using chunks to reduce the render time.
For the ShapeRenderer (circle in particular), if we look at the method, radius does not effect performance, segments is where the work is. And this is most likely what is hurting you, as you scale up in size, you increase the segments for detail.
I am not sure about there being opengl native vector graphics either... I think ultimately to reach the graphics card, you need to eventually become vertices and polygons (if you are filling). So actually, I think the Pixmap solution is the one you might be looking for. You compute the segments and the polygons to draw once (at the highest resolution you need).
With the Pixmap you should be able to do this in a way which is as performant as any other rendering of a Texture which you change sizes using the scaling variables (which should be as performant as not changing the scale). As you can see from the circle draw method that the ShapeRenderer uses, the circle is still really just describing a polygon (you are just computing its geometry every time).
If you want to give the Pixmap option a go, here is some code to get you bootstrapped.
Here is a kotlin function for building a PolygonSprite. You will have to do the maths for plotting the vertices of your circle, but you can probably use the circle draw method to get an idea for that. If you compute your geometry for a radius of 1, then you can just use your x/y scale to set the radius at whatever size you want.
fun polygonSprite(points: Array<Vector2>): PolygonSprite {
val pix = Pixmap(1, 1, Pixmap.Format.RGBA8888)
pix.setColor(0xFFFFFFFF.toInt())
pix.fill()
val textureSolid = Texture(pix)
val vertices = FloatArray(points.size * 2)
val triangleIndices = triangulator.computeTriangles(vertices)
for (i in 0..points.size - 1) {
val point = points.get(i)
val offset = i * 2
vertices[offset] = point.x
vertices[offset + 1] = point.y
}
val polyReg = PolygonRegion(TextureRegion(textureSolid),
vertices, triangleIndices.toArray())
val poly = PolygonSprite(polyReg)
return poly
}
And here is some rendering code. It takes into account relative positioning of the shape from the parent Body and some other stuff:
fun render(camera: OrthographicCamera) {
val parentRotation = (me().physicsRoot.rotationR() * MathUtils.radDeg)
val parentTransform = me().physicsRoot.transform
val myPosition = vec2(offsetX, offsetY)
parentTransform.mul(myPosition)
poly.color = color.get()
poly.setOrigin(0f, 0f)
poly.setPosition(myPosition.x, myPosition.y)
poly.rotation = parentRotation + rotationD
poly.setScale(scaleX, scaleY)
poly.draw(JJ.B.renderWorld.polyBatch)
recycle(myPosition)
}
Also, don't make a new one of these for every one, try and reuse them.
PS: Another option is to make a circle shader :D
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
I want to add text on the top of the enemies body.
Downscale issue starts here:
https://gamedev.stackexchange.com/questions/73688/why-is-my-text-is-too-large-even-when-scaled-to-05f-in-libgdx
The issue is that the text is very very big and i can't scale it smaller.
There is a solution for this, to add another camera / viewport.
This is OK - for HUD screen.
But i need the text on the enemy head.
From font class:
viewport = new FitViewport(Application.V_WIDTH * 4 , Application.V_HEIGHT * 4, new OrthographicCamera());
stage = new Stage(viewport, SpriteBatch);
stage.addActor(tableText);
Game main class:
viewport = new FitViewport(Application.V_WIDTH / Application.PPM, Application.V_HEIGHT / Application.PPM, orthographicCamera);
AND THE GAME. . .
What should i do? Do i need to find the right ratio betwin the game and the font class?
Danke
There is another way that works for me now:
bitmapFont.getData().setScale(0.008f);
bitmapFont.setUseIntegerPositions(false);
From libgdx API:
/** Specifies whether to use integer positions. Default is to use them so filtering doesn't kick in as badly. */
public void setUseIntegerPositions (boolean integer) {
this.integer = integer;
cache.setUseIntegerPositions(integer);
}
I'm testing my sprite that has the game title, and on my Motorola Moto G 2nd generation the dimensions of the sprite looks good but I'm testing also on my mothers phone, a Samsung GT-S5830i, and the height of the sprite looks stretched out.
I'm also trying to understand the concept of Viewport (I'm using the StretchViewport), but I don't know if I'm doing right. My game are designed for mobile, not desktop.
I did that to my SplashScreen:
this.gameTitle = new Sprite(new Texture(Gdx.files.internal("images/GameTitle.png")));
this.gameTitle.setSize(Configuration.DEVICE_WIDTH - 50, this.gameTitle.getHeight() * Configuration.DEVICE_HEIGHT / Configuration.DEVICE_WIDTH);
The DEVICE_HEIGTH and DEVICE_WIDTH are constants about the dimension of the screen. And the "-50" is a margin to the sprite
In my Viewport I used the real size of the screen for the dimensions, or should I use a virtual dimension? But how it works?
This is a part of my main class, what can I change?
// Create the Orthografic camera
this.orthoCamera = new OrthographicCamera(Configuration.DEVICE_WIDTH, Configuration.DEVICE_HEIGHT);
this.orthoCamera.setToOrtho(false, Configuration.VIRTUAL_GAME_WIDTH, Configuration.VIRTUAL_GAME_HEIGHT);
this.orthoCamera.position.set(this.orthoCamera.viewportWidth / 2f, this.orthoCamera.viewportHeight / 2f, 0);
this.orthoCamera.update();
// Combine SpriteBatch with the camera
this.spriteBatch.setTransformMatrix(this.orthoCamera.combined);
// Create the ViewPort
this.viewPort = new ExtendViewport(Configuration.DEVICE_WIDTH, Configuration.DEVICE_HEIGHT);
I updated my viewport to the ExtendViewport as you said.
Main class render method:
public void render() {
super.render();
// Update Orthographic camera
this.orthoCamera.update();
// Combine SpriteBatch with the camera
this.spriteBatch.setTransformMatrix(this.orthoCamera.combined);
}
Screen class render method:
#Override
public void render(float delta) {
// OpenGL clear screen
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(Gdx.gl.GL_COLOR_BUFFER_BIT | Gdx.gl.GL_DEPTH_BUFFER_BIT);
// SpriteBatch begins
this.game.spriteBatch.begin();
// Display the ClimbUp logo
this.gameTitle.draw(this.game.spriteBatch);
this.character.draw(this.game.spriteBatch);
// SpriteBatch ends
this.game.spriteBatch.end();
}
If you don't want stuff to look distorted on some devices and you don't want black bars (which none of your customers will like), you need to use an ExtendViewport instead of StretchViewport. And the dimensions you give it should be virtual dimensions based on whatever units you would like to work with.
For example, assuming a landscape orientation game, you could use 800 x 480 as virtual dimensions, and then you know that anything within that area (in world units) will be shown on the screen and you can design your game for that. On narrower devices (4:3 ratio) there will be more than 480 vertical units shown, and on wider devices (16:9 ratio) there will be more than 800 horizontal units shown.
There's one other option that avoids black bars and distortion, and that's FillViewport. But I think in general that's not a good option because you have no easy way to predict how much of your virtual dimensions are going to get cropped off.
Based on your edited question, here's what I would change in your code:
//No need to create your own camera. ExtendViewport creates its own.
// Pointless to call this now before resize method is called. Call this in render
//XXX this.spriteBatch.setTransformMatrix(this.orthoCamera.combined);
//This is where you give the viewport its minimum virtual dimensions
this.viewPort = new ExtendViewport(Configuration.VIRTUAL_GAME_WIDTH, Configuration.VIRTUAL_GAME_HEIGHT);
//Get reference to the viewport's camera for use with your sprite batch:
this.orthoCamera = (OrthographicCamera) this.viewport.getCamera();
Then in the resize method:
orthoCamera.setPosition(/*wherever you want it*/);
viewport.update(width, height, false); //these are actual device width and height that are provided by the resize method parameters.
You might want to position your camera in relation to the size calculated by the viewport. Then you should omit the setPosition line above, and instead calculate it after calling viewport.update. For example if you want 0,0 in the bottom left of the screen:
viewport.update(width, height, false);
orthoCamera.setPosition(orthoCamera.viewportWidth/2f, orthoCamera.viewportHeight/2f);
In your render method you can put this before spriteBatch.begin():
orthoCamera.update(); //good idea to call this right before applying to SpriteBatch, in case you've moved it.
spriteBatch.setProjectionMatrix(orthoCamera.combined);
public void render() {
this.spriteRenderer.setProjectionMatrix(this.instance.camera.combined);
this.spriteRenderer.begin();
if (!this.ballon.isPoping()) {
Sprite s = this.ballon.getSprite();
spriteRenderer.draw(s, 50, 50, s.getWidth(), s.getHeight());
}
// else {
// if (!this.ballon.isPoped()) {
// Sprite s = this.ballon.getCurrentAnimation();
// this.spriteRenderer.draw(s, this.ballon.getX(),
// this.ballon.getY(),
// this.ballon.getSprite().getWidth() * SpriteConfiguration.BALLON_SCALE,
// this.ballon.getSprite().getHeight() * SpriteConfiguration.BALLON_SCALE);
// this.ballon.processAnimation();
// }
// }
this.spriteRenderer.end();
}
I have set camera.setToOrtho to true with my width, and height. whenever I run that render method, my ballon sprite is basically vertically turned, looks like it flipped by 180 degrees over, like the image is saved as a standing ballon, and upon draw it looks like its turned, so I have to rotate the screen to see it.
The reason I used setProjectionMatrix is because the default location of the screen (0, 0) on android portrait mode I assume, is top-right corner, which on libGDX it is automtaically at bottom left corner, so basically setToOrtho fixes it for libGDX, but the phone's default location doesn't change, and I want it to change & be the same as libGDX's one because I need to handle screen touching, and get the correct position.
So basically setProjectionMatrix & setToOrtho(true...) does it's job, but the only problem that it just rotates the image by 180 degrees.
Does anyone know why it does that?