com.samrj.devil.game.Game.java Source code

Java tutorial

Introduction

Here is the source code for com.samrj.devil.game.Game.java

Source

/*
 * Copyright (c) 2016 Sam Johnson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.samrj.devil.game;

import com.samrj.devil.display.DisplayException;
import com.samrj.devil.display.GLFWUtil;
import com.samrj.devil.display.HintSet;
import com.samrj.devil.game.step.TimeStepper;
import com.samrj.devil.game.sync.Sync;
import com.samrj.devil.math.Vec2i;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GLCapabilities;

/**
 * Utility game class.
 * 
 * @author Samuel Johnson (SmashMaster)
 */
public abstract class Game {
    private static boolean errorCallInit;
    private static boolean initialized;
    private static Thread mainThread;

    private static void ensureMainThread() {
        if (Thread.currentThread() != mainThread)
            throw new IllegalThreadStateException("Not on main thread " + mainThread);
    }

    /**
     * Initializes the windowing system. Whichever thread calls this becomes
     * the main thread, and only classes on the main thread may construct and
     * run games.
     */
    public static final void init() {
        if (initialized)
            throw new IllegalStateException("Already initialized.");

        if (!errorCallInit) {
            GLFW.glfwSetErrorCallback(DisplayException::glfwThrow);
            errorCallInit = true;
        }

        GLFW.glfwInit();
        Gamepads.init();
        mainThread = Thread.currentThread();
        initialized = true;
    }

    /**
     * Terminates the windowing system. Must be called on the main thread.
     */
    public static final void terminate() {
        ensureMainThread();
        initialized = false;
        mainThread = null;
        GLFW.glfwTerminate();
    }

    /**
     * Provides a simple and convenient way to run a game. The game must have a
     * zero-argument constructor.
     * 
     * To use this method, simply call Game.run(YourGameClass::new);
     * 
     * @param constructor Any method which constructs and returns a Game.
     */
    public static final void run(GameConstructor constructor) throws Exception {
        Game.init();
        Game instance = constructor.construct();
        instance.run();
        instance.destroy();
        Game.terminate();
    }

    private boolean running;
    private long lastFrameTime;
    private long frameStart;

    public final long monitor, window;
    public final GLCapabilities capabilities;
    public final Sync sync;
    public final Mouse mouse;
    public final Keyboard keyboard;
    public final TimeStepper stepper;

    private final long frameTime;
    private final EventBuffer eventBuffer;

    private boolean onLongFrame;
    private boolean destroyed;

    /**
     * Creates a new game object. Initializes the window with the given config.
     * 
     * @param title The title of the window.
     * @param hints The window hints to use.
     * @param config The configuration to use.
     */
    public Game(String title, HintSet hints, GameConfig config) {
        if (title == null || config == null)
            throw new NullPointerException();
        if (!initialized)
            throw new IllegalStateException("Game.init() not called.");
        ensureMainThread();

        // <editor-fold defaultstate="collapsed" desc="Initialize Window">
        {
            GLFW.glfwDefaultWindowHints();
            GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GL11.GL_FALSE);
            GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GL11.GL_FALSE);
            GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, config.borderless ? GL11.GL_FALSE : GL11.GL_TRUE);
            GLFW.glfwWindowHint(GLFW.GLFW_FLOATING, GL11.GL_FALSE);
            GLFW.glfwWindowHint(GLFW.GLFW_STENCIL_BITS, 0);
            if (config.msaa > 0)
                GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, config.msaa);
            if (hints != null)
                hints.glfw();

            GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_DEBUG_CONTEXT, GL11.GL_TRUE);

            monitor = config.fullscreen ? GLFW.glfwGetPrimaryMonitor() : 0;
            window = GLFW.glfwCreateWindow(config.resolution.x, config.resolution.y, title, monitor, 0);

            GLFW.glfwMakeContextCurrent(window);
            GLFW.glfwSwapInterval(config.vsync ? 1 : 0);
            GLFW.glfwSetInputMode(window, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED);
        }

        if (!config.fullscreen) //Center window
        {
            Vec2i windowSize = GLFWUtil.getWindowSize(window);
            GLFWVidMode mode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());

            GLFW.glfwSetWindowPos(window, (mode.width() - windowSize.x) / 2, (mode.height() - windowSize.y) / 2);
        }
        // </editor-fold>
        // <editor-fold defaultstate="collapsed" desc="Initialize OpenGL Context">
        {
            capabilities = GL.createCapabilities();
            GL11.glViewport(0, 0, config.resolution.x, config.resolution.y);
            GL11.glDisable(GL13.GL_MULTISAMPLE);
        }
        // </editor-fold>
        // <editor-fold defaultstate="collapsed" desc="Initialize Sync">
        {
            if (!config.vsync && config.fps > 0) {
                sync = new Sync(config.fps, config.sleeper);
                frameTime = sync.getFrameTime();
            } else {
                sync = null;
                GLFWVidMode mode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());
                frameTime = Math.round(1_000_000_000.0 / mode.refreshRate());
            }
        }
        // </editor-fold>
        // <editor-fold defaultstate="collapsed" desc="Initialize Input">
        {
            mouse = new Mouse(window, this::onMouseMoved, this::onMouseButton, this::onMouseScroll);
            mouse.setGrabbed(false);
            keyboard = new Keyboard(this::onKey);
            eventBuffer = new EventBuffer(window, mouse, keyboard);
        }
        // </editor-fold>
        stepper = config.stepper;
    }

    /**
     * Creates a new game object. Initializes the window with the given config.
     * 
     * @param title The title of the window.
     * @param config The configuration to use.
     */
    public Game(String title, GameConfig config) {
        this(title, null, config);
    }

    /**
     * Creates a new game window with the default title "Game" and the default
     * configuration. The default config creates a decorated window at 1280p.
     */
    public Game() {
        this("Game", new GameConfig());
    }

    /**
     * Sets the title of this window.
     * 
     * @param title The title to set to.
     */
    public final void setTitle(String title) {
        ensureMainThread();
        GLFW.glfwSetWindowTitle(window, title);
    }

    public final Vec2i getResolution() {
        return GLFWUtil.getWindowSize(window);
    }

    // <editor-fold defaultstate="collapsed" desc="Overridable Methods">
    /**
     * Called whenever the mouse is moved. Always called before step() and
     * render(). The coordinates are relative to the bottom left corner of the
     * display.
     * 
     * @param x The x position of the mouse.
     * @param y The y position of the mouse.
     * @param dx The amount the x position has changed since the last call.
     * @param dy The amount the y position has changed since the last call.
     */
    public void onMouseMoved(float x, float y, float dx, float dy) {
    };

    /**
     * Called whenever a mouse button is pressed. Always called before step()
     * and render(). The key modifier bit field is defined by GLFW:
     * 
     * http://www.glfw.org/docs/latest/group__mods.html
     * 
     * @param button The GLFW enum representing which button was affected.
     * @param action One of GLFW_PRESS or GLFW_RELEASE.
     * @param mods Bit field describing which modifier keys were held down.
     */
    public void onMouseButton(int button, int action, int mods) {
    };

    /**
     * Called whenever the scroll wheel is moved. Always called before step()
     * and render().
     * 
     * @param dx The horizontal scroll offset.
     * @param dy The vertical scroll offset.
     */
    public void onMouseScroll(float dx, float dy) {
    };

    /**
     * Called whenever a key is pressed. Always called before step() and
     * render(). The key modifier bit field is defined by GLFW:
     * 
     * http://www.glfw.org/docs/latest/group__mods.html
     * 
     * @param key The GLFW enum representing which key was affected.
     * @param action One of GLFW_PRESS, GLFW_RELEASE or GLFW_REPEAT.
     * @param mods Bit field describing which modifier keys were held down.
     */
    public void onKey(int key, int action, int mods) {
    };

    /**
     * Steps the simulation by a given amount of time. Called after input and
     * before rendering. The duration and number of time steps depends on the
     * time step method chosen.
     * 
     * @param dt The time step, in seconds.
     */
    public void step(float dt) {
    };

    /**
     * Called once per frame after all input and time steps, should be used for
     * any rendering code with OpenGL.
     */
    public void render() {
    };

    /**
     * Called when this game is destroyed. Should release any system resources
     * associated with this game.
     */
    public void onDestroy() {
    };
    // </editor-fold>

    /**
     * Flushes the keyboard/mouse input queues and discards all events. Also
     * releases any held buttons and invalidates the mouse position.
     */
    public final void discardInput() {
        eventBuffer.discardInput();
    }

    /**
     * Marks the current frame as taking a long time, so that excessively long
     * steps are not called next frame. Pairs well with discardInput() in order
     * to combat jumpy state after long frames, such as after loading screens.
     */
    public final void markLongFrame() {
        onLongFrame = true;
    }

    /**
     * Runs the game, showing the window and beginning the game loop. Must be
     * called on the main thread, and the game cannot be destroyed.
     */
    public final void run() {
        if (!initialized)
            throw new IllegalStateException("Game.init() not called.");
        ensureMainThread();
        if (destroyed)
            throw new IllegalStateException("Game has been destroyed.");

        try {
            running = true;
            GLFW.glfwShowWindow(window);

            long lastFrameStart = System.nanoTime() - frameTime;

            while (running) {
                frameStart = System.nanoTime();

                //Input
                GLFW.glfwPollEvents();
                eventBuffer.flushEvents();
                Gamepads.update();
                if (GLFW.glfwWindowShouldClose(window))
                    stop();

                //Step
                if (onLongFrame)
                    lastFrameTime = frameTime;
                else
                    lastFrameTime = frameStart - lastFrameStart;
                onLongFrame = false;
                float dt = (float) (lastFrameTime / 1_000_000_000.0);
                stepper.step(this::step, dt);
                lastFrameStart = frameStart;

                render();

                if (sync != null)
                    sync.sync();
                GLFW.glfwSwapBuffers(window);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException("Thread interrupted.", e);
        } finally {
            stop();
        }
    }

    /**
     * Stops this game. May be called from any thread.
     */
    public final void stop() {
        running = false;
    }

    /**
     * @return How long, in nanoseconds, the duration of the previous frame.
     */
    public final long lastFrameTime() {
        return lastFrameTime;
    }

    /**
     * @return The time, as measured by System.nanoTime(), when this frame
     *         started. This is also the time that the previous frame ended.
     */
    public final long frameStart() {
        return frameStart;
    }

    /**
     * Destroys this game and window, and releases any resources associated
     * resources.
     */
    public final void destroy() {
        ensureMainThread();
        if (destroyed)
            return;
        destroyed = true;

        onDestroy();
        GLFW.glfwDestroyWindow(window);
    }

    /**
     * @return Whether this game has been destroyed.
     */
    public final boolean isDestroyed() {
        return destroyed;
    }

    @FunctionalInterface
    public interface GameConstructor {
        Game construct() throws Exception;
    }
}