====== 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]] ----