Is there some easy way to create a Button in LibGDX that contains several polygon drawables inside.
For example when the simple Button can be pictured like:
and advanced expected button like that:
And in my case following conditions must be met:
The Object must extends Button class.
Separated polygons (circle and triangle in pic) must have one common behavior - same up, down, hover styles, same click listeners ect. (for example when user hover circle the triangle and circle change color to green)
Actually it must have the absolutely same behavior as button with one polygon, just imagine that circle and triangle is one polygon, instead of separated ones.
The one way I figured out is to extend PolygonRegionDrawable, but to make it drawn correctly I need to override almost all methods from Drawable and TransformDrawable, is there any easier way to do that?
Maybe there can be found some DrawableGroup or something like that?
I dont think theres an easy way to make this work, here are some options.
Button is a Table, you can add stuff to it.
Button button = new Button(skin);
Image img = new Image(...); // insert polygon 1 here
img.setPostion(...); // offset so its in correct position in the button
button.addActor(img);
// add more stuff
Sadly this doesnt handle various state changes, like over etc. You would need to keep track of the added stuff and change them as button changes.
Other option is to make the polygons into single image.
Thats quite tricky, you would draw them into FrameBufferObject in correct places and make a textures out of that. Then use that texture for Drawable for the button style. Reapet for each state you want to handle. Packing them into and atlas would be optimal for perfomace reasons. A ton of texture switches is not great.
In order to reach such functionality I have implemented two additional classes that may assume a list of Drawables to draw it like only one.
So I just mention it here, I hope it may be useful for people who want to implement the same behavior.
An abstract class that assumes list of drawables and related coordinates in format:
[drawable1X, drawable1Y, drawable2X, drawable2Y, ..., drawableNX, drawableNY]
BulkBaseDrawable:
abstract class BulkBaseDrawable implements Drawable {
Array<Drawable> drawables;
private float leftWidth, rightWidth, topHeight, bottomHeight, minWidth, minHeight, leftX, bottomY, rightX, topY;
BulkBaseDrawable(Drawable[] drawables,
float... vertices) {
this.infos = new Array<>(drawables.length);
init(drawables, vertices);
}
#Override
public float getLeftWidth() {
return leftWidth;
}
#Override
public void setLeftWidth(float leftWidth) {
this.leftWidth = leftWidth;
}
#Override
public float getRightWidth() {
return rightWidth;
}
#Override
public void setRightWidth(float rightWidth) {
this.rightWidth = rightWidth;
}
#Override
public float getTopHeight() {
return topHeight;
}
#Override
public void setTopHeight(float topHeight) {
this.topHeight = topHeight;
}
#Override
public float getBottomHeight() {
return bottomHeight;
}
#Override
public void setBottomHeight(float bottomHeight) {
this.bottomHeight = bottomHeight;
}
#Override
public float getMinWidth() {
return minWidth;
}
#Override
public void setMinWidth(float minWidth) {
this.minWidth = minWidth;
}
#Override
public float getMinHeight() {
return minHeight;
}
#Override
public void setMinHeight(float minHeight) {
this.minHeight = minHeight;
}
void init(Drawable[] drawables, float[] vertices) {
initInfo(drawables, vertices);
initEdges();
initSize();
}
private void initInfo(Drawable[] drawables, float[] vertices) {
int i = 0;
for (Drawable drawable : drawables) {
infos.add(Info.builder()
.x(vertices[i])
.y(vertices[i + 1])
.width(drawable.getMinWidth())
.height(drawable.getMinHeight())
.drawable(drawable)
.build());
i += 2;
}
}
private void initSize() {
minHeight = topY - bottomY;
minWidth = rightX - leftX;
}
private void initEdges() {
topY = Float.MIN_VALUE;
rightX = Float.MIN_VALUE;
bottomY = Float.MAX_VALUE;
leftX = Float.MAX_VALUE;
int topI = 0;
int rightI = 0;
int bottomI = 0;
int leftI = 0;
for (int i = 0; i < infos.size; i++) {
Info info = infos.get(i);
if (info.y + info.height > topY) {
topY = info.y + info.height;
topI = i;
}
if (info.x + info.width > rightX) {
rightX = info.x + info.width;
rightI = i;
}
if (info.y < bottomY) {
bottomY = info.y;
bottomI = i;
}
if (info.x < leftX) {
leftX = info.x;
leftI = i;
}
}
Drawable top = infos.get(topI).drawable;
Drawable right = infos.get(rightI).drawable;
Drawable bottom = infos.get(bottomI).drawable;
Drawable left = infos.get(leftI).drawable;
leftWidth = left.getLeftWidth();
rightWidth = right.getRightWidth();
topHeight = top.getTopHeight();
bottomHeight = bottom.getBottomHeight();
}
static class Info {
float x, y, width, height;
Drawable drawable;
static InfoBuilder builder() {
return new InfoBuilder();
}
static class InfoBuilder {
float x, y, width, height;
Drawable drawable;
InfoBuilder x(float x) {
this.x = x;
return this;
}
InfoBuilder y(float y) {
this.y = y;
return this;
}
InfoBuilder width(float width) {
this.width = width;
return this;
}
InfoBuilder height(float height) {
this.height = height;
return this;
}
InfoBuilder drawable(Drawable drawable) {
this.drawable = drawable;
return this;
}
Info build() {
return new Info(x, y, width, height, drawable);
}
}
public Info(float x, float y, float width, float height, Drawable drawable) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.drawable = drawable;
}
}
}
And actually implementation BulkRegionDrawable:
public class BulkRegionDrawable extends BulkBaseDrawable {
public BulkRegionDrawable(Drawable[] drawables, float[] vertices) {
super(drawables, vertices);
}
#Override
public void draw(Batch batch,
float x,
float y,
float width,
float height) {
for (int i = 0; i < infos.size; i++) {
Info info = infos.get(i);
float yK = info.height / getMinHeight();
float xK = info.width / getMinWidth();
info.drawable.draw(
batch,
x + (info.x * width) / getMinWidth(),
y + (info.y * height) / getMinHeight(),
width * xK,
height * yK);
}
}
}
Related
I'm trying to implement undo/redo in JavaFX - I draw all my shapes using graphicsContext(). I have looked around and found that there's a save method on Graphics Context but it just saves attributes and not the actual shape/state of the canvas. What would be the best way of going about this?
This is one of my code snippets when I create a circle, for instance:
public CircleDraw(Canvas canvas, Scene scene, BorderPane borderPane) {
this.borderPane = borderPane;
this.scene = scene;
this.graphicsContext = canvas.getGraphicsContext2D();
ellipse = new Ellipse();
ellipse.setStrokeWidth(1.0);
ellipse.setFill(Color.TRANSPARENT);
ellipse.setStroke(Color.BLACK);
pressedDownMouse = event -> {
startingPosX = event.getX();
startingPosY = event.getY();
ellipse.setCenterX(startingPosX);
ellipse.setCenterY(startingPosY);
ellipse.setRadiusX(0);
ellipse.setRadiusY(0);
borderPane.getChildren().add(ellipse);
};
releasedMouse = event -> {
borderPane.getChildren().remove(ellipse);
double width = Math.abs(event.getX() - startingPosX);
double height = Math.abs(event.getY() - startingPosY);
graphicsContext.setStroke(Color.BLACK);
graphicsContext.strokeOval(Math.min(startingPosX, event.getX()), Math.min(startingPosY, event.getY()), width, height);
removeListeners();
};
draggedMouse = event -> {
ellipse.setCenterX((event.getX() + startingPosX) / 2);
ellipse.setCenterY((event.getY() + startingPosY) / 2);
ellipse.setRadiusX(Math.abs((event.getX() - startingPosX) / 2));
ellipse.setRadiusY(Math.abs((event.getY() - startingPosY) / 2));
};
}
The problem here is that there is that information like this is not saved in a Canvas. Furthermore there is no inverse operation that allows you to get back to the previous state for every draw information. Surely you could stroke the same oval, but with backgrund color, however the information from previous drawing information could have been overwritten, e.g. if you're drawing multiple intersecting ovals.
You could store the drawing operations using the command pattern however. This allows you to redraw everything.
public interface DrawOperation {
void draw(GraphicsContext gc);
}
public class DrawBoard {
private final List<DrawOperation> operations = new ArrayList<>();
private final GraphicsContext gc;
private int historyIndex = -1;
public DrawBoard(GraphicsContext gc) {
this.gc = gc;
}
public void redraw() {
Canvas c = gc.getCanvas();
gc.clearRect(0, 0, c.getWidth(), c.getHeight());
for (int i = 0; i <= historyIndex; i++) {
operations.get(i).draw(gc);
}
}
public void addDrawOperation(DrawOperation op) {
// clear history after current postion
operations.subList(historyIndex+1, operations.size()).clear();
// add new operation
operations.add(op);
historyIndex++;
op.draw(gc);
}
public void undo() {
if (historyIndex >= 0) {
historyIndex--;
redraw();
}
}
public void redo() {
if (historyIndex < operations.size()-1) {
historyIndex++;
operations.get(historyIndex).draw(gc);
}
}
}
class EllipseDrawOperation implements DrawOperation {
private final double minX;
private final double minY;
private final double width;
private final double height;
private final Paint stroke;
public EllipseDrawOperation(double minX, double minY, double width, double height, Paint stroke) {
this.minX = minX;
this.minY = minY;
this.width = width;
this.height = height;
this.stroke = stroke;
}
#Override
public void draw(GraphicsContext gc) {
gc.setStroke(stroke);
gc.strokeOval(minX, minY, width, height);
}
}
Pass a DrawBoard instance to your class instead of the Canvas and replace
graphicsContext.setStroke(Color.BLACK);
graphicsContext.strokeOval(Math.min(startingPosX, event.getX()), Math.min(startingPosY, event.getY()), width, height);
with
drawBoard.addDrawOperation(new EllipseDrawOperation(
Math.min(startingPosX, event.getX()),
Math.min(startingPosY, event.getY()),
width,
height,
Color.BLACK));
The undo and redo to move through the history.
I'm tryin to make a custom, clickable font to my game (for the menu, like: PLAY -> goes to playscreen), I've tryied with FreeType but when i make a new text with it, it uses a lot of memory on android. Can you suggest me a proper way to create a clickable texts from .ttf files? I did in this way:
I have a Box class:
public class Box {
protected float x;
protected float y;
protected float width;
protected float height;
public boolean contains(float x, float y) {
return x > this.x - width / 2 &&
x < this.x + width / 2 &&
y > this.y - height / 2 &&
y < this.y + height / 2;
}
}
Then the Texts class and its extends the box, so when i create a new Text, i can call .contains(x,y) so i can make the text clickable:
public class Texts extends Box{
private String text;
private int size;
private float density;
private int dpSize;
private BitmapFont font;
private FreeTypeFontGenerator generator;
private FreeTypeFontParameter parameter;
public Texts(String text, int size, float x ,float y){
this.text = text;
this.size = size;
generator = new FreeTypeFontGenerator(Gdx.files.internal("myfont.ttf"));
parameter = new FreeTypeFontParameter();
parameter.size = size;
dpSize = parameter.size;
font = generator.generateFont(parameter);
generator.dispose();
this.x = x;
this.y = y;
this.width = font.getBounds(text).width;
this.height = font.getBounds(text).height;
}
public void render(SpriteBatch sb){
font.draw(sb, text, x - width / 2 , y + height /2);
}
public void renderNoCenter(SpriteBatch sb){
font.draw(sb, text, x , y);
}
public float getWidth(){
return this.width;
}
public float getHeight(){
return this.height;
}
public void setText(String text){
this.text = text;
}
public void setXY(float x, float y){
this.x = x;
this.y = y;
}
public void update(float dt){
}
public int getDpSize(){
return dpSize;
}
public void dispose(){
font.dispose();
}
}
But when i create a new texts like this, the app consume + 12mb RAM / Texts:
Texts play = new Texts("PLAY", 200, 200, 80);
Texts options= new Texts("OPTIONS", 200, 200, 20);
So thats my, problem, thanks for the help!
Generating fonts at 200 pixels will fill up your video memory with rather big pages of bitmap font glyphs. If you use lots of different font types and/or scales (or just one big scaled font) you might want to look into implementing distance field fonts. Look at this answer: How to draw smooth text in libgdx?
Another option would be to create images and use Scene2d ImageButton in stead of clickable text.
I have made a class for the level generation and have got so far with it:
public class LevelGenerator {
private Sprite environment;
private float leftEdge, rightEdge, minGap, maxGap, y;
public Enemy enemy;
public LevelGenerator(Sprite environment, float leftEdge, float rightEdge,
float minGap, float maxGap) {
this.environment = environment;
this.leftEdge = leftEdge;
this.rightEdge = rightEdge;
this.minGap = minGap;
this.maxGap = maxGap;
}
public void generate(float topEdge){
if(y + MathUtils.random(minGap, maxGap) < topEdge)
return;
y = topEdge;
float x = MathUtils.random(leftEdge, rightEdge);
}
Basically, what I want to happen is for the enemy block to randomly generate on the sides of the screen. Here is the enemy block class (very simple):
public class Enemy extends Sprite{
public Enemy(Sprite sprite) {
super(sprite);
}
#Override
public void draw(Batch spriteBatch){
super.draw(spriteBatch);
}
}
This is what the game looks like at the moment when the block is just simply drawn on the game screen in a static position: http://i.imgur.com/SIt18Qn.png. What I am trying to achieve is for these "enemy" blocks to spawn randomly on either side of the screen but I can't seem to figure out a way to do it with the code I have so far.
Thank you!
I could not test but I think it will be fine, you have a rectangle if you want to see if it collides with another actor, if so updates its position in the update and draw method, and ramdon method start customizing to see if the coordinates, which colicionan be assigned to another actor rectagulo enemy or bye.
public class overFlowEnemy extends Sprite {
private final float maxH = Gdx.graphics.getHeight();
private final float maxW = Gdx.graphics.getWidth();
private Rectangle rectangle;
private Random random = new Random();
private float inttt = 0;
private float randomN = 0;
private boolean hasCollided = false;
public overFlowEnemy(Sprite sprite) {
super(sprite);
crearEnemigo();
rectangle = new Rectangle(getX(), getY(), getWidth(), getHeight());
}
#Override
public void draw(Batch spriteBatch) {
super.draw(spriteBatch);
}
private void crearEnemigo(){
setX(RandomNumber((int)maxW));
setY(RandomNumber((int)maxH));
}
private int RandomNumber(int pos) {
random.setSeed(System.nanoTime() * (long) inttt);
this.randomN = random.nextInt(pos);
inttt += randomN;
return (int)randomN;
}
public Rectangle getColliderActor(){
return this.rectangle;
}
}
the class as this should create a random enemy.
Edit: rereading your question, is that my English is not very good, and I think you wanted to be drawn only on the sides of the screen if so, tell me or adapts the class because when you create thought, which was across the screen.
I just added another class, if you can and want to work as you tell me which is correct, and delete the other.
public class overFlow extends Sprite {
private final float maxH = Gdx.graphics.getHeight();
private final float maxW = Gdx.graphics.getWidth();
private Rectangle rectangle;
private Random random = new Random();
private float inttt = 0;
private float randomN = 0;
private boolean hasCollided = false;
public overFlow(Sprite sprite) {
super(sprite);
crearEnemigo();
rectangle = new Rectangle(getX(), getY(), getWidth(), getHeight());
}
#Override
public void draw(Batch spriteBatch) {
super.draw(spriteBatch);
}
private void crearEnemigo(){
setX(RandomNumber((int)maxW, true));
setY(RandomNumber((int)maxH, false));
}
private int RandomNumber(int pos, boolean w) {
random.setSeed(System.nanoTime() * (long) inttt);
if (w = true){
this.randomN = random.nextInt((pos));
if(randomN % 2 == 0){
randomN = (pos - getWidth());
}else{
randomN = 0; //left screen
}
}else{
this.randomN = random.nextInt(pos - (int)getHeight());
}
inttt += randomN;
return (int)randomN;
}
public Rectangle getColliderActor(){
return this.rectangle;
}
}
Bah. I figured out the bug. Of course it was absurdly simple. The overridden update under MovingPanelItem needs to be written as follows:
#Override
public void update( int x, int y )
{
xCoord = xCoord + getxStep();
yCoord = yCoord + getyStep();
}
FULL DISCLOSURE: This is homework.
The PROBLEM:
Upon each Key listener event the screen should be updated and the moving objects should move. Currently, while the objects show up initially, if I press the prescribed key they disappear.
Also, all of the PanelItem's are stored in an ArrayList ( in another class ). All of the objects which are not subclasses of MovingPanelItem remain on the screen upon the KeyEvent.
I know I've left a lot to the imagination so if more detail is needed please let me know.
The superclass:
public class PanelItem
{
private Image img;
protected int xCoord;
protected int yCoord;
protected int width;
protected int height;
//constructor
public SceneItem( String path, int x, int y, int w int h )
{
xCoord = x;
yCoord = y;
setImage( path, w, h );
}
public void SetImage( String path, int w, int h )
{
width = w;
height = h;
img = ImageIO.read(new File(path));
}
//to be Overriden
public void update( int width, int height )
{
}
}
The Subclass:
public class MovingPanelItem extends PanelItem
{
private int xStep, yStep;
// x & y correspond to the coordinate plane
// w & h correspond to image width and height
// xs & ys correspond to unique randomized 'steps' in the for the x and y values
public MovingSceneItem(String path, int x, int y, int w, int h, int xs, int ys)
{
super( path, x, y, w, h);
setxStep(xs);
setyStep(ys);
update( x, y );
}
#Override
public void update( int x, int y )
{
this.xCoord = x + getxStep();
this.yCoord = y + getyStep();
}
}
(In response to Eels)
The panel/window itself is contained within the following class:
public class Panel extends JPanel {
protected ArrayList<PanelItem> panelItems;
private JLabel statusLabel;
private long randomSeed;
private Random random;
public Panel(JLabel sl) {
panelItems = new ArrayList<PanelItem>();
statusLabel = sl;
random = new Random();
randomSeed = 100;
}
private void addPanel() {
addPanelItems(10, "Mouse");
}
private void addPanelItems(int num, String type) {
Dimension dim = getSize();
dim.setSize(dim.getWidth() - 30, dim.getHeight() - 30);
synchronized (panelItems) {
for (int i = 0; i < num; i++) {
int x = random.nextInt(dim.width);
int y = random.nextInt(dim.height);
if (type.equals("Person")) {
int xs = random.nextInt(21) - 10;
int ys = random.nextInt(21) - 10;
panelItems.add(new Mouse(x, y, xs, ys));
}
}
repaint();
}
// causes every item to get updated.
public void updatePanel() {
Dimension dim = getSize();
synchronized (panelItems) {
for (PanelItem pi : panelItems) {
pi.update(dim.width, dim.height);
}
}
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
synchronized (panelItems) {
for (PanelItem pi : panelItems) {
pi.draw(g);
}
}
}
}
KeyListener class within the test class:
private class MyKeyListener extends KeyAdapter
{
#Override public void keyTyped(KeyEvent e)
{
if(e.getKeyChar() == 'f') {
panel.updatePanel();
panel.repaint();
}
// other key events include reseting the panel &
// creating a new panel, both of which are working.
}
}
Within the above code the only code I'm allowed to change ( & the only code I've written ) is the subclass MovingPanel item.
I have an image of a man that moves in x-axis. Now I want to move the man with its corresponding speed of 5 meter/s with delta time which is in nanoseconds and that is my problem. Could you give me some idea on how to do it?
Any help would be much appreciated...
Here's the code :
public class Board extends Canvas
{
public double meter;//PIXEL
private final java.util.List<Sprite> sprites = new ArrayList<Sprite>();
private final java.util.List<Sprite> z_sorted_sprites = new ArrayList<Sprite>();
private BufferStrategy strategy;
int x0_pixel;
int y0_pixel;
int x1_pixel;
int y1_pixel;
double x1_world;
double y1_world;
public Board(double meter)
{
this.setIgnoreRepaint(true);
this.meter = meter;
init();
addComponentListener(new ComponentAdapter()
{
#Override
public void componentResized(ComponentEvent e)
{
render();
}
});
}
public void init()
{
HumanBeing humanBeing = new HumanBeing(this, 2, 2, 0);
sprites.add(humanBeing);
z_sorted_sprites.add(humanBeing);
}
#Override
public void paint(Graphics g)
{
}
public void render()
{
setupStrategy();
x0_pixel = 0;
y0_pixel = 0;
x1_pixel = getWidth();
y1_pixel = getHeight();
x1_world = x1_pixel / meter;
y1_world = y1_pixel / meter;
Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
g2d.setBackground(Color.lightGray);
g2d.clearRect(0, 0, x1_pixel, y1_pixel);
g2d.setColor(Color.BLACK);
for (double x = 0; x < x1_world; x++)
{
for (double y = 0; y < y1_world; y++)
{
int xx = convertToPixelX(x);
int yy = convertToPixelY(y);
g2d.drawOval(xx, yy, 2, 2);
}
}
for (Sprite z_sorted_sprite : z_sorted_sprites)
{
z_sorted_sprite.render(g2d);
}
g2d.dispose();
strategy.show();
Toolkit.getDefaultToolkit().sync();
}
public int convertToPixelX(double distance)
{
return (int) (distance * meter);
}
public int convertToPixelY(double y_world)
{
return (int) (y1_pixel - (y_world * meter));
}
public void onZoomUpdated(int value)
{
meter = value;
render();
}
private void setupStrategy()
{
if (strategy == null)
{
this.createBufferStrategy(2);
strategy = this.getBufferStrategy();
}
}
public void start() throws InterruptedException
{
long previousTime = System.nanoTime();
while (true)
{
long now = System.nanoTime();
long dt = now - previousTime;
for (Sprite sprite : sprites)
{
sprite.move(0);
}
render();
Thread.sleep(1);
previousTime = now;
}
}
}
for Human Class
public class HumanBeing extends Sprite implements ImageObserver
{
private java.awt.Image humanImage;
private final Board board;
private double x;
private double y;
private int speed;
private java.util.List<Sprite> objects = new ArrayList<Sprite>();
private int cImage;
public HumanBeing(Board board, int x, int y, int speed)
{
this.board = board;
this.x = x;
this.y = y;
this.speed = speed;
URL iU = this.getClass().getResource("human.jpg");
ImageIcon icon = new ImageIcon(iU);
humanImage = icon.getImage();
objects.add(this);
}
public Image getImage()
{
return humanImage;
}
#Override
public void move(long ns)
{
x += 0.001;
}
#Override
public void render(Graphics2D g2d)
{
AffineTransform t = g2d.getTransform();
final double humanHeight = 1.6;// meter
final double humanWidth = 1.8; //meter
final double foot_position_x = humanHeight / 2;
final double foot_position_y = humanWidth;
int xx = board.convertToPixelX(x - foot_position_x); // to find the upper-left corner
int yy = board.convertToPixelY(y + foot_position_y); // to find the upper-left corner
g2d.translate(xx, yy);
// ratio for actual Image size
double x_expected_pixels = humanHeight * board.meter;
double y_expected_pixels = humanWidth * board.meter;
double w = ((ToolkitImage) humanImage).getWidth();
double h = ((ToolkitImage) humanImage).getHeight();
double x_s = x_expected_pixels / w;
double y_s = y_expected_pixels / h;
g2d.scale(x_s, y_s);
g2d.drawImage(getImage(), 0, 0, this); // upper left corner
g2d.setColor(Color.BLACK);
g2d.setTransform(t);
}
#Override
public void moveAt(double distance_x, double distance_y)
{
this.x = distance_x;
this.y = distance_y;
}
#Override
public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height)
{
return false;
}
}
Here's an idea for architecting your solution. I'm going to assume you have figured out how many pixels the image has to move every second to be the speed you want. Let's say that for your game or simulation, that means 10 pixels every second. You have a starting location and an ending location. So you know when you need to move the image. Use the class ScheduledThreadPoolExecutor and its method scheduleWithFixedRate to set up a periodic update of your image's position, issuing a call to draw the image once every second, at an updated location. Remember that your call to position your image could be delayed briefly as the Swing thread is servicing other GUI requests. But your scheduled thread is not affected. Say the scheduled thread says to Swing to put your image at position x and time 1.0 seconds. In fact, Swing gets to it a touch later at time 1.1 seconds. But you don't want that delta to screw up your timing. It doesn't because scheduleWithFixedRate will issue the next Swing call at the correct time 2.0 seconds, not at 2.1 seconds. The second image update is at exactly the right spot at the right time (or close enough depending on how busy the GUI thread is).