Friday, December 6, 2013

Realizing Animation without Flickering Using Java

Background

Years ago, during my studies, a friend of mine told me that he was programming a "Life" -simulation with "bacteria" moving in a window eating "algae" which "grow" as well in that window. The moving around consumes energy, while "eating the algae" gives back energy and if an "bacterium" has collected enough energy, it splits into two individuals.
I was very fascinated by his idea and wanted to create my own life simulation. I tried a couple of times with a couple of tools, but - being a unexperienced programmer at this time - never succeeded.
However, recently I had the wish to learn Java, as I collected some experience with C#, and wanted to switch to a more platform-independent programming language.

Output

Here is a little movie of the outcome of my attempts, the not-so fluent animation is a result of swapping due to low memory:



During development I experienced some challenges which I finally managed and which I want to share with you.

Environment

Hardware: MacBook5,1 with Intel Core 2 Duo processor and 2 GB RAM
Operating system: Mac OSX 10.7.5
IDE: Eclipse Kepler Service Release 1
UI Framework: WindowBuilder Pro (providing Swing & SWT)
JVM: 1.5 (Mac OX Default)

Challenges

Flickering

The first thing I stumbled over was the phenomenon of the "flickering" screen. This means that the animation does not run fluently, but creates occasional errors which switches the output window to white so the screen flickers. This phenomenon is a quite common one judged by the large number of discussion in the web. Finally I found a discussion which helped me. The solution of the flickering-problem involves creating a Buffer-Image which is posted to the screen instead of posting the graphics-object ("sprites") individually and a specific override-implementation of the "paint()" method.

Main Program

    public static void main(String[] args) {
        createAndShowGUI();
    }

Method createAndShowGUI()

    public static void createAndShowGUI() {
        JFrame mainFrame = new JFrame();
        mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        PixiesUI animationPanel = new PixiesUI();
        mainFrame.getContentPane().add(animationPanel);
        mainFrame.pack();
        mainFrame.setVisible(true);
        animationPanel.runAnimation();
    }

Method runAnimation() 

public void runAnimation() { [...]
  while (elapsedTime < animDuration) {
  //Create algae
  //Move Bacteriae
  try {
    Thread.sleep(30);
  }
  catch (Exception e) {
  }
  paint(this.getGraphics()); <-- This is important! pass the Graphics object of panel to paint()
  }
}

Override Method paint(Graphics g)

    @Override
    public void paint(Graphics g) {
 
      Dimension theDim = getSize();
      BufferedImage theBuffImg = new BufferedImage(5, 5, BufferedImage.TYPE_INT_RGB); <-- Bufferimage for fluent animation
      theBuffImg = (BufferedImage) createImage(theDim.width,theDim.height);
      Graphics2D theBuffImgGraphics = theBuffImg.createGraphics();
      theBuffImgGraphics.setPaint(Color.BLACK);
      Rectangle rect = new Rectangle(0, 0, theDim.width,theDim.height);
      theBuffImgGraphics.fill(rect);
      [... do more paint stuff here ...]
        g.drawImage(theBuffImg, 0, 0, this); <-- Finally paint image on JPanel
    }

 Object Housekeeping with ArrayLists

Another (common) problem is to savely add or remove objects from arrays holding all the objects in the application during loops. In essence there a two approaches, one involving the original list and a buffer list, another involving the use of an iterator.

Buffer List Approach

Here I created entries in a buffer list by loop in over the original list using an iterator. The removal of objects of the original list is driven by the buffer list (I call the "algae" "Pixie" ;)):
         ArrayList<CPixie> pixieList;
         ArrayList<CPixie> pixieBuffer;
         Iterator<CPixie> pixieIterator = pixieList.iterator();
         while (pixieIterator.hasNext()){
         CPixie thePixie = pixieIterator.next();
            if (thePixie.getEnergy() > 1500){
            CPixie newPixie = new CPixie(thePixie.getXPos() + 5, thePixie.getYPos() + 5);
            pixieBuffer.add(newPixie);
            thePixie.reduceEnergy(900);
            }
            }
 
            for (CPixie newPixie : pixieBuffer){
            addNewPixie(newPixie.getXPos(),newPixie.getYPos());
         }

Iterator Approach

ArrayList<CFood> foodItems; 
Iterator<CFood> foodIterator = foodItems.iterator();
while(foodIterator.hasNext()){
CFood foodItem = foodIterator.next();
if (foodItem.getEaten()) foodIterator.remove();
}

I hope that helps with you own project. To be complete I post the complete code of my project below.

All Code

Class PixieUI


import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class PixiesUI extends JPanel {
private static final long serialVersionUID = 1L;
//int ovalX = 50;
    long animDuration = 5000;
    long currentTime = System.nanoTime() / 1000000;
    long startTime = currentTime;
    long elapsedTime = currentTime - startTime;
    private CFoodList _foodItems;
    private CPixieList _pixieList;

    public PixiesUI() {
        setPreferredSize(new Dimension(500, 500));
        setDoubleBuffered(true);
    }

    public void runAnimation() {
    _foodItems = new CFoodList();
    _pixieList = new CPixieList();
    CPixieList _newPixieBuffer = new CPixieList();
    //Create the first Pixie
    Dimension theDim = getSize();
    _pixieList.addNewPixie(theDim.width / 2,theDim.height / 2);
        while (elapsedTime < animDuration) {
        //Remove all items from last pass
        _newPixieBuffer.pixieList.clear();
            elapsedTime = currentTime - startTime;
            //create new random food
    theDim = getSize();
            _foodItems.addNewFood(theDim.width,theDim.height);
            _foodItems.addNewFood(theDim.width,theDim.height);
            _foodItems.addNewFood(theDim.width,theDim.height);
           
            _pixieList.movePixies(theDim.width, theDim.height);
           
            try {
                Thread.sleep(30);
            }
            catch (Exception e) {
            }
           
            paint(this.getGraphics());
        }
    }

    @Override
    public void paint(Graphics g) {
   
    Dimension theDim = getSize();
    BufferedImage theBuffImg = new BufferedImage(5, 5, BufferedImage.TYPE_INT_RGB);
    theBuffImg = (BufferedImage) createImage(theDim.width,theDim.height);
    Graphics2D theBuffImgGraphics = theBuffImg.createGraphics();
    theBuffImgGraphics.setPaint(Color.BLACK);
    Rectangle rect = new Rectangle(0, 0, theDim.width,theDim.height);
    theBuffImgGraphics.fill(rect);
           
        for (CPixie thePixie : _pixieList.pixieList){
        for (CFood foodItem : _foodItems.foodItems){
        if (foodItem.getFoodSprite().contains(thePixie.getXPos(),thePixie.getYPos())){
        thePixie.feedPixie();
        _foodItems.foodItems.remove(foodItem);
        break;
        }
        }
       
        theBuffImgGraphics.setPaint(Color.RED);
        theBuffImgGraphics.fill(thePixie.getPixieSprite());
        }
       
        for (CFood theFoodItem : _foodItems.foodItems){
        theBuffImgGraphics.setPaint(Color.GREEN);
        theBuffImgGraphics.fill(theFoodItem.getFoodSprite());
        }
       
        theBuffImgGraphics.setPaint(Color.BLACK);
        rect = new Rectangle(8, 8, 50, 16);
        theBuffImgGraphics.fill(rect);
       
        theBuffImgGraphics.setPaint(Color.BLACK);
        rect = new Rectangle(60, 8, 50, 16);
        theBuffImgGraphics.fill(rect);
       
        theBuffImgGraphics.setPaint(Color.GREEN);
        int foodNumber = _foodItems.foodItems.size();
        String foodNumberStr = String.valueOf(foodNumber);
        theBuffImgGraphics.drawString(foodNumberStr,10,23);
       
        theBuffImgGraphics.setPaint(Color.RED);
        int pixieNumber = _pixieList.pixieList.size();
        String pixieNumberStr = String.valueOf(pixieNumber);
        theBuffImgGraphics.drawString(pixieNumberStr,62,23);
       
        g.drawImage(theBuffImg, 0, 0, this);
    }

    public static void main(String[] args) {
        createAndShowGUI();
    }

    public static void createAndShowGUI() {
        JFrame mainFrame = new JFrame();
        mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        PixiesUI animationPanel = new PixiesUI();
        mainFrame.getContentPane().add(animationPanel);
        mainFrame.pack();
        mainFrame.setVisible(true);
        //I made the call to runAnimation here now
        //after the containing frame is visible.
        animationPanel.runAnimation();
    }
}

Class CFood


import java.util.Random;
import java.awt.geom.*;

public class CFood {
protected int _xPos;
protected int _yPos;
protected int _radius = 7;
protected boolean _eaten = false;
protected Ellipse2D.Double _foodSprite;
//Constructor *******************************
public CFood(int winXSize, int winYSize)
{
//Set Position of food
setXPos(winXSize);
setYPos(winYSize);
setFoodSprite();
}
private void setFoodSprite(){
_foodSprite = new Ellipse2D.Double(getXPos(),getYPos(),getRadius(),getRadius());
}
public Ellipse2D.Double getFoodSprite(){
return _foodSprite;
}
private void setXPos(int winXSize)
Random seedGen = new Random();
Random theRandomGenerator = new Random(seedGen.nextInt(1234567));
_xPos = theRandomGenerator.nextInt( winXSize );
}
private void setYPos(int winYSize)
Random seedGen = new Random();
Random theRandomGenerator = new Random(seedGen.nextInt(1234567));
_yPos = theRandomGenerator.nextInt( winYSize );
}
public void setEaten(){
_eaten = true;
}
public boolean getEaten(){
return _eaten;
}
//*******************************************
//Position Getters **************************
public int getXPos()
{
return _xPos;
}
public int getYPos()
{
return _yPos;
}
public int getRadius()
{
return _radius;
}
//*******************************************
}

Class CFoodList


import java.util.ArrayList;
import java.util.Iterator;

public class CFoodList {

ArrayList<CFood> foodItems;
public CFoodList()
{
super();
foodItems = new ArrayList<CFood>();
}
public void addNewFood(int winXSize, int winYSize)
{
CFood foodItem = new CFood(winXSize, winYSize);
foodItems.add(foodItem);
}
public void removeEatenFoodItems(){
Iterator<CFood> foodIterator = foodItems.iterator();
while(foodIterator.hasNext()){
CFood foodItem = foodIterator.next();
if (foodItem.getEaten()) foodIterator.remove();
}
}
public void removeEatenFoodItem(CFood eatenFoodItem){
foodItems.remove(eatenFoodItem);
}
}

Class CPixie


import java.util.Random;
import java.lang.Math;
import java.awt.geom.*;


public class CPixie {
protected int _xPos;
protected int _yPos;
protected int _Energy;
protected int _radius = 7;
protected int _stepWidth;
protected Ellipse2D.Double _pixieSprite;

public CPixie(int xPos, int yPos) {
setXPos(xPos);
setYPos(yPos);
setEnergy(1000);
setPixieSprite();
Random ranGen = new Random();
_stepWidth = 1 + ranGen.nextInt(10);
}
private void setPixieSprite(){
_pixieSprite = new Ellipse2D.Double(getXPos(),getYPos(),getRadius(),getRadius());
}
public Ellipse2D.Double getPixieSprite(){
return _pixieSprite;
}
private void setXPos(int xPos){
_xPos = xPos;
}
private void setYPos(int yPos){
_yPos = yPos;
}
private void setEnergy(int theEnergy){
_Energy = theEnergy;
}
public int getEnergy(){
return _Energy;
}
public int getXPos(){
return _xPos;
}
public int getYPos(){
return _yPos;
}
public int getRadius(){
return _radius;
}
public void movePixie(int winWidth, int winHeight){
Random theRandomGen = new Random();
int xStep = theRandomGen.nextInt(_stepWidth) * (int) Math.pow(-1.0,(double)theRandomGen.nextInt(2));
if (!checkHorizInsidePanel(xStep,winWidth)) xStep = xStep * -1;
setXPos(getXPos() + xStep);
int yStep = theRandomGen.nextInt(_stepWidth) * (int) Math.pow(-1.0,(double)theRandomGen.nextInt(2));
if (!checkVerticalInsidePanel(yStep,winHeight)) yStep = yStep * -1;
setYPos(getYPos() + yStep);
setPixieSprite();
reduceEnergy();
}
public void feedPixie(){
setEnergy(getEnergy() + 450);
}
private boolean checkHorizInsidePanel(int xStep, int winWidth){
boolean insidePanel = false;
if (winWidth > getXPos() + xStep && getXPos() + xStep > 0) insidePanel = true;
return insidePanel;
}
private boolean checkVerticalInsidePanel(int yStep, int winHeight){
boolean insidePanel = false;
if (winHeight > getYPos() + yStep && getYPos() + yStep > 0) insidePanel = true;
return insidePanel;
}
public void reduceEnergy(int amount){
setEnergy(getEnergy() - amount);
}
private void reduceEnergy(){
setEnergy(getEnergy() - _stepWidth);
}
}

Class CPixieList


import java.util.ArrayList;
import java.util.Iterator;

public class CPixieList {
ArrayList<CPixie> pixieList;
ArrayList<CPixie> pixieBuffer;
public CPixieList()
{
super();
pixieList = new ArrayList<CPixie>();
pixieBuffer = new ArrayList<CPixie>();
}
public void movePixies(int winWidth, int winHeight){
pixieBuffer.clear();
        if (pixieList.size() > 0){
            for(CPixie thePixie : pixieList){
            thePixie.movePixie(winWidth, winHeight);
            }
        }
        
        //Clean Pixie list from dead pixies
        removeDeadPixies();
        
        //Create new Pixies if energy is high enough
        if (pixieList.size() > 0){
            //for(CPixie thePixie : _pixieList.pixieList){
        Iterator<CPixie> pixieIterator = pixieList.iterator();
        while (pixieIterator.hasNext()){
        CPixie thePixie = pixieIterator.next();
            if (thePixie.getEnergy() > 1500){
            CPixie newPixie = new CPixie(thePixie.getXPos() + 5, thePixie.getYPos() + 5);
            pixieBuffer.add(newPixie);
            thePixie.reduceEnergy(900);
            }
            }
            for (CPixie newPixie : pixieBuffer){
            addNewPixie(newPixie.getXPos(),newPixie.getYPos());
            }
        }
}
public void giveBirthToPixie(int xStartPos, int yStartPos, CPixie thePixie){
addNewPixie(xStartPos, yStartPos);
thePixie.reduceEnergy(900);
}
public void addNewPixie(int xStartPos, int yStartPos)
{
CPixie thePixie = new CPixie(xStartPos, yStartPos);
pixieList.add(thePixie);
}
public void removeDeadPixies(){
Iterator<CPixie> theIterator = pixieList.iterator();
ArrayList<CPixie> bufferList = new ArrayList<CPixie>();
while (theIterator.hasNext()){
CPixie thePixie = theIterator.next();
if (thePixie.getEnergy() <= 0){
theIterator.remove();
bufferList.add(thePixie);
}
}
for (CPixie deadPixie : bufferList){
pixieList.remove(deadPixie);
}
}
}