Java tutorial
/* * Copyright 2011 Alexander Baumgartner * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.ridiculousRPG; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.script.Invocable; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptException; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.Application.ApplicationType; import com.badlogic.gdx.Input.Buttons; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Camera; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.PixmapIO; import com.badlogic.gdx.graphics.Pixmap.Blending; import com.badlogic.gdx.graphics.Pixmap.Format; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.utils.Array; import com.ridiculousRPG.camera.CameraSimpleOrtho2D; import com.ridiculousRPG.camera.CameraTrackMovableService; import com.ridiculousRPG.event.EventObject; import com.ridiculousRPG.i18n.TextLoader; import com.ridiculousRPG.map.MapRenderService; import com.ridiculousRPG.movement.Movable; import com.ridiculousRPG.movement.misc.MoveFadeColorAdapter; import com.ridiculousRPG.service.GameService; import com.ridiculousRPG.service.GameServiceDefaultImpl; import com.ridiculousRPG.service.GestureDetectorService; import com.ridiculousRPG.ui.DisplayErrorService; import com.ridiculousRPG.ui.MenuService; import com.ridiculousRPG.util.ColorSerializable; import com.ridiculousRPG.util.ExecInMainThread; import com.ridiculousRPG.util.ExecWithGlContext; import com.ridiculousRPG.util.ObjectState; import com.ridiculousRPG.util.Speed; import com.ridiculousRPG.util.Zipper; /** * @author Alexander Baumgartner */ public abstract class GameBase extends GameServiceDefaultImpl implements ApplicationListener { private static GameBase instance; private SpriteBatch spriteBatch; private Camera camera; private ObjectState globalState; private GameServiceProvider serviceProvider; private ScriptFactory scriptFactory; private ScriptEngine sharedEngine; private GameOptions options; private TextLoader i18n; private Rectangle plane = new Rectangle(); private Rectangle screen = new Rectangle(); private List<Thread> glContextThread = new ArrayList<Thread>(); private Map<String, EventObject> globalEvents = new HashMap<String, EventObject>(); private boolean exitForced = true; private boolean triggerActionKeyPressed; private boolean fullscreen; private boolean controlKeyPressedOld, controlKeyPressed, actionKeyPressedOld, actionKeyPressed; private boolean glAsyncLoadable; private Color backgroundColor = new ColorSerializable(0f, 0f, 0f, 1f); private Color gameColorTint = new ColorSerializable(1f, 1f, 1f, 1f); private float gameColorBits = gameColorTint.toFloatBits(); private float longPressTime; private boolean terminating; private boolean tap; private boolean longPress; public GameBase(GameOptions options) { this.options = options; } public void create() { glContextThread.add(Thread.currentThread()); fullscreen = options.fullscreen; scriptFactory = options.scriptFactory; rebuildSpriteBatch(); camera = new CameraSimpleOrtho2D(); globalState = new ObjectState(); serviceProvider = new GameServiceProvider(); options.width = Gdx.graphics.getWidth(); options.height = Gdx.graphics.getHeight(); plane.width = camera.viewportWidth = screen.width = Gdx.graphics.getWidth(); plane.height = camera.viewportHeight = screen.height = Gdx.graphics.getHeight(); // Compute language locale FileHandle fh = Gdx.files.internal(options.i18nPath); FileHandle i18nPath = fh.child(options.i18nDefault); if (!i18nPath.exists() && options.i18nDefault.length() > 2) { i18nPath = fh.child(options.i18nDefault.substring(0, 2)); } if (!i18nPath.exists()) { GameBase.$error("GameBase.i18nmissing", "The default language " + "file is missing (invalid game.ini)", null); } i18n = new TextLoader(i18nPath, 5); setLanguage(Locale.getDefault()); // instance != null indicates that GameBase is initialized if (!isInitialized()) instance = this; try { scriptFactory.evalAllScripts(getSharedEngine(), GameBase.$options().initScript, false); } catch (Exception e) { GameBase.$error("GameBase.create", "Error occured while initializing the game", e); } // restore last display mode and language loadUserContext(); camera.update(); } public synchronized static void $error(String tag, String message, Exception e) { if ($().terminating) return; Gdx.app.error(tag, message, e); StringWriter stackTrace = new StringWriter(); e.printStackTrace(new PrintWriter(stackTrace)); String msg = "<" + tag + ">: " + message; msg += "\n\n" + stackTrace; msg += "\n\nEngine version: " + $options().engineVersion; DisplayErrorService.forceMessage(msg); } public static void $info(String tag, String message, Exception ex) { if ($().terminating) return; Gdx.app.log(tag, message, ex); } public static ObjectState $state() { return $().globalState; } public String getText(String container, String key) throws IOException { return i18n.getText(container, key); } public void setLanguage(Locale locale) { FileHandle fh = Gdx.files.internal(options.i18nPath); FileHandle i18nPath = fh.child(locale.getISO3Language()); if (i18nPath.exists()) { setLanguageDir(i18nPath); } else { i18nPath = fh.child(locale.getLanguage()); if (i18nPath.exists()) setLanguageDir(i18nPath); } } public void setLanguageDir(FileHandle directory) { i18n.setDirectory(directory); if (isInitialized()) saveUserContext(); } public void setLanguageISO(String iso639_1) { setLanguage(new Locale(iso639_1)); } public void rebuildSpriteBatch() { if (spriteBatch != null) spriteBatch.dispose(); spriteBatch = new SpriteBatch(); } /** * Returns the FIRST GameBase instance which has been initialized to * simplify the access * * @return The first instance, which has been initialized. */ public static GameBase $() { if (!isInitialized()) throw new IllegalStateException("GameBase not initialized!"); return instance; } /** * The {@link GameServiceProvider} from the first GameBase instance which * has been initialized is used to obtain the requested service.<br> * A shortcut for calling {@link #$serviceProvider() * (TYPE_CAST)$serviceProvider()} * {@link GameServiceProvider#getService(String) .getService(String)} * * @return The requested service or null if no service matches. * @throws ClassCastException * If the named service is not from type T */ @SuppressWarnings("unchecked") public static <T extends GameService> T $service(String name) { if (!isInitialized()) throw new IllegalStateException("GameBase not initialized!"); return (T) $serviceProvider().getService(name); } /** * The {@link GameServiceProvider} from the first GameBase instance which * has been initialized.<br> * A shortcut for calling {@link GameBase#$()}{@link #getServiceProvider() * .getServiceProvider()} * * @return The standard game service provider */ public static GameServiceProvider $serviceProvider() { return $().getServiceProvider(); } /** * The {@link ScriptFactory} from the first GameBase instance which has been * initialized.<br> * A shortcut for calling {@link GameBase#$()}{@link #getScriptFactory() * .getScriptFactory()} * * @return The standard script factory */ public static ScriptFactory $scriptFactory() { return $().getScriptFactory(); } /** * The {@link GameOptions} from the first GameBase instance which has been * initialized.<br> * A shortcut for calling {@link GameBase#$()}{@link #getOptions() * .getOptions()} * * @return The standard script factory */ public static GameOptions $options() { return $().getOptions(); } /** * Returns a path to store temporary files while the game is running * * @return Path to store temporary files */ public static FileHandle $tmpPath() { FileHandle fh = Gdx.files.external($().options.savePath).child(".tmpStorage"); if (!fh.exists()) fh.mkdirs(); return fh; } /** * Deletes all temporary files */ public void clearTmpFiles() { Gdx.files.external($().options.savePath).child(".tmpStorage").deleteDirectory(); } public final void render() { try { if (Gdx.input.isTouched(0) && Gdx.input.isButtonPressed(Buttons.LEFT)) { longPressTime += Gdx.graphics.getDeltaTime(); if (!longPress && longPressTime > .3f) { longPress = true; Array<GestureDetectorService> services = serviceProvider .getServices(GestureDetectorService.class); for (int i = 0; i < services.size; i++) { services.get(i).reset(); } } } else { if (longPressTime > 0f && longPressTime <= .3f) tap = true; longPress = false; longPressTime = 0f; } controlKeyPressedOld = controlKeyPressed; controlKeyPressed = Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) || Gdx.input.isKeyPressed(Keys.CONTROL_RIGHT) || Gdx.input.isTouched(1); actionKeyPressedOld = actionKeyPressed; actionKeyPressed = tap || triggerActionKeyPressed || Gdx.input.isKeyPressed(Input.Keys.SPACE) || Gdx.input.isKeyPressed(Input.Keys.ENTER); triggerActionKeyPressed &= !actionKeyPressedOld; tap &= !actionKeyPressedOld; serviceProvider.computeAll(); Thread.yield(); serviceProvider.drawAll(options.debug); } catch (Exception e) { GameBase.$error("GameBase.render", "Error occured while executing the game", e); } Thread.yield(); } /** * Indicates if the first {@link GameBase} instance has been initialized.<br> * (The first instance which is initzalized becomes the default) * * @return true if initialized */ public static boolean isInitialized() { return instance != null; } /** * Signals if the control key is just pressed.<br> * This method is safe for hiding the keyboard on mobile devices * * @return true if left or right control key is just pressed or the third * finger touches the touchpad */ public boolean isControlKeyDown() { return controlKeyPressed && !controlKeyPressedOld; } /** * Evaluates the given script term and returns the result.<br> * The same engine is used for all evaluations! * * @param script * The script to evaluate (Either the path to the script or the * script itself) * @return The result from this evaluation. * @throws ScriptException */ public Object eval(String script) throws ScriptException { ScriptEngine sharedEngine = getSharedEngine(); Object result = sharedEngine.eval(loadWithLogInfo(script, sharedEngine)); sharedEngine.getBindings(ScriptContext.ENGINE_SCOPE).clear(); return result; } private String loadWithLogInfo(String script, ScriptEngine sharedEngine) { String s = getScriptFactory().loadScript(script); if (s != script) { sharedEngine.put(ScriptEngine.FILENAME, script); } else if (sharedEngine.get(ScriptEngine.FILENAME) == null) { String excerpt = script.replaceAll("\\s+", " ").trim(); if (excerpt.length() > 30) excerpt = excerpt.substring(0, 30); sharedEngine.put(ScriptEngine.FILENAME, excerpt); } return s; } /** * Invokes the function which is defined inside the given script, using the * given arguments.<br> * The same engine is used for all evaluations/invocations! * * @param script * The script containing the function to invoke (Either the path * to the script or the script itself) * @param fncName * The function to invoke * @param args * Arguments for the function * @return The result which was returned by the invoked function * @throws ScriptException * @throws NoSuchMethodException */ public Object invokeFunction(String script, String fncName, Object... args) throws ScriptException, NoSuchMethodException { ScriptEngine sharedEngine = getSharedEngine(); try { if (sharedEngine instanceof Invocable) { sharedEngine.eval(loadWithLogInfo(script, sharedEngine)); return ((Invocable) sharedEngine).invokeFunction(fncName, args); } else { throw new ScriptException("ScriptEngine not Invocable!"); } } finally { sharedEngine.getBindings(ScriptContext.ENGINE_SCOPE).clear(); } } /** * Returns a shared script engine, which is used in many different cases. * * @return A script engine instance */ public ScriptEngine getSharedEngine() { if (sharedEngine == null) { sharedEngine = getScriptFactory().obtainEngine(); } return sharedEngine; } /** * Signals if the action key is just pressed.<br> * This method is safe for hiding the keyboard on mobile devices * * @return true if space, enter or the right mouse button is just pressed * (left mouse button is used for movement) or the second finger * touches the touchpad (first finger is used for movement) */ public boolean isActionKeyDown() { return actionKeyPressed && !actionKeyPressedOld; } /** * Signals if the control key is pressed.<br> * This method is safe for hiding the keyboard on mobile devices * * @return true if left or right control key is pressed or the third finger * touches the touchpad */ public boolean isControlKeyPressed() { return controlKeyPressed && controlKeyPressedOld; } /** * Signals if the action key is pressed.<br> * This method is safe for hiding the keyboard on mobile devices * * @return true if space, enter or the right mouse button is pressed (left * mouse button is used for movement) or the second finger touches * the touchpad (first finger is used for movement) */ public boolean isActionKeyPressed() { return actionKeyPressed && actionKeyPressedOld; } /** * The start up options for the game * * @return start up options */ public GameOptions getOptions() { return options; } /** * Signals if the control key is just released.<br> * This method is safe for hiding the keyboard on mobile devices * * @return true if left or right control key is just released or the third * finger released the touchpad */ public boolean isControlKeyUp() { return controlKeyPressedOld && !controlKeyPressed; } /** * Signals if the action key is just released.<br> * This method is safe for hiding the keyboard on mobile devices * * @return true if space, enter or the right mouse button is just released * (left mouse button is used for movement) or the second finger * released the touchpad (first finger is used for movement) */ public boolean isActionKeyUp() { return actionKeyPressedOld && !actionKeyPressed; } public boolean isLongPress() { return longPress; } /** * Resizes the planes dimensions (e.g. the dimension of the tiled map). */ public void resizePlane(int planeWidth, int planeHeight) { plane.width = planeWidth; plane.height = planeHeight; camera.update(); serviceProvider.resize((int) screen.width, (int) screen.height); } @Override public void resize(int width, int height) { Camera cam = camera; if (options.resize) { float centerX = cam.viewportWidth * .5f; float centerY = cam.viewportHeight * .5f; cam.viewportWidth *= width / screen.width; cam.viewportHeight *= height / screen.height; centerX -= cam.viewportWidth * .5f; centerY -= cam.viewportHeight * .5f; cam.translate(centerX, centerY, 0); } screen.width = width; screen.height = height; cam.update(); serviceProvider.resize(width, height); if (!terminating) saveUserContext(); } public void restoreDefaultResolution() { Gdx.graphics.setDisplayMode(options.width, options.height, fullscreen); } public void pause() { if (Gdx.app.getType() != ApplicationType.Desktop && exitForced && !(getServiceProvider().queryAttention() instanceof MenuService)) autoSave(); } public void resume() { if (lastExitForced()) autoLoad(); } /** * The default tint is {@link Color#WHITE}. That means, that everything is * drawn as it is.<br> * Reset to {@link Color#WHITE} if you want to remove the coloring.<br> * If you use the alpha channel on the entire game, all layers of a map will * become visible. That's probably not what you want.<br> * If you want to fade the entire game out, make a transition to * {@link Color#BLACK}.<br> * Tip: Use an {@link EventObject} in combination with the * {@link MoveFadeColorAdapter} to create color animations (e.g. day and * night effects). * * @see {@link MoveFadeColorAdapter#$(Speed, Color, boolean)} */ public void setGameColorTint(Color tint) { gameColorTint = ColorSerializable.wrap(tint); gameColorBits = tint.toFloatBits(); } public Color getGameColorTint() { return gameColorTint; } public float getGameColorBits() { return gameColorBits; } public Camera getCamera() { return camera; } public void setCamera(Camera camera) { this.camera = camera; } /** * The {@link GameServiceProvider} contains all services which are managed * inside this engine instance. */ public GameServiceProvider getServiceProvider() { return serviceProvider; } /** * The {@link #globalState} can be used to share global variables */ public ObjectState getGlobalState() { return globalState; } /** * Returns the screen bounds. x and y should always be zero.<br> * Don't forget to update the camera if you change the bounds. * * @return The windows (or full screens) dimension */ public Rectangle getScreen() { return screen; } /** * Returns the planes position, which is actually shown by the camera and * the planes width and height .<br> * <br> * The planes position will automatically be updated by * {@link CameraSimpleOrtho2D}. So... use * {@link CameraSimpleOrtho2D#translate(float, float, float)} or * {@link CameraSimpleOrtho2D#lookAt(float, float, float)} instead setting * it directly!<br> * Furthermore you should use {@link #setPlaneWidth(int)} and * {@link #setPlaneHeight(int)} to change the planes dimension! The * dimension should be set to the displayed maps width and height. For * example the class {@link MapRenderService} will automatically change the * dimension when a new map is loaded. * * @return The planes bounds actually displayed on the screen */ public Rectangle getPlane() { return plane; } /** * True if the game is running in fullscreen mode */ public boolean isFullscreen() { return fullscreen; } /** * Toggle the fullscreen mode * * @return true if succeeded, false otherwise */ public boolean toggleFullscreen() { try { // resize is called Gdx.graphics.setDisplayMode(options.width, options.height, !fullscreen); // In Linux setDisplayMode returns false even though it succeeds // ==> DON'T make an if statement! fullscreen = !fullscreen; return true; } catch (Throwable notTooBad) { } return false; } public SpriteBatch getSpriteBatch() { return spriteBatch; } public int getOriginalWidth() { return options.width; } public int getOriginalHeight() { return options.height; } public ScriptFactory getScriptFactory() { return scriptFactory; } public static void toggleCursor() { Gdx.input.setCursorCatched(!Gdx.input.isCursorCatched()); } public void dispose() { try { terminating = true; if (fullscreen) toggleFullscreen(); serviceProvider.dispose(); if (spriteBatch != null) spriteBatch.dispose(); clearTmpFiles(); } catch (Exception ignored) { } } /** * Exits the running game */ public void exit() { terminating = true; exitForced = false; if (fullscreen) toggleFullscreen(); new ExecWithGlContext() { @Override public void exec() { Gdx.app.exit(); } }.runWait(); } public void setBackgroundColor(Color backgroundColor) { this.backgroundColor = ColorSerializable.wrap(backgroundColor); } public Color getBackgroundColor() { return backgroundColor; } public void setGlobalEvents(HashMap<String, EventObject> globalEvents) { this.globalEvents = globalEvents; } /** * Returns the map which stores the global events * * @return A map with all global events * @see #getGlobalEventsClone() */ public Map<String, EventObject> getGlobalEvents() { return globalEvents; } /** * Returns a shallow clone of the global events map, which may be * manipulated without effecting the main map. * * @return A map with all global events * @see #getGlobalEvents() */ @SuppressWarnings("unchecked") public Map<String, EventObject> getGlobalEventsClone() { // the clone method should be faster than instantiating a new map if (globalEvents instanceof HashMap<?, ?>) { return (Map<String, EventObject>) ((HashMap<String, EventObject>) globalEvents).clone(); } return new HashMap<String, EventObject>(globalEvents); } /** * Simulates the action key pressed event. */ public void triggerActionKeyPressed() { this.triggerActionKeyPressed = true; } /** * You have to implement sharing the rendering context yourself.<br> * E.g. In LWJGL you can share the context by the following code:<br> * <code>new SharedDrawable(Display.getDrawable()).makeCurrent();</code><br> * <br> * If sharing the context fails or is not supported, all texture loading and * drawing is done in the main thread. * * @return false If sharing the context failed or is not supported by the * backend, true otherwise. */ protected abstract boolean shareGLContext(); /** * Register a thread context to allow parallel texture loading if possible. */ public void registerGlContextThread() { if (shareGLContext()) { glAsyncLoadable = true; synchronized (glContextThread) { glContextThread.add(Thread.currentThread()); } } } public boolean isGlContextThread() { return glContextThread.contains(Thread.currentThread()); } public boolean isMainThread() { return Thread.currentThread() == glContextThread.get(0); } public boolean isGlAsyncLoadable() { return glAsyncLoadable; } private String getUserContextPath() { return GameBase.$options().savePath + "userContext.sav"; } private FileHandle getServiceStateTmpPath() { return $tmpPath().child("restoreGameServiceState.ser.sav"); } public String getScreenThumbnailName() { return "screenShot.cim"; } public FileHandle getSaveFile(int i) { return Gdx.files.external(GameBase.$options().savePath + "gameState" + i + ".sav"); } /** * Calls {@link #listSaveFiles(int, int, int)} with the default values (2, * 1, 1) * * @see #listSaveFiles(int, int) * @return An array of files, containing null values for empty save slots */ public FileHandle[] listSaveFiles() { return listSaveFiles(2, 1, 1); } /** * Reads all files at the specified save directory.<br> * Empty files are indicated with null. The maximum number of files is * 100*cols+2, where the first file is reserved for auto save (e.g. incoming * call on android device). The second file is reserved for a quick save * feature and should be treated specially by the load/save menu (the auto * save file can be entirely ignored or can also be treated as a special * save file by the menu).<br> * In fact, the amount of 100*cols+2 represents a maximum of 100 rows. * * @param cols * Amount of columns per row * @param emptyTailRows * Amount of empty rows appended at the end * @param minRows * Minimum amount of rows * @return An array of files, containing null values for empty save slots */ public FileHandle[] listSaveFiles(int cols, int emptyTailRows, int minRows) { return listSaveFiles(cols, emptyTailRows, minRows, 100); } /** * Reads all files at the specified save directory.<br> * Empty files are indicated with null. The maximum number of files is * maxRows*cols+2, where the first file is reserved for auto save (e.g. * incoming call on android device). The second file is reserved for a quick * save feature and should be treated specially by the load/save menu (the * auto save file can be entirely ignored or can also be treated as a * special save file by the menu).<br> * * @param cols * Amount of columns per row * @param emptyTailRows * Amount of empty rows appended at the end * @param minRows * Minimum amount of rows * @param maxRows * Maximum amount of rows * @return An array of files, containing null values for empty save slots */ public FileHandle[] listSaveFiles(int cols, int emptyTailRows, int minRows, int maxRows) { maxRows = maxRows * cols + 2; String[] fileNames = Gdx.files.external(GameBase.$options().savePath).file().list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.startsWith("gameState") && name.endsWith(".sav"); } }); // 1 auto save + 1 quick save + maxRows * cols save files FileHandle[] fh = new FileHandle[maxRows]; int max = 0; for (String nm : fileNames) { int index = Integer.parseInt(nm.substring(9, nm.length() - 4)); if (index < fh.length) { fh[index] = getSaveFile(index); int idxEnd = index + 2 + cols * emptyTailRows; if (idxEnd > max) { max = Math.min(idxEnd, fh.length); } } } if (max < 2 + minRows * cols) { max = 2 + minRows * cols; } else while ((max - 2) % cols != 0) { max++; } if (max < fh.length) { FileHandle[] newHandles = new FileHandle[max]; System.arraycopy(fh, 0, newHandles, 0, max); fh = newHandles; } return fh; } private void saveUserContext() { FileHandle fh = Gdx.files.external(getUserContextPath()); try { ObjectOutputStream oOut = new ObjectOutputStream(fh.write(false)); oOut.writeObject(screen); oOut.writeBoolean(fullscreen); oOut.writeObject(i18n.getDirectory().path()); oOut.close(); } catch (IOException e) { GameBase.$info("GameBase.saveUserContext", "Error occured while saving the user context", e); } } private void loadUserContext() { FileHandle fh = Gdx.files.external(getUserContextPath()); if (fh.exists()) { try { ObjectInputStream oIn = new ObjectInputStream(fh.read()); Rectangle newScreen = (Rectangle) oIn.readObject(); fullscreen = oIn.readBoolean(); String i18nPath = (String) oIn.readObject(); oIn.close(); i18n.setDirectory(Gdx.files.internal(i18nPath)); Gdx.graphics.setDisplayMode((int) newScreen.width, (int) newScreen.height, false); if (fullscreen) { Gdx.graphics.setDisplayMode((int) newScreen.width, (int) newScreen.height, true); } } catch (Exception e) { GameBase.$info("GameBase.loadUserContext", "Error occured while loading the user context", e); } } } /** * Saves the state to the save-file number 1, which is the quick-load/save * file * * @see #saveFile(int) * @return true if save is successful, false otherwise */ public boolean quickSave() { return saveFile(1); } /** * Saves the state to the save-file number 0, which is the auto-load/save * file. This will automatically be called on android device, when the game * is paused by an incoming call. * * @see #saveFile(int) * @return true if save is successful, false otherwise */ public boolean autoSave() { return saveFile(0); } /** * Loads the state from save-file number 1, which is the quick-load/save * file * * @see #loadFile(int) * @return true if load is successful, false otherwise */ public boolean quickLoad() { return loadFile(1); } /** * Loads the state from save-file number 0, which is the auto-load/save * file. This will automatically be called on android device, when the game * is resumed (e.g. after an incoming call). * * @see #saveFile(int) * @return true if save is successful, false otherwise */ public boolean autoLoad() { return loadFile(0); } /** * Determines if last exit has been forced by a phone call * * @return true if exit has been forced */ protected boolean lastExitForced() { FileHandle fh = getSaveFile(0); if (fh.exists()) { try { clearTmpFiles(); Zipper.unzip(fh, $tmpPath()); InputStream is = getServiceStateTmpPath().read(); ObjectInputStream oIn = new ObjectInputStream(is); boolean exitForced = oIn.readBoolean(); oIn.close(); clearTmpFiles(); return exitForced; } catch (Exception e) { GameBase.$info("GameBase.lastExitForced", "Error occured while verifying the last exit state", e); } } return false; } /** * Reset the entire engine. The initialization script(s) will be executed to * set the engine into the same state as starting it from scratch. */ public void resetEngine() { try { new ExecInMainThread() { @Override public void exec() { clearTmpFiles(); globalEvents.clear(); globalState.clear(); serviceProvider.clearServices(); scriptFactory.clearGlobalState(); DebugHelper.clear(); scriptFactory.evalAllScripts(sharedEngine, GameBase.$options().initScript, false); } }.runWait(); } catch (Exception e) { GameBase.$error("GameBase.create", "Error occured while initializing the game", e); } } /** * Saves the state to the specified save-file * * @return true if save is successful, false otherwise */ public boolean saveFile(int fileNumber) { try { OutputStream os = getServiceStateTmpPath().write(false); ObjectOutputStream oOut = new ObjectOutputStream(os); oOut.writeBoolean(exitForced); oOut.writeObject(globalState); oOut.writeObject(globalEvents); oOut.writeObject(camera); oOut.writeObject(plane); oOut.writeObject(screen); oOut.writeObject(backgroundColor); oOut.writeObject(gameColorTint); $serviceProvider().saveSerializableServices(oOut); oOut.close(); Zipper.zip($tmpPath(), getSaveFile(fileNumber)); return true; } catch (IOException e) { GameBase.$error("GameBase.saveFile", "Error occured while saving the game", e); } return false; } /** * Takes a screenshot from the current frame buffer. The screenshot will be * stretched to fit into the Rectangle specified by dstW and dstH * * @param srcX * @param srcY * @param srcW * @param srcH * @param dstW * @param dstH * @return A Pixmap containing the screenshot. The screenshot will be * flipped at the y-axis. * @throws IOException */ public Pixmap takeScreenshot(int srcX, int srcY, int srcW, int srcH, int dstW, int dstH) throws IOException { Gdx.gl.glPixelStorei(GL10.GL_PACK_ALIGNMENT, 1); Pixmap pixmap = new Pixmap(srcW, srcH, Format.RGBA8888); Gdx.gl.glReadPixels(srcX, srcY, srcW, srcH, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, pixmap.getPixels()); // scale the picture if (srcW != dstW || srcH != dstH) { Pixmap scale = new Pixmap(dstW, dstH, Format.RGBA8888); Blending old = Pixmap.getBlending(); Pixmap.setBlending(Blending.None); scale.drawPixmap(pixmap, 0, 0, srcW, srcH, 0, 0, dstW, dstH); Pixmap.setBlending(old); pixmap.dispose(); pixmap = scale; } return pixmap; } /** * Takes a screenshot from the current player position and stores it as CIM * file into the temporary save directory.<br> * The following default values will be used:<br> * width=64, height=48, reduction=5, lookAt=player * * @see #writeScreenshot(int, int, float, Movable, FileHandle) * @throws IOException */ public void writeScreenshot() throws IOException { // first try with common name "cameraTrack" CameraTrackMovableService s = serviceProvider.getService(CameraTrackMovableService.class, "cameraTrack"); if (s.getTrackObj() == null) s = null; if (s == null) { Array<CameraTrackMovableService> s2 = serviceProvider.getServices(CameraTrackMovableService.class); for (int i = 0; s == null && i < s2.size; i++) { s = s2.get(i); if (s.getTrackObj() == null) s = null; } } writeScreenshot(64, 48, 5, s == null ? null : s.getTrackObj(), $tmpPath().child(getScreenThumbnailName())); } /** * Takes a screenshot using the parameters below. If the given * {@link FileHandle} has the extension png, the image will be stored as * PNG-image. Otherwise the internal CIM format will be used. * * @param width * The width of the screenshot * @param height * The height of the screenshot * @param reduction * Zoom value. The higher the value, the more the image will be * shrinked.<br> * 1 means no shrinking. < 1 will zoom in. > 1 will zoom * out. * @param lookAt * Optional parameter. If null, the center of the screen will be * the reference point * @param saveTo * The path to save the image * @throws IOException */ public void writeScreenshot(int width, int height, float reduction, Movable lookAt, FileHandle saveTo) throws IOException { Pixmap thumbnail = takeScreenshot(width, height, reduction, lookAt); if ("png".equalsIgnoreCase(saveTo.extension())) { PixmapIO.writePNG(saveTo, thumbnail); } else { PixmapIO.writeCIM(saveTo, thumbnail); } thumbnail.dispose(); } /** * Takes a screenshot using the parameters below. * * @param width * The width of the screenshot * @param height * The height of the screenshot * @param reduction * Zoom value. The higher the value, the more the image will be * shrinked.<br> * 1 means no shrinking. < 1 will zoom in. > 1 will zoom * out. * @param lookAt * Optional parameter. If null, the center of the screen will be * the reference point * @return A Pixmap containing the screenshot. The screenshot will be * flipped at the y-axis. * @throws IOException */ public Pixmap takeScreenshot(int width, int height, float reduction, Movable lookAt) throws IOException { // revise distortion from zooming and changing window size float ratioX = screen.width / camera.viewportWidth; float ratioY = screen.height / camera.viewportHeight; float clipW = width * reduction * ratioX; float clipH = height * reduction * ratioY; // border at the left/right of the screen float camX = Math.max(0f, -camera.position.x * ratioX); // border at the top/bottom of the screen float camY = Math.max(0f, -camera.position.y * ratioY); if (clipW == 0 || clipW > Gdx.graphics.getWidth() - camX * 2f) { clipW = Gdx.graphics.getWidth() - camX * 2f; } if (clipH == 0 || clipH > Gdx.graphics.getHeight() - camY * 2f) { clipH = Gdx.graphics.getHeight() - camY * 2f; } int x; int y; if (lookAt != null) { x = (int) (lookAt.getScreenX() - clipW * .5f); y = (int) (lookAt.getScreenY() - clipH * .5f); } else { x = (int) (Gdx.graphics.getWidth() - clipW * .5f); y = (int) (Gdx.graphics.getHeight() - clipH * .5f); } int w = (int) clipW; int h = (int) clipH; if (x < camX) { x = (int) camX; } else if (Gdx.graphics.getWidth() - camX < x + w) { x = Gdx.graphics.getWidth() - (int) camX - w; } if (y < camY) { y = (int) camY; } else if (Gdx.graphics.getHeight() - camY < y + h) { y = Gdx.graphics.getHeight() - (int) camY - h; } Pixmap thumbnail = takeScreenshot(x, y, w, h, width, height); return thumbnail; } /** * Loads the state from the specified save-file * * @return true if save is successful, false otherwise */ @SuppressWarnings("unchecked") public boolean loadFile(int fileNumber) { FileHandle fh = getSaveFile(fileNumber); if (fh.exists()) { try { resetEngine(); Zipper.unzip(fh, $tmpPath()); } catch (Exception e) { GameBase.$error("GameBase.loadFile", "Error occured while loading the game", e); } new ExecWithGlContext() { @Override public void exec() throws Exception { InputStream is = getServiceStateTmpPath().read(); ObjectInputStream oIn = new ObjectInputStream(is); oIn.readBoolean(); // exitForced globalState = (ObjectState) oIn.readObject(); globalEvents = (Map<String, EventObject>) oIn.readObject(); camera = (Camera) oIn.readObject(); plane = (Rectangle) oIn.readObject(); Rectangle oldScreen = screen; screen = (Rectangle) oIn.readObject(); resize((int) oldScreen.width, (int) oldScreen.height); backgroundColor = (Color) oIn.readObject(); gameColorTint = (Color) oIn.readObject(); gameColorBits = gameColorTint.toFloatBits(); $serviceProvider().forceAttentionReset(); $serviceProvider().loadSerializableServices(oIn); oIn.close(); camera.update(); serviceProvider.resize((int) screen.width, (int) screen.height); } }.runWait(); return true; } return false; } }