====== Java: Images, Animation, Sprites ======
----
===== Image buffering =====
==== drawImage() and Image Quirks ====
* [[cs498gd/java_user_interface_components_and_2d_graphics#drawing_images_external_images_eg_gif_png_jpg|drawImage()]] can be weird, in that it does not necessarily draw an Image immediately.
* Under certain circumstances, [[http://java.sun.com/javase/6/docs/api/java/awt/Graphics.html#drawImage%28java.awt.Image,%20int,%20int,%20int,%20int,%20int,%20int,%20int,%20int,%20java.awt.image.ImageObserver%29|drawImage() may return without drawing an image]].
* Loading an image to memory (RAM) and manipulating it there ("buffering" the image) makes this behavior of drawImage() less likely to occur.
* Therefore, we want to use buffering when doing animation and gaming.
==== BufferedImage and Image Processing ====
* Link [[http://web.archive.org/web/20120419214056/http://java.sun.com/developer/technicalArticles/GUI/java2d/java2dpart2.html|Java 2D documentation]]
* BufferedImage class - Provides methods for storing, interpreting, and rendering image pixel data
* Effective for large "unchanging" (static) images that take a long time to create: Create them once, then display them repeatedly from memory, instead of recomputing them each time.
* Example of loading a buffered image:
// Load an image using BufferedImage, with optimization
public BufferedImage loadImageBuffered (String imageName)
{
// Some preliminaries and setup
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsConfiguration gc = ge.getDefaultScreenDevice().getDefaultConfiguration();
try {
BufferedImage bi = ImageIO.read(getClass().getResource(imageName));
// imageName read into bi
int transparency = bi.getColorModel().getTransparency();
BufferedImage copy = gc.createCompatibleImage(bi.getWidth(), bi.getHeight(), transparency);
// buffered, optimized copy of bi made
Graphics2D g2d = copy.createGraphics();
g2d.drawImage(bi, 0, 0, null);
// bi drawn
g2d.dispose();
return copy;
}
catch (IOException e) {
System.err.println("Image loading error: " + e.toString());
return null;
}
}
* Buffering enables you to manipulate and display pixel-mapped images whose data is stored in memory.
* BufferedImage (input) => Buffered image operation => BufferedImage (output)
* Possible Graphics2D transformations on Images:
* rotate()
* scale()
* shear()
* Possible applications: sprites, side scrolling games, animations
* Small example (reads an external image file into a BufferedImage):
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/StaticFileAnimationBuffered.java|StaticFileAnimationBuffered.java]]
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/mario.png|mario.png]] (external image file)
* Uses threads for animation; see below.
----
===== Animation and Threads =====
==== The Game Loop ====
* The key to a game's operation
* Responsible for keeping track of the current frame and for requesting periodic screen updates
* The pseudocode / idea:
while (game is running) {
while (isPaused && isRunning) {
sleep(someInterval);
}
updateState();
processInputs();
updateAI();
updatePhysicsAndCollisions();
updateAnimations();
playSounds();
updateVideo();
drawGraphics();
...
...
...
}
* Inputs or none, the game loop must run.
* Some of the above has to run concurrently or performance will suffer.
----
==== Threads ====
* Link: http://java.sun.com/docs/books/tutorial/essential/concurrency/
* Animation creates some kind of motion on the screen by drawing successive frames at a relatively high speed.
* Threads - allow for multiple tasks to run concurrently in a program
* Think multitasking in an operating system (i.e., run more than one program at a time)
* Implement a thread containing the animation loop.
* MUST NOT implement animation loop in paintComponent()
* In a threaded panel (or threaded canvas (AWT))
=== Animation Framework - Simple ===
import java.awt.*;
import javax.swing.*;
// "Runnable" JPanel is a thread
public class AnimationFramework extends JPanel implements Runnable
{
// http://www.javablogging.com/what-is-serialversionuid/
// Certain IDEs will warn that you are missing SerialVersionUID
private static final long serialVersionUID = 1L;
private Thread animator;
public AnimationFramework()
{
}
// start(), stop(), run() are Thread methods
public void start()
{
animator = new Thread(this);
animator.start();
}
public void stop()
{
animator = null;
}
public void run()
{
long start = System.currentTimeMillis();
Thread current = Thread.currentThread();
while (current == animator) {
try {
Thread.sleep(0);
}
catch (InterruptedException e) {
System.err.println("An error occurred: " + e.toString());
e.printStackTrace();
}
// Draw something
repaint();
// Figure out how fast it is running
if (start + 1000 < System.currentTimeMillis()) {
start = System.currentTimeMillis();
}
}
}
public void paintComponent (Graphics g)
{
// Do your drawing stuff here
}
}
* In your frame class, you would instantiate this threaded panel, and then **start the animation by executing the panel's start() method.**
* Problems with the above framework:
* The framework runs at breakneck speed, as fast as possible, as evidenced by "Thread.sleep(0)".
* The call to repaint() happens with no pause.
* Maybe fine for animation, but for games, usually not a good thing.
* As an alternative, the animation could sleep for a fixed amount of time between frames (repaints).
* Again, fine for animation, but in games, you could end up waiting too long between frames.
* Demo: [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/moveLine2.java|Move a line]]
=== Handling Frames Per Second (FPS) ===
* A frame is one pass through the update-render-sleep loop inside the JPanel thread's run() method.
* ... in which the repaint() occurs.
* Theoretically, the loop's execution speed should be about the same on all platforms, except ...
* On a slower machine => too slow => unplayable game
* On a faster machine => too fast => unplayable game
* Need to **compute the estimated required delay between frames based on the current time.**
* Keep track of the starting time.
* There are many ways to optimize the FPS and thread sleeping times.
* The goal is to reduce "lag" as much as possible.
=== An Improved Animation Framework ===
import java.awt.*;
import javax.swing.*;
public class AnimationFramework extends JPanel implements Runnable
{
private static final long serialVersionUID = 1L;
private Thread animator;
// Going to adjust frame delay
private int fps, delay;
public AnimationFramework()
{
// 30 frames per second; considered sort of a minimum acceptable frame rate
fps = 30;
// 1000 milliseconds = 1 second
delay = 1000 / fps;
// for 30 fps, delay will be about 33.3 ms.
}
public AnimationFramework (int fps)
{
this.fps = fps;
delay = 1000 / this.fps;
}
public void start()
{
animator = new Thread(this);
animator.start();
}
public void stop()
{
animator = null;
}
public void run()
{
long start = System.currentTimeMillis();
Thread current = Thread.currentThread();
while (current == animator) {
try {
start += delay;
// Delay next frame depending on how far behind current time we are.
// For simple animations, incrementing start by the delay value
// will probably keep pace with the current time, but if we're doing
// a lot in this while loop, we'll eventually fall behind current time.
// So to keep up, sleep(0) will be used. Also, see the if statement below.
Thread.sleep(Math.max(0, start - System.currentTimeMillis()));
}
catch (InterruptedException e) {
System.err.println("An error occurred: " + e.toString());
e.printStackTrace();
}
// Draw something
repaint();
// Figure out how fast it is running;
// if start is more than 1000 ms behind current time, reset start
// to current time
if (start + 1000 < System.currentTimeMillis()) {
start = System.currentTimeMillis();
}
}
}
public void paintComponent (Graphics g)
{
// Do your drawing stuff in this routine
}
}
* Demo: [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/RectanglesMany.java|generating rectangles]]
==== A Little More on Timing ====
* Unlike many other applications, timing is critical for games:
* Updating the state of the game by the duration of the previous frame
* Locking the frame rate of a renderer
* Maintaining and evaluating proper transition of AI states
* Correctly identifying times to change frames in an animation (as we have done in the above)
* Testing for the proper amount of time to pass before getting additional input from a device
* To get the system-level timing in Java: long currentTime = System.currentTimeMillis()
* For our purposes, this is acceptable.
* With Java 1.5, a very high-resolution timer was introduced: long currentTime = System.nanoTime()
* nanoseconds
==== Java2D code samples ====
* Get the above Java2D code samples in one Eclipse project at https://cssegit.monmouth.edu:2443/jchung/Java2D.git.
----
===== Sprites =====
* Sprite - Any object that appears on the screen
* Overlayed on background surface
* Can be an image or drawing
* Can be motionless (dull)
* Can be animated: move around the screen randomly
* Even better: can respond to the environment
* Hopefully, your sprite images have transparent pixels
* We have already laid most of the groundwork for creating sprite.
* Keep track of the x and y coordinates
* Images should be BufferedImage
* Draw the sprite
* Update the sprite (e.g., coordinates, speed)
* Keep track of it's state (e.g., hidden/shown)
* Earlier example (reads an external image file into a [[cs498gd:java_images_animation_sprites#bufferedimage_and_image_processing|BufferedImage]]):
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/StaticFileAnimationBuffered.java|StaticFileAnimationBuffered.java]]
* Variation on above example, with image scaling and rotation:
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/StaticFileAnimationRotatedScaled3.java|StaticFileAnimationRotatedScaled3.java]]
* Example above with image scaling and rotation (AffineTransform rotation for image rotation):
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/StaticFileAnimationAffineTrans2.java|StaticFileAnimationAffineTrans2.java]]
----
==== GameSprite class framework examples ====
* Import all 3 example projects below into Eclipse.
* Import from Git, using the following gitlab URI: https://cssegit.monmouth.edu:2443/jchung/GameSpriteClassExamples.git
a) Multiple moving circles and squares using a ''GameSprite'' class:
* Notable features:
* Elastic collisions with window borders
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/gamesprite/GameSprite.java|GameSprite.java]] (**superclass**)
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/gamesprite/CircleSprite.java|CircleSprite.java]] (**subclass**)
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/gamesprite/SquareSprite.java|SquareSprite.java]] (**subclass**)
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/gamesprite/SpriteDemoPanel.java|SpriteDemoPanel.java]]
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/gamesprite/SpriteDemoFrame.java|SpriteDemoFrame.java]] (main program)
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/gamesprite/RandomCoordinates.java|RandomCoordinates.java]]
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/DrawnSpritesExample.zip|Zip file containing exported Eclipse project]]
* Download and import into Eclipse
b) A snowfall using a ''GameSprite'' class:
* Notable features:
* Scaling and rotation of an external snowflake image
* A background image
* Keyboard events to blow snowflakes horizontally
* screen edge warping (disappear out one side, come back from the other side)
----
===== Animated Sprites =====
* What if we have a strip image?
{{http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/media/bat.gif}} (bat.gif)
{{http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/media/explosion.gif}} (explosion.gif)
* What if we have a series of images?
* The idea: flipbook
* Updating and extending GameSprite
* The key:
public ArrayList loadStripImageArray(String imageName, int numImages)
{
ArrayList list = new ArrayList();
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsConfiguration gc = ge.getDefaultScreenDevice().getDefaultConfiguration();
BufferedImage stripIm;
if ( numImages > 0 ) {
stripIm = loadImageBuffered( imageName );
if (stripIm != null) {
int width = (int) stripIm.getWidth() / numImages;
int height = stripIm.getHeight();
int transparency = stripIm.getColorModel().getTransparency();
Graphics2D stripGC;
for (int i = 0; i < numImages; i++) {
BufferedImage holder = gc.createCompatibleImage(width, height, transparency);
list.add(holder);
stripGC = holder.createGraphics();
stripGC.drawImage(stripIm, 0, 0, width, height, i * width, 0, (i * width) + width, height, null);
stripGC.dispose();
}
return list;
}
}
return null;
}
==== GameSpriteAdvanced class framework example ====
The ''loadStripImageArray()'' method listed above is used in a ''GameSpriteAdvanced'' class for the following demonstration:
c) Bats and fireballs using the GameSpriteAdvanced class:
* Notable features:
* Elastic collisions with window borders
* Rectangle intersection collisions between bats and fireballs
* Sound playback
* Sprite destruction
* Requires [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/media/bat.gif|images/bat.gif]], [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/media/explosion.gif|images/explosion.gif]] and [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/media/explosion.wav|sounds/explosion.wav]]
* It is a good practice to put images (including sprites) and sounds in their own separate directories; well-organized code is always a good practice.
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/animatedSprites/GameSpriteAdvanced.java|GameSpriteAdvanced.java]] (superclass)
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/animatedSprites/BatAnimatedSprite.java|BatAnimatedSprite.java]] (subclass)
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/animatedSprites/FireballAnimatedSprite.java|FireballAnimatedSprite.java]] (subclass)
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/animatedSprites/SoundPlayback.java|SoundPlayback.java]]
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/animatedSprites/AnimatedSpriteDemoPanel.java|AnimatedSpriteDemoPanel.java]]
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/animatedSprites/AnimatedSpriteDemo.java|AnimatedSpriteDemo.java]] (main program)
* [[http://rockhopper.monmouth.edu/~jchung/cs498gd/fa15/labs/animation/animatedSprites/RandomCoordinates.java|RandomCoordinates.java]]
----