1

I am making a clone of Flappy Bird. I was doing just fine performance-wise: 60 fps. This was when it had 1 pillar/obstacle only. As soon as I added 3 of them my fps dropped to 30 and below. Then game is unplayable now. I get that this has something to do with doing repaint() all the time.

Here is the code:

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.ArrayList;

/**
 * Created by Lazar on 25/05/15.
 */
public class Environment extends JComponent implements ActionListener {

    public static final Dimension dimension = new Dimension(800,600);
    BufferedImage img;
    BufferedImage ptica1;
    BufferedImage ptica2;
    double skokbrojac = 0; 
    int brzina = 4; // speed // MUST Background % brzina = 0
    int dx;
    int dx2;
    int pad = 0; //drop
    Timer timer;
    boolean parno;
    boolean skok = false;

    //Stubovi // Pillars
    Stub stub1 = new Stub();
    Stub stub2 = new Stub();
    Stub stub3 = new Stub();
    ArrayList<Stub>stubovi = new ArrayList<Stub>();
    int razmakStub; // Space between pillars

    public Environment() {
        setPreferredSize(dimension);
        img = Util.openImage("pozadina.png");
        ptica1 = Util.openImage("ptica1.png");
        ptica2 = Util.openImage("ptica2.png");

        stubovi.add(stub1);
        stubovi.add(stub2);
        stubovi.add(stub3);

        dx = img.getWidth()/2;
        timer = new Timer(1000/60,this); 
        timer.start();

        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                super.mousePressed(e);
                skok = true;  // start jump
                skokbrojac = 0; //jump frame counter
            }
        });

    }

    protected void paintComponent(Graphics g){
        Graphics2D g2d = (Graphics2D)g;
        //g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        //g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        if(dx == img.getWidth()){ //image horizontal scroll
            dx2 = 0;
        }
        if(dx2 == img.getWidth()/2){ //image horizontal scroll
            dx = dimension.width;
        }
        g2d.drawImage(img,getWidth() - dx, 0, null); //draw background
        if(dx >= img.getWidth()){
            g2d.drawImage(img,getWidth() - dx2, 0, null);
        }
        if(parno){
            g2d.drawImage(ptica1,dimension.width/2, 290 + pad, null); //draw bird
        }
        else{
            g2d.drawImage(ptica2,dimension.width/2, 290 + pad, null); //draw bird
        }
        stub1.postoji = true; //pillar1 exists?
        if(razmakStub > 240){
            stub2.postoji = true;
        }
        if(razmakStub > 480){ //pillar1 exists?
            stub3.postoji = true;
        }
        for(Stub i : stubovi){ //draw pillars if they exist
            if(i.postoji)
                i.crtaj(g2d);
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        dx = dx + brzina; 
        dx2 = dx2 + brzina;

        if(skokbrojac > 5) // jump frame lenght
            skok = false;
        if(skok){
            pad -= 15; // jump height
        }
        else{  
            pad += 8; //rate of the fall
        }
        skokbrojac++;
        parno ^= true; // for different bird images
        if(290 + pad >= 536 || 290 + pad<= 3) //border hit detect
            timer.stop();
        razmakStub += brzina;
        for(Stub i : stubovi){ //reset pillars and make them move
            if(i.postoji){
                if(i.getDx() < -50){
                    i.setDx(800);
                    i.randomDy();
                }
                i.setDx(i.getDx() - brzina);
            }
        }   
        repaint();
    }
}

Complete project source

Also bear in mind this is really unpolished version so the code is ugly. I am looking for a solution to boost performance.

Main Class:

import javax.swing.*;

/**
 * Created by Lazar on 25/05/15.
 */
public class Main {

    public static void main(String[] args){

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Frame(new Environment());
            }
        });
    }
}

Frame class:

import javax.swing.*;

/**
 * Created by Lazar on 25/05/15.
 */
public class Frame extends JFrame{

    public Frame(JComponent content){
        setContentPane(content);
        setTitle("Flappy");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(getPreferredSize());
        setResizable(false);
        setVisible(true);
        setLocationRelativeTo(null);
    }
}

Stub/Pillar class:

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

/**
 * Created by Lazar on 26/05/15.
 */
public class Stub {

    BufferedImage dole;
    BufferedImage gore;
    Random r = new Random();
    int dx = 700;
    int dy = r.nextInt(250) + 250;
    boolean postoji = false;

    public void crtaj(Graphics2D g2d){
        dole = Util.openImage("stub_dole.png");
        gore = Util.openImage("stub_gore.png");
        g2d.drawImage(dole, dx, dy, null);
        g2d.drawImage(gore, dx, -(560-dy), null);
    }

    public void setDx(int dx) {
        this.dx = dx;
    }

    public void randomDy(){
        this.dy = r.nextInt(250) + 250;
    }

    public int getDx() {
        return dx;
    }
}

Ptica/Brid class:

import java.awt.Graphics;
import java.awt.image.BufferedImage;



/**
 * Created by Lazar on 26/05/15.
 */

public class Ptica {

    BufferedImage ptica1;
    BufferedImage ptica2;
    boolean ptica;
    boolean skok = false;
    int pad = 0;
    double skokBrojac = 0;

    public Ptica(){
        ptica1 = Util.openImage("/slike/ptica1.png");
        ptica2 = Util.openImage("/slike/ptica2.png");
    }

    public void crtajPticu(Graphics g2d){

        ptica ^= true;

        if(ptica){
            g2d.drawImage(ptica1, Environment.dimension.width/2, Environment.dimension.height/2-110 + pad, null);
        }
        else{
            g2d.drawImage(ptica2, Environment.dimension.width/2, Environment.dimension.height/2-110 + pad, null);
        }

        System.out.println(pad);
    }


    public void setSkok(boolean skok) {
        this.skok = skok;
    }

    public void setSkokBrojac(double skokBrojac) {
        this.skokBrojac = skokBrojac;
    }

    public double getSkokBrojac() {
        return skokBrojac;
    }

    public boolean isSkok() {
        return skok;
    }

    public void setPad(int pad) {
        this.pad = pad;
    }

    public int getPad() {
        return pad;
    }


}

Util class:

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;

/**
 * Created by Lazar on 25/05/15.
 */
public class Util {

    public static BufferedImage openImage(String name){
        try {
            if(!name.startsWith("/slike/")){
                name="/slike/"+name;
            }
            return ImageIO.read(Util.class.getResource(name));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}
Alex Salauyou
  • 14,185
  • 5
  • 45
  • 67
Pendula
  • 688
  • 6
  • 17

3 Answers3

2
  • Avoid adding all you classes to the default package, this could cause issues with class loading on some versions of Java
  • Painting should paint the state and should not be making decisions or changing the state
  • Don't, repeatedly, load resources

For example, from you Stub class, which Environment's paintComponent calls crtaj, you do the following...

public void crtaj(Graphics2D g2d){
    dole = Util.openImage("stub_dole.png");
    gore = Util.openImage("stub_gore.png");
    g2d.drawImage(dole, dx, dy, null);
    g2d.drawImage(gore, dx, -(560-dy), null);
}

Loading the images can take time. You should either have a "cache" class which managers them (loading them once) or load them when the Stub class is created (I'd prefer the cache class, as if you create and destroy many Stubs, loading the resources within the Stub class (constructor for example) could become a bottle neck

For example, which was able to go from 200-300 objects moving simultaneously, to over 4000 through the use of a re-usable object cache (rather the re-creating the objects and re-loading their resources)

Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • It was an easy fix. Loading images in the constructor of the Stub class alone did the trick. I will implement other solutions also :) Thanks man for your help! – Pendula May 27 '15 at 22:19
0

Use a profiler to determine where you code is actually spending time (Note that YourKit has a 15 day free trial license available).

Once you know what your bottleneck is then determine if there's an easy fix, if not consider better algorithms and data-structures to reduce the algorithmic complexity of your code.

Alex Fitzpatrick
  • 643
  • 5
  • 10
0

Profiling, as suggested by @alex-fitzpatrick, is always good. Also:

  • Is the type of images created by your Util.openImage call compliant with the graphics2D object you paint on? You may be spending some with the conversions (image types).
  • eliminate calls to getWidth() etc. You know these values after object initialization, cache them.
  • If possible, don't call repaint on the entire component. Use the overloaded version that specifies the area to repaint.
  • ... and consider using JavaFX for games :-)
Michael Bar-Sinai
  • 2,729
  • 20
  • 27
  • `getWidth` when you need, as size of component might change. When a component is initialised, the size is unknown until it's laid out, then best time to know that, is when the component is `paintComponent` – MadProgrammer May 27 '15 at 21:55
  • You can add a component listener that will update you only when changes happen: https://docs.oracle.com/javase/tutorial/uiswing/events/componentlistener.html – Michael Bar-Sinai May 28 '15 at 10:49
  • Yeah, but the cost of calling getWidth/Height as apposed to the event notification overheads would need to be considered, personally, I don't find getWidth/Height that big a drag – MadProgrammer May 28 '15 at 10:55
  • It's not, but it does hurt you when you need the performance most, and that was the issue raised in the question. – Michael Bar-Sinai May 28 '15 at 12:19