package com.indyforge.twod.engine.graphics.rendering.scenegraph; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Transparency; import java.util.HashSet; import java.util.Set; import com.indyforge.twod.engine.graphics.ImageDesc; import com.indyforge.twod.engine.graphics.rendering.scenegraph.math.Vector2f; import com.indyforge.twod.engine.resources.TransientRenderedEntity; import com.indyforge.twod.engine.resources.assets.AssetManager; import com.indyforge.twod.engine.sound.SoundManager; /** * A scene is a special entity which has some more features: * * - Keyboard input management * * - Scaled rendering (always chooses the maximum size but never ignores the * ratio!!!) This is very handy. * * Since this is also an entity you have all basic features already provided by * the entity class. Obviously you can add multiple entities to a scene :-) . * * @author Christopher Probst * @see GraphicsEntity * @see Vector2f */ public class Scene extends GraphicsEntity { /** * */ private static final long serialVersionUID = 1L; /* * The keyboard state which is used to process the input. * * IMPORTANT: This attribute is not serialized! */ private transient Set<Integer> keyboardState, singleKeyboardState, singleDiscardedKeyboardState; // The asset manager of this scene private final AssetManager assetManager; // The sound manager of this scene private final SoundManager soundManager; // The width and height of the scene private final int width, height; // The ratio of this entity private final float ratio; /* * The processor which processes this scene. * * IMPORTANT: This attribute is transient. */ transient SceneProcessor processor = null; /** * @return the single discarded keyboard state. If the set does not exist * yet it will be created. */ private Set<Integer> singleDiscardedKeyboardState() { if (singleDiscardedKeyboardState == null) { singleDiscardedKeyboardState = new HashSet<Integer>(); } return singleDiscardedKeyboardState; } /** * @return the single keyboard state. If the set does not exist yet it will * be created. */ private Set<Integer> singleKeyboardState() { if (singleKeyboardState == null) { singleKeyboardState = new HashSet<Integer>(); } return singleKeyboardState; } /** * @return the keyboard state. If the set does not exist yet it will be * created. */ private Set<Integer> keyboardState() { if (keyboardState == null) { keyboardState = new HashSet<Integer>(); } return keyboardState; } /** * Creates a new scene without an asset manager. * * @param width * The provided viewport width. * @param height * The provided viewport height. */ public Scene(int width, int height) throws Exception { this(null, width, height); } /** * Creates a new scene. * * @param assetManager * The asset manager of this scene. Can be null if you do not * need assets. * @param width * The provided viewport width. * @param height * The provided viewport height. */ public Scene(AssetManager assetManager, int width, int height) throws Exception { if (width <= 0) { throw new IllegalArgumentException("width must be > 0"); } else if (height <= 0) { throw new IllegalArgumentException("height must be > 0"); } // Save the asset manager this.assetManager = assetManager; // Create and save the new sound manager soundManager = new SoundManager(assetManager); // Init this.width = width; this.height = height; // Calc new ratio ratio = width / (float) height; } /** * @return the asset manager of this scene. */ public AssetManager assetManager() { return assetManager; } /** * @return the sound manager of this scene. */ public SoundManager soundManager() { return soundManager; } /** * @return the scene processor. */ public SceneProcessor processor() { return processor; } /** * Clears the keyboard state. * * @return this scene for chaining. */ public Scene clearKeyboardState() { // Clear the complete keyboard state if (keyboardState != null && !keyboardState.isEmpty()) { keyboardState.clear(); } return this; } /** * Sets the pressed flag for the given key code. * * @param vk * The virtual key code. * @param pressed * The pressed flag. * @return this for chaining. */ public Scene pressed(int vk, boolean pressed) { // Add or remove the key code if (pressed) { // Already exists ? if (keyboardState().add(vk)) { // This key was not pressed yet! singleKeyboardState().add(vk); } } else { /* * Remove from all maps! */ if (keyboardState != null) { keyboardState.remove(vk); } if (singleKeyboardState != null) { singleKeyboardState.remove(vk); } if (singleDiscardedKeyboardState != null) { singleDiscardedKeyboardState.remove(vk); } } return this; } /** * Returns a given key's pressed status. If this method returns true for a * given key code the key must be released and pressed again before this * method returns true again for the given key code. * * @param vk * The keycode you want to check. (KeyEvent.VK_****) * @return true if the key is pressed otherwise false. */ public boolean isSinglePressed(int vk) { if (singleKeyboardState != null && singleKeyboardState.contains(vk)) { singleDiscardedKeyboardState().add(vk); return true; } return false; } /** * Returns a given key's pressed status. * * @param vk * The keycode you want to check. (KeyEvent.VK_****) * @return true if the key is pressed otherwise false. */ public boolean isPressed(int vk) { return keyboardState != null && keyboardState.contains(vk); } /** * @return the x-y ratio as float. */ public float ratio() { return ratio; } /** * @return the viewport width. */ public int width() { return width; } /** * @return the viewport height. */ public int height() { return height; } /** * @return the size as vector. */ public Vector2f sizeAsVector() { return new Vector2f(width, height); } /** * @return the size as dimension. */ public Dimension size() { return new Dimension(width, height); } /** * Creates a rendered opaque image using the given graphics entity. * * @param bgColor * The background color. * @param graphicsEntity * The graphics entity you want to render. * @return a rendered opaque image. */ public RenderedImage renderedOpaqueEntity(Color bgColor, GraphicsEntity graphicsEntity) { return renderedEntity(Transparency.OPAQUE, bgColor, graphicsEntity); } /** * Creates a rendered image using the given graphics entity. * * @param transparency * The transparency. * @param bgColor * The background color. * @param graphicsEntity * The graphics entity you want to render. * @return a rendered image. */ public RenderedImage renderedEntity(int transparency, Color bgColor, GraphicsEntity graphicsEntity) { // Bundle to entity and store as rendered transient entity TransientRenderedEntity renderedEntity = new TransientRenderedEntity( new ImageDesc().width(width).height(height) .transparency(transparency), bgColor, graphicsEntity); // Create a new entity RenderedImage root = new RenderedImage(renderedEntity); // This image is centered root.centered(true); // Set position root.position().set(width * 0.5f, height * 0.5f); // Adjust scale root.scale().set(width, height); return root; } /** * Simulates the scene with a time-per-frame of 0. This method will * transforms the rendering in a way, that the viewport of the scene is * translated to the destination image. Basically needed for simple * rendering without modifying the states of the children. * * * @param destination * The image you want to render to. * @return this scene for chaining. */ public final Scene simulate(Image destination) { return simulate(destination, 0); } /** * Simulates the scene with a given time-per-frame. This method will * transforms the rendering in a way, that the viewport of the scene is * translated to the destination image. * * @param destination * The image you want to render to. * @param tpf * The time-per-frame to update the children states. * @return this scene for chaining. */ public final Scene simulate(Image destination, long tpf) { if (destination == null) { throw new NullPointerException("destination"); } // Get the graphics context Graphics graphics = destination.getGraphics(); try { if (graphics instanceof Graphics2D) { // Simulate the scene to image return simulate((Graphics2D) graphics, destination.getWidth(null), destination.getHeight(null), tpf); } else { throw new IllegalArgumentException("The image must return a " + "Graphics2D object when calling getGraphics()."); } } finally { // Dispose graphics.dispose(); } } /** * Simulates the scene with a time-per-frame of 0. This method will * transforms the rendering in a way, that the viewport of the scene is * translated to the graphics context. Basically needed for simple rendering * without modifying the states of the children. * * @param graphics * The graphics context you want to render to. If null the scene * is only updated. * @param width * The width of the destination frame. If < 1 the scene is only * update. * @param height * The height of the destination frame. If < 1 the scene is only * update. * @return this scene for chaining. */ public final Scene simulate(Graphics2D graphics, int width, int height) { return simulate(graphics, width, height, 0); } /** * Simulates the scene with a given time-per-frame. This method will * transforms the rendering in a way, that the viewport of the scene is * translated to the graphics context. * * @param graphics * The graphics context you want to render to. If null the scene * is only updated. * @param width * The width of the destination frame. If < 1 the scene is only * update. * @param height * The height of the destination frame. If < 1 the scene is only * update. * @param tpf * The time-per-frame (ms) to update the children states. * @return this scene for chaining. */ public final Scene simulate(Graphics2D graphics, int width, int height, float tpf) { // Remove the discarded single keyboard state if (singleDiscardedKeyboardState != null) { for (Integer vk : singleDiscardedKeyboardState) { singleKeyboardState.remove(vk); } singleDiscardedKeyboardState.clear(); } // Only update if tpf is > 0 if (tpf >= 0) { // Update the complete scene update(tpf); } // Render if graphics exists and dimension is correct if (graphics != null && width > 0 && height > 0) { // Calc the ratio which we have to fit float dstRatio = ratio; // Calc new dimension float newWidth = width, newHeight = width / dstRatio; // Check height if (newHeight > height) { // Recalc newHeight = height; newWidth = dstRatio * newHeight; } // Copy the graphics Graphics2D copy = (Graphics2D) graphics.create(); try { // At first translate the context copy.translate(width * 0.5f - newWidth * 0.5f, height * 0.5f - newHeight * 0.5f); // Scale using the vector copy.scale(newWidth / this.width, newHeight / this.height); // Limit the clip copy.setClip(0, 0, this.width, this.height); // Set background color copy.setBackground(Color.white); // Clear the rect copy.clearRect(0, 0, this.width, this.height); // Finally render render(copy); } finally { copy.dispose(); } } return this; } }