/* * Copyright (c) 2013, Slick2D * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the Slick2D nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package org.newdawn.slick; import java.io.IOException; import java.util.Properties; import org.lwjgl.LWJGLException; import org.lwjgl.Sys; import org.lwjgl.input.Cursor; import org.lwjgl.opengl.Display; import org.lwjgl.opengl.Drawable; import org.lwjgl.opengl.Pbuffer; import org.lwjgl.opengl.PixelFormat; import org.newdawn.slick.gui.GUIContext; import org.newdawn.slick.openal.SoundStore; import org.newdawn.slick.opengl.CursorLoader; import org.newdawn.slick.opengl.ImageData; import org.newdawn.slick.opengl.renderer.Renderer; import org.newdawn.slick.opengl.renderer.SGL; import org.newdawn.slick.util.Log; import org.newdawn.slick.util.ResourceLoader; /** * A generic game container that handles the game loop, fps recording and * managing the input system * * @author kevin */ public abstract class GameContainer implements GUIContext { /** The renderer to use for all GL operations */ protected static SGL GL = Renderer.get(); /** The shared drawable if any */ protected static Drawable SHARED_DRAWABLE; /** The time the last frame was rendered */ protected long lastFrame; /** The last time the FPS recorded */ protected long lastFPS; /** The last recorded FPS */ protected int recordedFPS; /** The current count of FPS */ protected int fps; /** True if we're currently running the game loop */ protected boolean running = true; /** The width of the display */ protected int width; /** The height of the display */ protected int height; /** The game being managed */ protected Game game; /** The default font to use in the graphics context */ private Font defaultFont; /** The graphics context to be passed to the game */ private Graphics graphics; /** The input system to pass to the game */ protected Input input; /** The FPS we want to lock to */ protected int targetFPS = -1; /** True if we should show the fps */ private boolean showFPS = true; /** The minimum logic update interval */ protected long minimumLogicInterval = 1; /** The stored delta */ protected long storedDelta; /** The maximum logic update interval */ protected long maximumLogicInterval = 0; /** The last game started */ protected Game lastGame; /** True if we should clear the screen each frame */ protected boolean clearEachFrame = true; /** True if the game is paused */ protected boolean paused; /** True if we should force exit */ protected boolean forceExit = true; /** True if vsync has been requested */ protected boolean vsync; /** Smoothed deltas requested */ protected boolean smoothDeltas; /** The number of samples we'll attempt through hardware */ protected int samples; /** True if this context supports multisample */ protected boolean supportsMultiSample; /** True if we should render when not focused */ protected boolean alwaysRender; /** True if we require stencil bits */ protected static boolean stencil; /** * Create a new game container wrapping a given game * * @param game The game to be wrapped */ protected GameContainer(Game game) { this.game = game; lastFrame = getTime(); getBuildVersion(); Log.checkVerboseLogSetting(); } public static void enableStencil() { stencil = true; } /** * Set the default font that will be intialised in the graphics held in this container * * @param font The font to use as default */ public void setDefaultFont(Font font) { if (font != null) { this.defaultFont = font; } else { Log.warn("Please provide a non null font"); } } /** * Indicate whether we want to try to use fullscreen multisampling. This will * give antialiasing across the whole scene using a hardware feature. * * @param samples The number of samples to attempt (2 is safe) */ public void setMultiSample(int samples) { this.samples = samples; } /** * Check if this hardware can support multi-sampling * * @return True if the hardware supports multi-sampling */ public boolean supportsMultiSample() { return supportsMultiSample; } /** * The number of samples we're attempting to performing using * hardware multisampling * * @return The number of samples requested */ public int getSamples() { return samples; } /** * Indicate if we should force exitting the VM at the end * of the game (default = true) * * @param forceExit True if we should force the VM exit */ public void setForceExit(boolean forceExit) { this.forceExit = forceExit; } /** * Indicate if we want to smooth deltas. This feature will report * a delta based on the FPS not the time passed. This works well with * vsync. * * @param smoothDeltas True if we should report smooth deltas */ public void setSmoothDeltas(boolean smoothDeltas) { this.smoothDeltas = smoothDeltas; } /** * Check if the display is in fullscreen mode * * @return True if the display is in fullscreen mode */ public boolean isFullscreen() { return false; } /** * Get the aspect ratio of the screen * * @return The aspect ratio of the display */ public float getAspectRatio() { return getWidth() / getHeight(); } /** * Indicate whether we want to be in fullscreen mode. Note that the current * display mode must be valid as a fullscreen mode for this to work * * @param fullscreen True if we want to be in fullscreen mode * @throws SlickException Indicates we failed to change the display mode */ public void setFullscreen(boolean fullscreen) throws SlickException { } /** * Enable shared OpenGL context. After calling this all containers created * will shared a single parent context * * @throws SlickException Indicates a failure to create the shared drawable */ public static void enableSharedContext() throws SlickException { try { SHARED_DRAWABLE = new Pbuffer(64, 64, new PixelFormat(8, 0, 0), null); } catch (LWJGLException e) { throw new SlickException("Unable to create the pbuffer used for shard context, buffers not supported", e); } } /** * Get the context shared by all containers * * @return The context shared by all the containers or null if shared context isn't enabled */ public static Drawable getSharedContext() { return SHARED_DRAWABLE; } /** * Indicate if we should clear the screen at the beginning of each frame. If you're * rendering to the whole screen each frame then setting this to false can give * some performance improvements * * @param clear True if the the screen should be cleared each frame */ public void setClearEachFrame(boolean clear) { this.clearEachFrame = clear; } /** * Renitialise the game and the context in which it's being rendered * * @throws SlickException Indicates a failure rerun initialisation routines */ public void reinit() throws SlickException { } /** * Pause the game - i.e. suspend updates */ public void pause() { setPaused(true); } /** * Resumt the game - i.e. continue updates */ public void resume() { setPaused(false); } /** * Check if the container is currently paused. * * @return True if the container is paused */ public boolean isPaused() { return paused; } /** * Indicates if the game should be paused, i.e. if updates * should be propogated through to the game. * * @param paused True if the game should be paused */ public void setPaused(boolean paused) { this.paused = paused; } /** * True if this container should render when it has focus * * @return True if this container should render when it has focus */ public boolean getAlwaysRender () { return alwaysRender; } /** * Indicate whether we want this container to render when it has focus * * @param alwaysRender True if this container should render when it has focus */ public void setAlwaysRender (boolean alwaysRender) { this.alwaysRender = alwaysRender; } /** * Get the build number of slick * * @return The build number of slick */ public static int getBuildVersion() { try { Properties props = new Properties(); props.load(ResourceLoader.getResourceAsStream("version")); int build = Integer.parseInt(props.getProperty("build")); Log.info("Slick Build #"+build); return build; } catch (Exception e) { Log.info("Unable to determine Slick build number"); return -1; } } /** * Get the default system font * * @return The default system font */ @Override public Font getDefaultFont() { return defaultFont; } /** * Check if sound effects are enabled * * @return True if sound effects are enabled */ public boolean isSoundOn() { return SoundStore.get().soundsOn(); } /** * Check if music is enabled * * @return True if music is enabled */ public boolean isMusicOn() { return SoundStore.get().musicOn(); } /** * Indicate whether music should be enabled * * @param on True if music should be enabled */ public void setMusicOn(boolean on) { SoundStore.get().setMusicOn(on); } /** * Indicate whether sound effects should be enabled * * @param on True if sound effects should be enabled */ public void setSoundOn(boolean on) { SoundStore.get().setSoundsOn(on); } /** * Retrieve the current default volume for music * @return the current default volume for music */ public float getMusicVolume() { return SoundStore.get().getMusicVolume(); } /** * Retrieve the current default volume for sound fx * @return the current default volume for sound fx */ public float getSoundVolume() { return SoundStore.get().getSoundVolume(); } /** * Set the default volume for sound fx * @param volume the new default value for sound fx volume */ public void setSoundVolume(float volume) { SoundStore.get().setSoundVolume(volume); } /** * Set the default volume for music * @param volume the new default value for music volume */ public void setMusicVolume(float volume) { SoundStore.get().setMusicVolume(volume); } /** * Get the width of the standard screen resolution * * @return The screen width */ @Override public abstract int getScreenWidth(); /** * Get the height of the standard screen resolution * * @return The screen height */ @Override public abstract int getScreenHeight(); /** * Get the width of the game canvas * * @return The width of the game canvas */ @Override public int getWidth() { return width; } /** * Get the height of the game canvas * * @return The height of the game canvas */ @Override public int getHeight() { return height; } /** * Set the icon to be displayed if possible in this type of * container * * @param ref The reference to the icon to be displayed * @throws SlickException Indicates a failure to load the icon */ public abstract void setIcon(String ref) throws SlickException; /** * Set the icons to be used for this application. Note that the size of the icon * defines how it will be used. Important ones to note * * Windows window icon must be 16x16 * Windows alt-tab icon must be 24x24 or 32x32 depending on Windows version (XP=32) * * @param refs The reference to the icon to be displayed * @throws SlickException Indicates a failure to load the icon */ public abstract void setIcons(String[] refs) throws SlickException; /** * Get the accurate system time * * @return The system time in milliseconds */ @Override public long getTime() { return (Sys.getTime() * 1000) / Sys.getTimerResolution(); } /** * Sleep for a given period * * @param milliseconds The period to sleep for in milliseconds */ public void sleep(int milliseconds) { long target = getTime()+milliseconds; while (getTime() < target) { try { Thread.sleep(1); } catch (Exception e) {} } } /** * Set the mouse cursor to be displayed - this is a hardware cursor and hence * shouldn't have any impact on FPS. * * @param ref The location of the image to be loaded for the cursor * @param hotSpotX The x coordinate of the hotspot within the cursor image * @param hotSpotY The y coordinate of the hotspot within the cursor image * @throws SlickException Indicates a failure to load the cursor image or create the hardware cursor */ @Override public abstract void setMouseCursor(String ref, int hotSpotX, int hotSpotY) throws SlickException; /** * Set the mouse cursor to be displayed - this is a hardware cursor and hence * shouldn't have any impact on FPS. * * @param data The image data from which the cursor can be construted * @param hotSpotX The x coordinate of the hotspot within the cursor image * @param hotSpotY The y coordinate of the hotspot within the cursor image * @throws SlickException Indicates a failure to load the cursor image or create the hardware cursor */ @Override public abstract void setMouseCursor(ImageData data, int hotSpotX, int hotSpotY) throws SlickException; /** * Set the mouse cursor based on the contents of the image. Note that this will not take * account of render state type changes to images (rotation and such). If these effects * are required it is recommended that an offscreen buffer be used to produce an appropriate * image. An offscreen buffer will always be used to produce the new cursor and as such * this operation an be very expensive * * @param image The image to use as the cursor * @param hotSpotX The x coordinate of the hotspot within the cursor image * @param hotSpotY The y coordinate of the hotspot within the cursor image * @throws SlickException Indicates a failure to load the cursor image or create the hardware cursor */ public abstract void setMouseCursor(Image image, int hotSpotX, int hotSpotY) throws SlickException; /** * Set the mouse cursor to be displayed - this is a hardware cursor and hence * shouldn't have any impact on FPS. * * @param cursor The cursor to use * @param hotSpotX The x coordinate of the hotspot within the cursor image * @param hotSpotY The y coordinate of the hotspot within the cursor image * @throws SlickException Indicates a failure to load the cursor image or create the hardware cursor */ @Override public abstract void setMouseCursor(Cursor cursor, int hotSpotX, int hotSpotY) throws SlickException; /** * Get a cursor based on a image reference on the classpath. The image * is assumed to be a set/strip of cursor animation frames running from top to * bottom. * * @param ref The reference to the image to be loaded * @param x The x-coordinate of the cursor hotspot (left -> right) * @param y The y-coordinate of the cursor hotspot (bottom -> top) * @param width The x width of the cursor * @param height The y height of the cursor * @param cursorDelays image delays between changing frames in animation * * @throws SlickException Indicates a failure to load the image or a failure to create the hardware cursor */ public void setAnimatedMouseCursor(String ref, int x, int y, int width, int height, int[] cursorDelays) throws SlickException { try { Cursor cursor; cursor = CursorLoader.get().getAnimatedCursor(ref, x, y, width, height, cursorDelays); setMouseCursor(cursor, x, y); } catch (IOException e) { throw new SlickException("Failed to set mouse cursor", e); } catch (LWJGLException e) { throw new SlickException("Failed to set mouse cursor", e); } } /** * Set the default mouse cursor - i.e. the original cursor before any native * cursor was set */ @Override public abstract void setDefaultMouseCursor(); /** * Get the input system * * @return The input system available to this game container */ @Override public Input getInput() { return input; } /** * Get the current recorded FPS (frames per second) * * @return The current FPS */ public int getFPS() { return recordedFPS; } /** * Indicate whether mouse cursor should be grabbed or not * * @param grabbed True if mouse cursor should be grabbed */ public abstract void setMouseGrabbed(boolean grabbed); /** * Check if the mouse cursor is current grabbed. This will cause it not * to be seen. * * @return True if the mouse is currently grabbed */ public abstract boolean isMouseGrabbed(); /** * Retrieve the time taken to render the last frame, i.e. the change in time - delta. * * @return The time taken to render the last frame */ protected int getDelta() { long time = getTime(); int delta = (int) (time - lastFrame); lastFrame = time; return delta; } /** * Updated the FPS counter */ protected void updateFPS() { if (getTime() - lastFPS > 1000) { lastFPS = getTime(); recordedFPS = fps; fps = 0; } fps++; } /** * Set the minimum amount of time in milliseonds that has to * pass before update() is called on the container game. This gives * a way to limit logic updates compared to renders. * * @param interval The minimum interval between logic updates */ public void setMinimumLogicUpdateInterval(int interval) { minimumLogicInterval = interval; } /** * Set the maximum amount of time in milliseconds that can passed * into the update method. Useful for collision detection without * sweeping. * * @param interval The maximum interval between logic updates */ public void setMaximumLogicUpdateInterval(int interval) { maximumLogicInterval = interval; } /** * Update and render the game * * @param delta The change in time since last update and render * @throws SlickException Indicates an internal fault to the game. */ protected void updateAndRender(int delta) throws SlickException { if (smoothDeltas) { if (getFPS() != 0) { delta = 1000 / getFPS(); } } input.poll(width, height); Music.poll(delta); if (!paused) { storedDelta += delta; if (storedDelta >= minimumLogicInterval) { try { if (maximumLogicInterval != 0) { long cycles = storedDelta / maximumLogicInterval; for (int i=0;i minimumLogicInterval) { game.update(this, (int) (remainder % maximumLogicInterval)); storedDelta = 0; } else { storedDelta = remainder; } } else { game.update(this, (int) storedDelta); storedDelta = 0; } } catch (Throwable e) { // Log.error(e); throw new SlickException("Game.update() failure.", e); } } } else { game.update(this, 0); } if (hasFocus() || getAlwaysRender()) { if (clearEachFrame) { GL.glClear(SGL.GL_COLOR_BUFFER_BIT | SGL.GL_DEPTH_BUFFER_BIT); } GL.glLoadIdentity(); graphics.resetTransform(); graphics.resetFont(); graphics.resetLineWidth(); graphics.setAntiAlias(false); try { game.render(this, graphics); } catch (Throwable e) { // Log.error(e); throw new SlickException("Game.render() failure.", e); } graphics.resetTransform(); if (showFPS) { defaultFont.drawString(10, 10, "FPS: "+recordedFPS); } GL.flush(); } if (targetFPS != -1) { Display.sync(targetFPS); } } /** * Indicate if the display should update only when the game is visible * (the default is true) * * @param updateOnlyWhenVisible True if we should updated only when the display is visible */ public void setUpdateOnlyWhenVisible(boolean updateOnlyWhenVisible) { } /** * Check if this game is only updating when visible to the user (default = true) * * @return True if the game is only updated when the display is visible */ public boolean isUpdatingOnlyWhenVisible() { return true; } /** * Initialise the GL context */ protected void initGL() { Log.info("Starting display "+width+"x"+height); GL.initDisplay(width, height); if (input == null) { input = new Input(height); } input.init(height); // no need to remove listeners? //input.removeAllListeners(); if (game instanceof InputListener) { input.removeListener((InputListener) game); input.addListener((InputListener) game); } if (graphics != null) { graphics.setDimensions(getWidth(), getHeight()); } lastGame = game; } /** * Initialise the system components, OpenGL and OpenAL. * * @throws SlickException Indicates a failure to create a native handler */ protected void initSystem() throws SlickException { initGL(); setMusicVolume(1.0f); setSoundVolume(1.0f); graphics = new Graphics(width, height); defaultFont = graphics.getFont(); } /** * Enter the orthographic mode */ protected void enterOrtho() { enterOrtho(width, height); } /** * Indicate whether the container should show the FPS * * @param show True if the container should show the FPS */ public void setShowFPS(boolean show) { showFPS = show; } /** * Check if the FPS is currently showing * * @return True if the FPS is showing */ public boolean isShowingFPS() { return showFPS; } /** * Set the target fps we're hoping to get * * @param fps The target fps we're hoping to get */ public void setTargetFrameRate(int fps) { targetFPS = fps; } /** * Indicate whether the display should be synced to the * vertical refresh (stops tearing) * * @param vsync True if we want to sync to vertical refresh */ public void setVSync(boolean vsync) { this.vsync = vsync; Display.setVSyncEnabled(vsync); } /** * True if vsync is requested * * @return True if vsync is requested */ public boolean isVSyncRequested() { return vsync; } /** * True if the game is running * * @return True if the game is running */ protected boolean running() { return running; } /** * Inidcate we want verbose logging * * @param verbose True if we want verbose logging (INFO and DEBUG) */ public void setVerbose(boolean verbose) { Log.setVerbose(verbose); } /** * Cause the game to exit and shutdown cleanly */ public void exit() { running = false; } /** * Check if the game currently has focus * * @return True if the game currently has focus */ public abstract boolean hasFocus(); /** * Get the graphics context used by this container. Note that this * value may vary over the life time of the game. * * @return The graphics context used by this container */ public Graphics getGraphics() { return graphics; } /** * Enter the orthographic mode * * @param xsize The size of the panel being used * @param ysize The size of the panel being used */ protected void enterOrtho(int xsize, int ysize) { GL.enterOrtho(xsize, ysize); } }