package frame;
import io.ADXInput;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import javax.swing.JFrame;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Controllers;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import static org.lwjgl.opengl.GL11.*;
import render.ADXGraphics;
/**
* The main game class. This class manages everything, it is the backbone of the entire engine.
* It works with a 2 threads approach. The main thread renders all the instances while another one updates them.
* The 2 threads are synchronized at the fewer places possible to get the maximum out of each one and to reduce the risks to have a thread blocking the other.
* It is in this class that all flag, states and important variables are, such as the game size, display states, etc.
* @author Alexandre Desbiens
*/
public class ADXGame {
/* Static constants */
/** No stretch, the game texture will be centered in the canvas. */
public static final byte STRETCH_NONE = 0;
/** Full stretch, the canvas will be filled and the game texture will probably be deformed. */
public static final byte STRETCH_FULL = 1;
/** Smart stretch, the game will try to stretch the game texture to the maximum but without distorting it. */
public static final byte STRETCH_SMART = 2;
/** Extended stretch, similar to Smart, but will entirely fill the screen, cutting some of the game texture. */
public static final byte STRETCH_EXTEND = 3;
/* Member variables */
// Game and Display state
private int width;
private int height;
private int displayWidth;
private int displayHeight;
private boolean fullscreen = false;
private boolean resizable = false;
private boolean autoResize = false;
private boolean vsync = false;
private byte stretchMode = STRETCH_NONE;
private static boolean run = false;
private boolean resizeDisplayMode = false;
private boolean resizeCanvas = false;
private boolean resizeViewport = false;
private boolean controllersEnabled = false;
// Update
private int updateLoops = 0;
private int renderLoops = 0;
private ADXInput input;
// Rendering variables
private int renderX = 0;
private int renderY = 0;
private float renderXScale = 1;
private float renderYScale = 1;
private int updateFPS = 60;
private int renderFPS = -1;
private int gBufferStart = 1000;
private int gBufferInc = 1000;
private ADXGraphics g;
private JFrame frame = null;
private Canvas canvas = null;
// Rooms, Instances and Views
private ArrayList<ADXRoom> lRoom = new ArrayList<ADXRoom>();
private ArrayList<ADXInstance> lInstance = new ArrayList<ADXInstance>();
private ArrayList<DepthChange> lDepthChange = new ArrayList<DepthChange>();
private ArrayList<ADXView> lView = new ArrayList<ADXView>();
private ADXRoom currentRoom = null;
private int currentRoomID;
private int viewXDecal = 0;
private int viewYDecal = 0;
private int viewXMinus = 0;
private int viewYMinus = 0;
private int currentView = -1;
/* Constructors */
/** Empty constructor, initializes the game with size 1024x768. */
public ADXGame() {
this(1024, 768);
}
/**
* Initializes the game and the display with the same given size.
* @param width Width of the game, in pixels
* @param height Height of the game, in pixels
*/
public ADXGame(int width, int height) {
this(width, height, width, height);
}
/**
* Initializes the game and the display with different given sizes.
* @param width Width of the game, in pixels
* @param height Height of the game, in pixels
* @param displayWidth Width of the display, in pixels (minus the insets)
* @param displayHeight Height of the display, in pixels (minus the insets)
*/
public ADXGame(int width, int height, int displayWidth, int displayHeight) {
this.width = width;
this.height = height;
this.displayWidth = displayWidth;
this.displayHeight = displayHeight;
}
/* Accessors/Mutators */
/**
* Returns the width of the game.
* @return Width of the game, in pixels
*/
public int getWidth() {
return width;
}
/**
* Return the height of the game.
* @return Height of the game, in pixels
*/
public int getHeight() {
return height;
}
/**
* Return the width of the content section of the display.
* @return Width of the display, in pixels (minus insets)
*/
public int getDisplayWidth() {
return displayWidth;
}
/**
* Return the height of the content section of the display.
* @return Height of the display, in pixels (minus insets)
*/
public int getDisplayHeight() {
return displayHeight;
}
/**
* Sets the size of the content section of the display to the given size.
* @param width Width of the display, in pixels (minus insets)
* @param height Height of the display, in pixels (minus insets)
*/
public void setDisplaySize(int width, int height) {
if (width != displayWidth || height != displayHeight) {
displayWidth = width;
displayHeight = height;
resizeCanvas = true;
resizeViewport = true;
}
}
/**
* Return the width of the canvas embedded in the display.
* @return Width of the canvas, in pixels
*/
public int getCanvasWidth() {
return canvas.getWidth();
}
/**
* Return the height of the canvas embedded in the display.
* @return Height of the canvas, in pixels
*/
public int getCanvasHeight() {
return canvas.getHeight();
}
/**
* Gets the width of the current desktop (or monitor).
* @return Width of the desktop, in pixels
*/
public int getDesktopWidth() {
return Display.getDesktopDisplayMode().getWidth();
}
/**
* Gets the height of the current desktop (or monitor).
* @return Height of the desktop, in pixels
*/
public int getDesktopHeight() {
return Display.getDesktopDisplayMode().getHeight();
}
/**
* Return the fullscreen state of the game.
* @return Fullscreen state of the game
*/
public boolean isFullscreen() {
return fullscreen;
}
/**
* Sets the fullscreen state of the game.
* @param full Fullscreen state of the game
*/
public void setFullscreen(boolean full) {
if (fullscreen != full) {
fullscreen = full;
resizeDisplayMode = true;
resizeCanvas = false;
resizeViewport = false;
}
}
/**
* Returns whether or not the display is resizable by the user.
* @return Resizable state of the display
*/
public boolean isResizable() {
return resizable;
}
/**
* Sets whether or not the display can be resized by the user.
* @param resize Resizable state of the display
*/
public void setResizable(boolean resize) {
resizable = resize;
}
/**
* Returns true if the game's size is automatically resized to the canvas size when resized.
* @return Automatic resize state of the game
*/
public boolean isAutoResize() {
return autoResize;
}
/**
* Sets the automatic resize option of the game with the size of the underlying canvas.
* @param resize Automatic resize state of the game
*/
public void setAutoResize(boolean resize) {
autoResize = resize;
}
/**
* Returns if V-Sync is enabled on the display.
* @return V-Sync state of the display
*/
public boolean isVSync() {
return vsync;
}
/**
* Enables or disables V-Sync on the display.
* @param sync V-Sync state of the display
*/
public void setVSync(boolean sync) {
vsync = sync;
Display.setVSyncEnabled(sync);
}
/**
* Returns the current stretch mode of the game texture in the canvas.
* @return Current stretch mode
*/
public byte getStretchMode() {
return stretchMode;
}
/**
* Changes the current stretch mode.
* @param mode New stretch mode
*/
public void setStretchMode(byte mode) {
stretchMode = mode;
}
/**
* Returns the X position of the game texture on the underlying canvas.
* @return X position of the game texture, in pixels
*/
public int getRenderX() {
return renderX;
}
/**
* Returns the Y position of the game texture when drawn on the canvas.
* @return Y position of the game texture, in pixels
*/
public int getRenderY() {
return renderY;
}
/**
* Returns the horizontal scale of the game texture when drawn on the canvas.
* @return Horizontal scale of the game texture (1.0f is 100%)
*/
public float getRenderXScale() {
return renderXScale;
}
/**
* Returns the vertical scale of the game texture on the canvas.
* @return Vertical scale of the game texture (1.0f is 100%)
*/
public float getRenderYScale() {
return renderYScale;
}
/**
* Gets the number of update cycles per second.
* @return Number of update cycles per second
*/
public int getUpdateFPS() {
return updateFPS;
}
/**
* Sets the number of update cycles the game will process per second (set to -1 for maximum UPS).
* @param fps Number of update cycles per second
*/
public void setUpdateFPS(int fps) {
updateFPS = fps;
}
/**
* Gets the number of render cycle the game will process per second.
* @return Number of render cycles per second
*/
public int getRenderFPS() {
return renderFPS;
}
/**
* Sets the number of render cycles per second (set to -1 for maximum FPS).
* @param fps Number of render cycles per second
*/
public void setRenderFPS(int fps) {
renderFPS = fps;
}
/**
* Initializes the graphics buffer with the given values.
* @param startingSize Size of the buffer at game start
* @param increment Size to add to the buffer on overflow
*/
public void setGraphicsBuffer(int startingSize, int increment) {
if (run) {
postWarning("Changing graphics buffer size and increment has no effect when game is started.");
return;
}
gBufferStart = startingSize;
gBufferInc = increment;
}
/**
* Enables or disables the use of controllers in the game.
* @param c Controllers enabled state
*/
public void setControllersEnabled(boolean c) {
if (run) {
postWarning("Enabling controllers has no effect when game is started.");
return;
}
controllersEnabled = c;
}
/**
* Returns the graphics object created by the game.
* @return Graphics object
*/
public ADXGraphics getGraphics() {
return g;
}
/**
* Returns the input object created by the game.
* @return Input object
*/
public ADXInput getInput() {
return input;
}
/**
* Sets a custom frame and canvas on which to render the game.
* @param frame Frame the canvas is on
* @param canvas Custom canvas to render to
*/
public void setFrame(JFrame frame, Canvas canvas) {
if (run) {
postWarning("Modifying frame and canvas has no effect when game is started.");
return;
}
this.frame = frame;
this.canvas = canvas;
}
/* Classes */
/**
* Buffered depth change for a given instance.
* Depth change is buffered to avoid certain glitches when changing an instance's position in the instance list while navigating it.
* @author Alexandre Desbiens
*/
private class DepthChange {
public ADXInstance inst;
public int depth;
public DepthChange(ADXInstance inst, int depth) {
this.inst = inst;
this.depth = depth;
}
}
/**
* The update thread that manages the current room, instances and views.
* It synchronizes using a non-blocking method and the number of cycles it can do in a second can be changed at any time.
* The update thread also calculates a second time and outputs some useful information to the default debug output stream.
* @author Alexandre Desbiens
*/
private class TUpdate implements Runnable {
private ADXGame game;
private double updateDelta = 0;
private double secondTimer = 0;
public TUpdate(ADXGame game) {
this.game = game;
}
@Override
public void run() {
long timeThen = System.nanoTime();
long timeNow;
ADXInstance inst;
while (run) {
// Calculate time delta
timeNow = System.nanoTime();
if (updateFPS > 0) {
updateDelta += (timeNow - timeThen) * updateFPS / 1000000000.0f;
}
secondTimer += (timeNow - timeThen) / 1000000000.0f;
timeThen = timeNow;
if (updateDelta >= 1 || updateFPS == -1) {
input.update();
// Update instances
currentRoom.update(game, input);
synchronized (lInstance) {
for (int i = lInstance.size() - 1; i >= 0; i--) {
inst = lInstance.get(i);
if (inst.getRoomID() == currentRoomID || inst.getRoomID() == -1) {
inst.update(game, input);
}
}
}
// Change instances depth
if (lDepthChange.size() > 0) {
for (DepthChange d : lDepthChange) {
removeInstance(d.inst);
d.inst.setDepth(d.depth);
addInstance(d.inst, false);
}
}
updateLoops++;
//secondTimer += updateDelta;
updateDelta = 0;
}
// Debug output
if (secondTimer >= 1) {
postString("UP: " + updateLoops + " | RE: " + renderLoops + " | IN: " + lInstance.size());
secondTimer = 0;
updateLoops = 0;
renderLoops = 0;
}
}
}
}
/* Member functions */
/**
* Initializes the game, starts the update thread and enters the render loop.
* It starts by creating a frame and a canvas to draw on if no custom ones has been specified.
* The it creates the Keyboard, Mouse, Controllers (if enabled), and Graphics and Input object.
* OpenGL is initialized afterwards and the update thread is started after everything has been created and initialized.
* The render thread is then entered. It manages all the drawing side of the game by asking the current room and instances to render.
* When it quits, it releases all hardware, disposes the Swing frame and closes.
*/
public void start() {
if (run) {
postWarning("Cannot start more than 1 game instance.");
return;
}
postString("ADX Engine Code I - Booting up");
postBreak();
postString("Starting game...");
// Create Custom Swing Frame
postString("Creating Custom Swing Frame...");
if (canvas == null) {
canvas = new Canvas();
canvas.setBackground(Color.black);
}
canvas.setSize(displayWidth, displayHeight);
if (frame == null) {
frame = new JFrame();
frame.add(canvas);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
frame.addWindowFocusListener(new WindowAdapter() {
@Override
public void windowGainedFocus(WindowEvent e) {
canvas.requestFocusInWindow();
}
});
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
run = false;
}
});
canvas.requestFocus();
// Link IO Hardware
postString("Linking IO Hardware...");
try {
Display.setParent(canvas);
Display.setDisplayMode(new DisplayMode(displayWidth, displayHeight));
Display.create();
Keyboard.create();
Mouse.create();
if (controllersEnabled) {
Controllers.create();
}
} catch (LWJGLException e) {
e.printStackTrace();
}
input = new ADXInput(this, controllersEnabled);
g = new ADXGraphics(gBufferStart, gBufferInc);
// Change OpenGL State
postString("Changing OpenGL State...");
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
adjustViewport();
// Initiate starting room
postString("Initiating Starting Room...");
if (currentRoom != null) {
currentRoom.init(this);
} else {
postError("The game has no starting room!");
}
// Create Update Thread
run = true;
postString("Threading Update...");
Thread tUpdate = new Thread(new TUpdate(this));
tUpdate.start();
postString("Game successfully initiated.");
postBreak();
ADXInstance inst;
while (run && !Display.isCloseRequested()) {
// Change display
if (resizeDisplayMode) {
adjustDisplayMode();
currentRoom.resize(this);
resizeDisplayMode = false;
}
// Resize canvas
if (resizeCanvas) {
canvas.setSize(displayWidth, displayHeight);
frame.pack();
resizeCanvas = false;
}
glClear(GL_COLOR_BUFFER_BIT);
if (lView.size() == 0) {
// No views in list, draw in entire canvas
if (currentView != -1) {
viewXDecal = 0;
viewYDecal = 0;
viewXMinus = 0;
viewYMinus = 0;
adjustViewport();
currentView = -1;
}
g.startDraw(this, null, -1);
currentRoom.render(this, g);
synchronized (lInstance) {
for (int i = 0; i < lInstance.size(); i++) {
inst = lInstance.get(i);
if (inst.getRoomID() == currentRoomID || inst.getRoomID() == -1) {
lInstance.get(i).render(this, g);
}
}
}
g.flushDraw();
} else {
// Draw each view
int it = lView.size() - 1;
ADXView v;
while (it >= 0) {
if (it < lView.size()) {
v = lView.get(it);
if (currentView != it) {
viewXDecal = v.getScreenX();
viewYDecal = height - v.getScreenY() - v.getHeight();
viewXMinus = width - v.getWidth();
viewYMinus = height - v.getHeight();
adjustViewport();
currentView = it;
v.setID(it);
}
g.startDraw(this, v, it);
currentRoom.render(this, g);
synchronized (lInstance) {
for (int j = 0; j < lInstance.size(); j++) {
inst = lInstance.get(j);
if (inst.getRoomID() == currentRoomID || inst.getRoomID() == -1) {
lInstance.get(j).render(this, g);
}
}
}
g.flushDraw();
}
it--;
}
}
if ((Display.wasResized() || resizeViewport) && !fullscreen) {
displayWidth = canvas.getWidth();
displayHeight = canvas.getHeight();
adjustViewport();
currentRoom.resize(this);
resizeViewport = false;
}
Display.update();
if (renderFPS != -1) {
Display.sync(renderFPS);
}
renderLoops++;
}
run = false;
postBreak();
postString("Stopping game...");
postString("Clearing Room and Instances...");
clearAllInstances();
currentRoom.exit(this, null);
postString("Joining Update Thread...");
try {
tUpdate.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
postString("Breaking Hardware Links...");
// TODO - Ca brise ici maintenant, aucune id�e pourquoi...
Display.destroy();
Keyboard.destroy();
Mouse.destroy();
if (controllersEnabled) {
Controllers.destroy();
}
postString("Disposing Swing Frame...");
frame.dispose();
postString("Game successfully stopped.");
postBreak();
postString("ADX Engine Code I - Shutdown");
}
/** Stops the game. */
public void stop() {
run = false;
}
/** Adjusts the OpenGL viewport to match the current size of the game, canvas and view. */
private void adjustViewport() {
if (autoResize) {
// Resize the game texture to the size of the canvas
width = displayWidth;
height = displayHeight;
renderX = 0;
renderY = 0;
renderXScale = 1;
renderYScale = 1;
} else {
switch (stretchMode) {
case STRETCH_NONE:
renderX = (displayWidth - width) / 2;
renderY = (displayHeight - height) / 2;
renderXScale = 1;
renderYScale = 1;
break;
case STRETCH_FULL:
renderX = 0;
renderY = 0;
renderXScale = displayWidth / (float) width;
renderYScale = displayHeight / (float) height;
break;
case STRETCH_SMART:
float x = displayWidth / (float) width;
float y = displayHeight / (float) height;
if (x < y) {
renderX = 0;
renderY = (int) (displayHeight - height * x) / 2;
renderXScale = x;
renderYScale = x;
} else {
renderX = (int) (displayWidth - width * y) / 2;
renderY = 0;
renderXScale = y;
renderYScale = y;
}
break;
case STRETCH_EXTEND:
x = displayWidth / (float) width;
y = displayHeight / (float) height;
if (x < y) {
renderX = (int) (displayWidth - width * y) / 2;
renderY = 0;
renderXScale = y;
renderYScale = y;
} else {
renderX = 0;
renderY = (int) (displayHeight - height * x) / 2;
renderXScale = x;
renderYScale = x;
}
break;
}
}
int xDecal = (int) (viewXDecal * renderXScale);
int yDecal = (int) (viewYDecal * renderYScale);
int xMinus = (int) (viewXMinus * renderXScale);
int yMinus = (int) (viewYMinus * renderYScale);
glViewport(renderX + xDecal, renderY + yDecal, (int) (width * renderXScale) - xMinus, (int) (height * renderYScale) - yMinus);
glLoadIdentity();
glOrtho(0, width - viewXMinus, height - viewYMinus, 0, 1, -1);
}
/** Adjusts the display options, size and fullscreen state. */
private void adjustDisplayMode() {
if (fullscreen) {
// Search for a valid display mode
DisplayMode m = null;
try {
DisplayMode[] modes = Display.getAvailableDisplayModes();
for (DisplayMode mode : modes) {
if (mode.isFullscreenCapable() && mode.getBitsPerPixel() >= 32 && mode.getWidth() == displayWidth && mode.getHeight() == displayHeight) {
m = mode;
break;
}
}
if (m != null) {
Display.setDisplayMode(m);
Display.setFullscreen(true);
}
} catch (LWJGLException e) {
e.printStackTrace();
}
} else {
// Resize display
try {
Display.setFullscreen(false);
Display.setDisplayMode(new DisplayMode(displayWidth, displayHeight));
} catch (LWJGLException e) {
e.printStackTrace();
}
}
adjustViewport();
}
/**
* Adds the given room to the list of game rooms.
* @param room Room to add
*/
public void addRoom(ADXRoom room) {
lRoom.add(room);
if (currentRoom == null) {
currentRoom = room;
currentRoomID = room.getRoomID();
}
}
/**
* Returns the room matching the given ID.
* @param roomID The ID of the room to return
* @return Room matching the given ID, or null if none has been found.
*/
public ADXRoom getRoom(int roomID) {
for (ADXRoom r : lRoom) {
if (r.getRoomID() == roomID) {
return r;
}
}
return null;
}
/**
* Change the current game's room to the given one and initializes it.
* @param roomID New room ID
*/
public void changeRoom(int roomID) {
for (ADXRoom r : lRoom) {
if (r.getRoomID() == roomID) {
currentRoom.exit(this, r);
clearInstances(currentRoom.getRoomID());
currentRoomID = r.getRoomID();
r.init(this);
currentRoom = r;
break;
}
}
}
/**
* Adds the given instance to the instance list.
* @param inst Instance to add
*/
public void addInstance(ADXInstance inst) {
addInstance(inst, true);
}
/**
* Adds the given instance to the instance list and initialized it if init is true.
* @param inst Instance to add
* @param init Initialize the added instance
*/
public void addInstance(ADXInstance inst, boolean init) {
boolean added = false;
if (inst.getRoomID() == Integer.MIN_VALUE) {
inst.setRoomID(currentRoomID);
}
if (init) {
inst.init(this);
}
for (int i = 0; i < lInstance.size(); i++) {
if (lInstance.get(i).getDepth() < inst.getDepth()) {
added = true;
lInstance.add(i, inst);
break;
}
}
if (!added) {
lInstance.add(inst);
}
}
/** Ask for an instance's depth to be changed by the game. */
public void changeInstanceDepth(ADXInstance inst, int depth) {
lDepthChange.add(new DepthChange(inst, depth));
}
/**
* Removes the given instance from the instance list.
* @param inst Instance to remove
*/
public void removeInstance(ADXInstance inst) {
lInstance.remove(inst);
}
/**
* Clear all instances with the given room ID.
* @param roomID ID of the room to clear
*/
public void clearInstances(int roomID) {
for (int i = lInstance.size() - 1; i >= 0; i--) {
if (lInstance.get(i).getRoomID() == roomID) {
lInstance.get(i).delete(this);
}
}
}
/** Clears all instances. */
public void clearAllInstances() {
for (int i = lInstance.size() - 1; i >= 0; i--) {
lInstance.get(i).delete(this);
}
}
/**
* Adds the given view to the list of views.
* @param view View to add
*/
public void addView(ADXView view) {
lView.add(view);
}
/**
* Remove the given view from the game's view list.
* @param view View to remove
*/
public void removeView(ADXView view) {
lView.remove(view);
}
/* Static utilities */
/** Posts a line break to the current debug output stream. */
public static void postBreak() {
System.out.println();
}
/**
* Posts a normal message to the current debug output stream.
* @param message Message to post
*/
public static void postString(String message) {
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss:SSS");
Date date = new Date();
System.out.println("[" + dateFormat.format(date) + "] " + message);
}
/**
* Posts a warning message to the current debug output stream.
* @param message Warning message to post
*/
public static void postWarning(String message) {
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss:SSS");
Date date = new Date();
System.out.println("[" + dateFormat.format(date) + "][War] " + message);
}
/**
* Posts an error message to the current debug output stream and asks the game to close.
* @param message Error message to post.
*/
public static void postError(String message) {
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss:SSS");
Date date = new Date();
System.out.println("[" + dateFormat.format(date) + "][ERR] " + message);
run = false;
}
}