Author: Brian A. Ree
Updates: Included jars in the project's local lib dir.
1: Static Main
If you're coming into this tutorial fresh you may want to go through tutorial 0 first to get an idea
of the code we've written thus far. If you're an experienced programmer you can just look over the code here.
In this tutorial we will be picking up where we left off in turoial 0. We're going to be adding to our game foundation code and setting up the GUI elements we'll need for a video game.
Now the first thing we'll need to do is get the basis for drawing windows, in this case Java Swing controls, in place. Now Java Swing is an older GUI API however we only really need to hook
into it for drawing so as long as we can hardware accelerate it, we're fine. Hardware acceleration, for those who don't know indicates that the lower level drawing routines supported by Java have the
ability to take advantage of higher end graphics cards and push drawing operations normally done on the CPU to the GPU. Usually this entails hooking in to OpenGL or DirectX drivers. Since hardware
acceleration is such an increasingly important part of the end user's experience it has become much more pervasive and now Java has the ability to take advantage of
hardware acceleration, if available. I have personally seen how the hardware accelerated graphics calls can speed up your game loop.
package com.middlemindgames.Tutorial1Page0;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import javax.swing.*;
/**
* Handles housing the GamePanel, JPanel, that draws each game state by making the corresponding
* game screen the active screen. Created on February 1, 2017, 10:57 PM by Middlemind Games
*
* @author Victor G. Brusca
*/
public final class MainFrame extends JFrame {
It's time to get excited because we're about to start getting into some GUI, graphical user interface, real soon.
As you can see above the next class we're building here is one that extends the Java Swing APIs JFrame. Now for those of you new to coding
a class is an object model we use in programming to represent that object in a way that is useful to the program we're writing. In this case the
class models a graphical window to be drawn by the computer. The word extension refers to the Java programming languages ability to allow you
to extend existing, compiled classes and make them work the way you want. In this case we need to take control of the JFrame class and override its
usual behavior so that we can draw our game at the way we want. Lastly, the Java Swing API is an older advanced programming interface for
GUI programming, usually of the generic desktop business application variety, however, we only need to use the base graphics context and override its
drawing and update behavior so that we can draw our own graphics.
/**
* The GamePanel, extends JPanel, class that handles drawing the different
* game states.
*/
private GamePanel pnlGame;
/**
* The window width.
*/
private final int winWidth;
/**
* The window height.
*/
private final int winHeight;
/**
* The X offset of this frame.
*/
private final int myX;
/**
* The Y offset of this frame.
*/
private final int myY;
/**
* The game panel width.
*/
private final int panelWidth;
/**
* The game panel height.
*/
private final int panelHeight;
/**
* Constructor that sets the window width and height, and defaults the X, Y
* offsets to 0.
*
* @param WinWidth The window width.
* @param WinHeight The window height.
*/
public MainFrame(int WinWidth, int WinHeight) {
winWidth = WinWidth;
winHeight = WinHeight;
panelWidth = winWidth;
panelHeight = winHeight;
myX = 0;
myY = 0;
InitComponents();
}
public MainFrame(int WinWidth, int WinHeight, int PanWidth, int PanHeight) {
winWidth = WinWidth;
winHeight = WinHeight;
panelWidth = PanWidth;
panelHeight = PanHeight;
myX = (winWidth - panelWidth) / 2;
myY = (winHeight - panelHeight) / 2;
InitComponents();
}
2: Main Frame
Next we're taking a look at the variables and constructors of our MainFrame class.
We're revealing code about this class one piece at a time while we go over it. Then we will plug our new class
into our existing game code. One variable that should catch your eye is the GamePanel pnlGame.
You can sort of tell from this that pnlGame is where we are going to be doing our drawing. Take a look at some of the other
variables. Notice how we have window width/height, and panel width/height? They seem to match up to the variables we had in our static main.
Does this seem redundant? Is there a design issue? It could be considered redundant, it might not be a design issue though. You see
the static main is the grand entrance way into the ballroom that is our game. As such it is considered some what out of scope
for any game heavy variables, and we may want to pass those variables along to higher level classes where there is a more logical connection.
In short we don't want to have to pass around references to our static main launching class or reference its variables once it does
its job of gathering the configuration settings and loading our game.
The next two methods are very self explanatory, they are constructors for our class. You can see there is some flexibilty in the constructors for how values are passed in.
Also you can see there are different values for the panel and the window dimensions. This lets us frame the game panel inside the window and
to potentially have the game panel be a smaller part of a more complex set of controls. The offsets myX and myY are calculated
from the values passed in. Next we'll be covering the InitComponents method you can see called in the constructors above. I wanted to quickly mention
the use of final in this class. Generally speaking when you're designing code in Java if your class is not meant to be extended and customized
you should lock whatever variables and methods that are not going to be allowed to be overridden with the final keyword.
This will prevent any class that extends your main frame implementation from altering it's behavior. It can also cut down on some internal checks
Java has to do when looking for overloaded method signatures etc.
public final GamePanel GetGamePanel() {
return pnlGame;
}
public final int GetWindowWidth() {
return winWidth;
}
public final int GetWindowHeight() {
return winHeight;
}
public final int GetOffsetX() {
return myX;
}
public final int GetOffsetY() {
return myY;
}
public final int GetGamePanelWidth() {
return panelWidth;
}
public final int GetGamePanelHeight() {
return panelHeight;
}
Very quickly, before we get into the init method, I wanted to list the getters for this class. Notice there are no setters.
Can you guess why? This class is meant to be a vehicle to get us from our static main starting point to a GUI display.
The values in the class come from our configuration setup so we don't want these values changing while our app is running.
We also don't want any alterations to our simple class if it is extended, so for that reason we want our class sort of locked down.
For this reason the methods and variables are final, and there are no setters in our class. One exception is the GamePanel instance.
Because it requires a little bit more complexity. We don't instantiate it in the constructor but in the InitComponents method.
For that reason we can't make it final but we don't expose a setter either. So for all practical purposes the member is closed to errant changes.
And now let's start looking into the InitComponents method... but first, I know, I know, get to the point. Quickly I'd like to address
the use of upper case methods in the APIs we're using here. This does not follow the capitalization paradigm that Java uses. That being said,
we are taking artistic license. I personally like separating my methods from standard Java methods so that when I browse them in my IDE not only
are they bold but they are also capitalized. This helps me locate their entries a bit faster.
/**
* Initializes the components used by this JFrame.
*/
public final void InitComponents() {
MmgApiUtils.wr("MainFrame: Found Screen Dimen: " + winWidth + "x" + winHeight);
MmgApiUtils.wr("MainFrame: Found Position: " + myX + "x" + myY);
pnlGame = new GamePanel(this, panelWidth, panelHeight, (winWidth - panelWidth) / 2, (winHeight - panelHeight) / 2);
add(pnlGame.GetCanvas());
pnlGame.GetCanvas().setFocusable(true);
pnlGame.GetCanvas().requestFocus();
pnlGame.GetCanvas().requestFocusInWindow();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
addWindowListener(new WindowListener() {
@Override
public void windowOpened(WindowEvent e) {
}
@Override
@SuppressWarnings("CallToPrintStackTrace")
public void windowClosing(WindowEvent e) {
try {
MmgApiUtils.wr("WindowClosing");
/*
if (GamePanel.SCREEN_MAIN_GAME != null && GamePanel.SCREEN_MAIN_GAME.GetOverworldView() != null) {
GamePanel.SCREEN_MAIN_GAME.GetOverworldView().SetPaused(true);
}
if (GamePanel.PC != null && GamePanel.PC.GetInventory() != null) {
GamePanel.PC.GetInventory().ExportInventory(GameSettings.SAVE_DIR_INVENTORY);
}
if (GamePanel.PC != null && GamePanel.PC.GetState() != null) {
GamePanel.PC.GetState().ExportState(GameSettings.SAVE_DIR_STATE);
}
ResourceContainer.ClearAll();
GamePanel.PAUSE = true;
GamePanel.EXIT = true;
RunFrameRate.PAUSE = true;
RunFrameRate.RUNNING = false;
*/
} catch (Exception ex) {
ex.printStackTrace();
}
dispose();
}
@Override
public void windowClosed(WindowEvent e) {
}
@Override
public void windowIconified(WindowEvent e) {
}
@Override
public void windowDeiconified(WindowEvent e) {
}
@Override
public void windowActivated(WindowEvent e) {
}
@Override
public void windowDeactivated(WindowEvent e) {
}
});
}
Alrighty InitComponents time. Let's look at the first two lines, boooring - I know. They print out the configuration parameters
that are passed into the class. This is a great example of a debugging output that should be controlled by our MmgApiUtils class and it's
boolean control. We can turn off this logging from one location when we're ready. Can you locate a print statement in this method that is not
controlled by our class? Can you tell why we want it that way? You guessed it, the printStackTrace() call. I know I could have used a more
modern and safer call but I got lazy and this is sufficient for our purposes here. The reason why we do not want the exception to be ignored
is because it's a low level initialization call at the core of our game. So we need to know when this fails in all cases.
The next line of code instantiates our member pnlGame. This is pretty straight forward. You can check the API Javadoc
for the GamePanel class to see what the values passed into the constructor are doing. I'll cover one or two here. First off the this reference
is a reference to the instance of our MainFrame class. This lets us reference methods from our MainFrame instance later on if we need to.
The second thing I wanted to mention is the centering of the width and height. You can see this expressed in the (winWidth - panelWidth) / 2 parameters
we pass to the GamePanel constructor. That simple math expression finds the center, so essentially we want to center the game drawing inside the main frame.
Next up, we need to add the GamePanel to the Java Swing drawing routine by calling add, you guessed it. So we call add(pnlGame.GetCanvas()) and now
we're one step closer to drawing a window and having a panel ready for some game drawing. One thing I forgot to mention is the use of private class members.
We could just let them be public but we really want to utilize encapsulation and create a class that exposes proper usage through methods. We'll cover the next
few lines pnlGame.GetCanvas().setFocusable(true) and the next two lines are Java Swing specific calls. Remember Java Swing is a GUI API so it has controls
that allow you to set which control is focused by default. We want the canvas member of our GamePanel class to be focused by default so our player's GUI interaction
is wired by default. Otherwise they will have to click into the main frame to select the canvas.
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
addWindowListener(new WindowListener() { ... }
A few small points. We want to set the default close operation on our window. This is a Java Swing API feature, different GUI APIs
will have different implementations of this. Once we have that set properly we can setup the window event handling. We need to be able to
receive some basic events from our window and we do so by registering a window listener with our window class. Notice the inline creation of a class?
This is the anonymous class construct and is often used to create inline event handlers like we did above. For classes where you just want to register the basic
event handler and not do too much work this is ideal. If you're event handler has a lot of work to do you may want to think about moving the class into a proper
Java class file. Next thing we're going to look at are the window events we're going to be handling.
addWindowListener(new WindowListener() {
@Override
public void windowOpened(WindowEvent e) {
}
@Override
@SuppressWarnings("CallToPrintStackTrace")
public void windowClosing(WindowEvent e) {
try {
MmgApiUtils.wr("WindowClosing");
/*
if (GamePanel.SCREEN_MAIN_GAME != null && GamePanel.SCREEN_MAIN_GAME.GetOverworldView() != null) {
GamePanel.SCREEN_MAIN_GAME.GetOverworldView().SetPaused(true);
}
if (GamePanel.PC != null && GamePanel.PC.GetInventory() != null) {
GamePanel.PC.GetInventory().ExportInventory(GameSettings.SAVE_DIR_INVENTORY);
}
if (GamePanel.PC != null && GamePanel.PC.GetState() != null) {
GamePanel.PC.GetState().ExportState(GameSettings.SAVE_DIR_STATE);
}
ResourceContainer.ClearAll();
GamePanel.PAUSE = true;
GamePanel.EXIT = true;
RunFrameRate.PAUSE = true;
RunFrameRate.RUNNING = false;
*/
} catch (Exception ex) {
ex.printStackTrace();
}
dispose();
}
@Override
public void windowClosed(WindowEvent e) {
}
@Override
public void windowIconified(WindowEvent e) {
}
@Override
public void windowDeiconified(WindowEvent e) {
}
@Override
public void windowActivated(WindowEvent e) {
}
@Override
public void windowDeactivated(WindowEvent e) {
}
});
We're really only interested in the closing event. This gives us a place to put our highest level clean up code.
I know Java is a managed language but we should still be responsible and release objects cleanly at the end of our game.
I left some calls in there from one of my games but I commented them out. You can kind of see what we're doing pausing things to
turn off drawing loops and freeing up resources.
/**
* Forces the GamePanel to repaint itself.
*/
public final void Redraw() {
if (pnlGame != null) {
pnlGame.RenderGame();
}
}
3: Game Panel
This is the last thing we'll be doing in this tutorial with the MainFrame class. We'll be getting into the details of the GamePanel class next.
Now we won't be using much of this class until a little farther down the road but we're going to review it a little bit now while we're here. I'm going to drop the whole
class on you following this text, then we'll go over the class piece by piece. There is alot going on here so don't be overwhelmed.
package com.middlemindgames.Tutorial1Page0;
import com.middlemindgames.TyreGame.GenericEventHandler;
import com.middlemindgames.TyreGame.GenericEventMessage;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.util.Hashtable;
import net.middlemind.MmgGameApiJava.MmgBase.MmgApiUtils;
import net.middlemind.MmgGameApiJava.MmgBase.MmgBmpScaler;
import net.middlemind.MmgGameApiJava.MmgBase.MmgFontData;
import net.middlemind.MmgGameApiJava.MmgBase.MmgGameScreen;
import net.middlemind.MmgGameApiJava.MmgBase.MmgPen;
import net.middlemind.MmgGameApiJava.MmgBase.MmgScreenData;
/**
*
* @author Victor G. Brusca, Middlemind Games
*/
public class GamePanel implements GenericEventHandler {
/**
* An enumeration that lists all of the current game states.
*/
public enum GameStates {
LOADING,
BLANK,
SPLASH,
MAIN_MENU,
ABOUT,
HELP_MENU,
HELP_PROLOGUE,
HELP_ITEM_DESC,
HELP_ENEMY_DESC,
HELP_ITEM_DESC_ITEM_TEXT,
HELP_GAME_PLAY_OVERWORLD,
HELP_GAME_PLAY_BATTLE_MODE,
HELP_CHAR_DESC,
HELP_QUEST_DESC,
HELP_ROOM_DESC,
HELP_ROOM_DESC_ROOM_DETAILS,
MAIN_GAME,
SETTINGS
}
/**
* MainFrame that this panel is hosted in.
*/
private final MainFrame mf;
/**
* Window width.
*/
private final int winWidth;
/**
* Window height.
*/
private final int winHeight;
/**
* The X coordinate of this panel.
*/
private final int myX;
/**
* The Y coordinate of this panel.
*/
private final int myY;
/**
* Window width.
*/
private final int sWinWidth;
/**
* Window height.
*/
private final int sWinHeight;
/**
* The X coordinate of this panel.
*/
private final int sMyX;
/**
* The Y coordinate of this panel.
*/
private final int sMyY;
/**
* Default target game width.
*/
public static final int GAME_WIDTH = 854;
/**
* Default target game height.
*/
public static final int GAME_HEIGHT = 416;
/**
* Pause the game.
*/
public static boolean PAUSE = false;
/**
* Exit the game.
*/
public static boolean EXIT = false;
/**
* Paint helper class, used in the paint drawing routine.
*/
private Graphics2D g2d;
/**
* Holds a reference to all game screens.
*/
private Hashtable<GameStates, MmgGameScreen=MmgGameScreen> gameScreens;
/**
* The current game screen being displayed.
*/
private MmgGameScreen currentScreen;
/**
* Paint helper class, used to draw Mmg API objects.
*/
private final MmgPen p;
/**
* Game state helper class, the previous game state.
*/
private GameStates prevGameState;
/**
* Game state helper class, the current game state.
*/
private GameStates gameState;
public static Hashtable<String, Object=Object> VARS = new Hashtable();
public static String FPS = "Drawing FPS: 0000 Actual FPS: 00";
public static String VAR1 = "** EMPTY **";
public static String VAR2 = "** EMPTY **";
private Canvas canvas;
private BufferStrategy strategy;
private BufferedImage background;
private Graphics2D backgroundGraphics;
private Graphics2D graphics;
private final double scale = 1;
private int updateTick = 0;
private long now;
private long prev;
private final Font debugFont;
private Font tmpF;
public static GameType GAME_TYPE = GameType.NEW_GAME;
public enum GameType {
NEW_GAME,
CONTINUED_GAME
}
public int lastX;
public int lastY;
public long lastKeyPressEvent = -1;
private Graphics2D bg;
private Graphics2D g;
/**
* Constructor, sets the MainFrame, window dimensions, and position of this
* JPanel.
*
* @param Mf The MainFrame class this panel belongs to.
* @param WinWidth The target window width.
* @param WinHeight The target window height.
* @param X The X coordinate of this JPanel.
* @param Y The Y coordinate of this JPanel.
*/
@SuppressWarnings({"LeakingThisInConstructor", "OverridableMethodCallInConstructor"})
public GamePanel(MainFrame Mf, int WinWidth, int WinHeight, int X, int Y) {
mf = Mf;
winWidth = WinWidth;
winHeight = WinHeight;
sWinWidth = (int) (winWidth * scale);
sWinHeight = (int) (winHeight * scale);
myX = X;
myY = Y;
sMyX = myX + (winWidth - sWinWidth);
sMyY = myY + (winHeight - sWinHeight);
now = System.currentTimeMillis();
prev = System.currentTimeMillis();
canvas = new Canvas(MmgBmpScaler.GRAPHICS_CONFIG);
canvas.setSize(winWidth, winHeight);
MmgApiUtils.wr("GamePanel Window Width: " + winWidth);
MmgApiUtils.wr("GamePanel Window Height: " + winHeight);
MmgApiUtils.wr("GamePanel Offset X: " + myX);
MmgApiUtils.wr("GamePanel Offset Y: " + myY);
MmgScreenData screenData = new MmgScreenData(winWidth, winHeight, GamePanel.GAME_WIDTH, GamePanel.GAME_HEIGHT);
MmgApiUtils.wr(MmgScreenData.ToString());
MmgFontData fontData = new MmgFontData();
MmgApiUtils.wr(MmgFontData.ToString());
debugFont = MmgFontData.CreateDefaultFontSm();
p = new MmgPen();
MmgPen.ADV_RENDER_HINTS = true;
gameScreens = new Hashtable(20);
gameState = GameStates.BLANK;
SwitchGameState(GameStates.SPLASH);
canvas.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseDragged(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
}
@Override
public void mouseMoved(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
}
});
canvas.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
//MmgApiUtils.wr("KeyTyped: " + e.getKeyCode() + " KeyChar: " + e.getKeyChar() + " X: " + lastX + " Y: " + lastY);
}
@Override
public void keyPressed(KeyEvent e) {
//MmgApiUtils.wr("KeyPressed: " + e.getKeyCode() + " KeyChar: " + e.getKeyChar());
}
@Override
public void keyReleased(KeyEvent e) {
//MmgApiUtils.wr("KeyReleased: " + e.getKeyCode() + " KeyChar: " + e.getKeyChar());
}
});
canvas.addMouseListener(new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
ProcessClick(e.getX(), e.getY());
}
@Override
public void mousePressed(MouseEvent e) {
ProcessPress(e.getX(), e.getY());
}
@Override
public void mouseReleased(MouseEvent e) {
ProcessRelease(e.getX(), e.getY());
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
});
}
public final void ProcessAClick() {
currentScreen.ProcessAClick();
}
public final void ProcessBClick() {
currentScreen.ProcessBClick();
}
public final void ProcessDebugClick() {
currentScreen.ProcessDebugClick();
}
public final void ProcessDpadPress(int dir) {
currentScreen.ProcessDpadPress(dir);
}
public final void ProcessDpadRelease(int dir) {
currentScreen.ProcessDpadRelease(dir);
}
public final void ProcessPress(int x, int y) {
int nx = x;
int ny = y;
int anx = x - MmgScreenData.GetGameLeft() - myX;
int any = y - MmgScreenData.GetGameTop() - myY;
currentScreen.ProcessScreenPress(anx, any);
}
public final void ProcessRelease(int x, int y) {
int nx = x;
int ny = y;
int anx = x - MmgScreenData.GetGameLeft() - myX;
int any = y - MmgScreenData.GetGameTop() - myY;
currentScreen.ProcessScreenRelease(anx, any);
}
public final void ProcessClick(int x, int y) {
int nx = x;
int ny = y;
int anx = x - MmgScreenData.GetGameLeft() - myX;
int any = y - MmgScreenData.GetGameTop() - myY;
currentScreen.ProcessScreenClick(nx, ny);
}
public final void PrepBuffers() {
// Background & Buffer
background = create(winWidth, winHeight, false);
canvas.createBufferStrategy(2);
do {
strategy = canvas.getBufferStrategy();
} while (strategy == null);
backgroundGraphics = (Graphics2D) background.getGraphics();
}
// create a hardware accelerated image
public final BufferedImage create(final int width, final int height, final boolean alpha) {
return MmgBmpScaler.GRAPHICS_CONFIG.createCompatibleImage(width, height, alpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE);
}
/**
* Gets the set of game screens this class has references too.
*
* @return A Hashtable of game screens, MmgGameScreen.
*/
public final Hashtable<GameStates, MmgGameScreen=MmgGameScreen> GetGameScreens() {
return gameScreens;
}
/**
* Sets the set of game screens this class has references too.
*
* @param GameScreens A Hashtable of game screens, MmgGameScreen.
*/
public final void SetGameScreens(Hashtable<GameStates, MmgGameScreen=MmgGameScreen> GameScreens) {
gameScreens = GameScreens;
}
public final Canvas GetCanvas() {
return canvas;
}
/**
* Gets the current game screen.
*
* @return A game screen object, MmgGameScreen.
*/
public final MmgGameScreen GetCurrentScreen() {
return currentScreen;
}
/**
* Sets the current game screen.
*
* @param CurrentScreen A game screen object.
*/
public final void SetCurrentScreen(MmgGameScreen CurrentScreen) {
currentScreen = CurrentScreen;
}
/**
* Switches the current game state, cleans up the current state, then loads
* up the next state. Currently does not use the gameScreens hash table.
* Uses direct references instead, for now.
*
* @param g The game state to switch to.
*/
public final void SwitchGameState(GameStates g) {
MmgApiUtils.wr("Switching Game State To: " + g);
}
/**
* A generic event, GenericEventHandler, callback method. Used to handle
* generic events from certain game screens, MmgGameScreen.
*
* @param obj
*/
@Override
public final void HandleGenericEvent(GenericEventMessage obj) {
if (obj != null) {
MmgApiUtils.wr("HandleGenericEvent " + obj.GetGameState());
/*
if (obj.GetGameState() == GameStates.LOADING) {
if (obj.GetId() == ScreenLoading.EVENT_LOAD_COMPLETE) {
SwitchGameState(GameStates.MAIN_MENU);
}
} else if (obj.GetGameState() == GameStates.SPLASH) {
if (obj.GetId() == ScreenSplash.EVENT_DISPLAY_COMPLETE) {
SwitchGameState(GameStates.LOADING);
}
}
*/
}
}
private Graphics2D GetBuffer() {
if (graphics == null) {
try {
graphics = (Graphics2D) strategy.getDrawGraphics();
} catch (IllegalStateException e) {
return null;
}
}
return graphics;
}
public final int GetWinWidth() {
return winWidth;
}
public final int GetWinHeight() {
return winHeight;
}
public final int GetX() {
return myX;
}
public final int GetY() {
return myY;
}
private boolean UpdateScreen() {
graphics.dispose();
graphics = null;
try {
strategy.show();
Toolkit.getDefaultToolkit().sync();
return (!strategy.contentsLost());
} catch (Exception e) {
return true;
}
}
public final void UpdateGame() {
updateTick++;
prev = now;
now = System.currentTimeMillis();
// update game logic here
if (currentScreen != null) {
currentScreen.MmgUpdate(updateTick, now, (now - prev));
}
}
public final void RenderGame() {
if (PAUSE == true || EXIT == true) {
} else {
UpdateGame();
}
// Update Graphics
do {
bg = GetBuffer();
g = backgroundGraphics;
g2d = (Graphics2D) g;
//if (currentScreen == null || currentScreen.IsPaused() == true || currentScreen.IsReady() == false) {
//} else {
//clear background
g.setColor(Color.DARK_GRAY);
g.fillRect(0, 0, winWidth, winHeight);
//draw border
g.setColor(Color.WHITE);
g.drawRect(MmgScreenData.GetGameLeft() - 1, MmgScreenData.GetGameTop() - 1, MmgScreenData.GetGameWidth() + 1, MmgScreenData.GetGameHeight() + 1);
g.setColor(Color.BLACK);
g.fillRect(MmgScreenData.GetGameLeft(), MmgScreenData.GetGameTop(), MmgScreenData.GetGameWidth(), MmgScreenData.GetGameHeight());
p.SetGraphics(g2d);
p.SetAdvRenderHints();
//currentScreen.MmgDraw(p);
if (MmgApiUtils.LOGGING == true) {
tmpF = g.getFont();
g.setFont(debugFont);
g.drawString(GamePanel.FPS, 15, 15);
g.drawString("Var1: " + GamePanel.VAR1, 15, 35);
g.drawString("Var2: " + GamePanel.VAR2, 15, 55);
g.setFont(tmpF);
}
//}
// thingy
if (scale != 1) {
bg.drawImage(background, sMyX, sMyY, sWinWidth, sWinHeight, 0, 0, winWidth, winHeight, null);
} else {
bg.drawImage(background, myX, myY, null);
}
bg.dispose();
} while (!UpdateScreen());
}
}
First up we're going to get to know our class a bit.
/**
*
* @author Victor G. Brusca, Middlemind Games
*/
public class GamePanel implements GenericEventHandler {
/**
* An enumeration that lists all of the current game states.
*/
public enum GameStates {
LOADING,
BLANK,
SPLASH,
MAIN_MENU,
ABOUT,
HELP_MENU,
HELP_PROLOGUE,
HELP_ITEM_DESC,
HELP_ENEMY_DESC,
HELP_ITEM_DESC_ITEM_TEXT,
HELP_GAME_PLAY_OVERWORLD,
HELP_GAME_PLAY_BATTLE_MODE,
HELP_CHAR_DESC,
HELP_QUEST_DESC,
HELP_ROOM_DESC,
HELP_ROOM_DESC_ROOM_DETAILS,
MAIN_GAME,
SETTINGS
}
/**
* MainFrame that this panel is hosted in.
*/
private final MainFrame mf;
/**
* Window width.
*/
private final int winWidth;
/**
* Window height.
*/
private final int winHeight;
/**
* The X coordinate of this panel.
*/
private final int myX;
/**
* The Y coordinate of this panel.
*/
private final int myY;
/**
* Window width.
*/
private final int sWinWidth;
/**
* Window height.
*/
private final int sWinHeight;
/**
* The X coordinate of this panel.
*/
private final int sMyX;
/**
* The Y coordinate of this panel.
*/
private final int sMyY;
/**
* Default target game width.
*/
public static final int GAME_WIDTH = 854;
/**
* Default target game height.
*/
public static final int GAME_HEIGHT = 416;
/**
* Pause the game.
*/
public static boolean PAUSE = false;
/**
* Exit the game.
*/
public static boolean EXIT = false;
Right off the bat the is some useful information we can gather about this new class. For one it's called GamePanel, so that kind of gives us a clue
that this class is going to present a video game on some kind of 2D UI panel. Secondly, you should have noticed that the class implements the GenericEventHandler interface.
This means that is exposes a method that follows the GenericEventHandler template. You'll see how this is used in tutorial 3, when we get into game screens!!!
Next up we'll look at the basic class members. You can see there is one called GameStates. It would seem to be filled with states from someone else's game, how dare they.
I left those in there to illustrate that this class is going to be the generic game manager. That is in a general way it is going to keep track of what state the game is in,
and this includes what is being drawn as well as where events are being routed.
After the GameStates enumeration there is our old friend the MainFrame class. This tells us that the GamePanel class will have a reference
to the main frame that will be working with it. I quickly wanted to mention enumerations. Enumerations are a great way to keep track of finite state variables, think days of the week.
Something that isn't going to have hundreds of different values but also something that you don't want to just hard code. Enumerations are great for that and you can do some nifty things with them.
For more information on Java enumerations here. Next up we'll quickly review the dimension and positioning variables.
/**
* Window width.
*/
private final int winWidth;
/**
* Window height.
*/
private final int winHeight;
/**
* The X coordinate of this panel.
*/
private final int myX;
/**
* The Y coordinate of this panel.
*/
private final int myY;
/**
* Window width.
*/
private final int sWinWidth;
/**
* Window height.
*/
private final int sWinHeight;
/**
* The X coordinate of this panel.
*/
private final int sMyX;
/**
* The Y coordinate of this panel.
*/
private final int sMyY;
/**
* Default target game width.
*/
public static final int GAME_WIDTH = 854;
/**
* Default target game height.
*/
public static final int GAME_HEIGHT = 416;
/**
* Pause the game.
*/
public static boolean PAUSE = false;
/**
* Exit the game.
*/
public static boolean EXIT = false;
You'll notice that we have two sets of variables for the window dimensions and the drawing coordinate associated with them. The use of these variables will become apprent
shortly when we cover the class's methods. You can also see that there is a final set of dimensions for target game width and height. And last but not least there are some control
booleans PAUSE and EXIT. So for the most part the variables covered so far are higher level variables used to keep track of what is being drawn and where it is being drawn.
The next set of variables will have more to do with the graphics layer. Just to paint a picture of what's happening, the MainFrame class of ours plugs into a Java graphics library called
Swing. Java Swing takes care of rendering a GUI window for our game foundation code, and the MainFrame is the higher level Java Swing class used by the GUI windowing system.
Our GamePanel class is the canvas our game will be drawn on, and it will take care of tracking the current game state and routing events to the correct event handler.
The GamePanel class is aware of the MainFrame owner and it exposes a Canvas that is part of the Java Swing GUI library. So we have this sort of shared feature here.
The MainFrame is in charge of drawing the Canvas with regard to the Java Swing API but the GamePanel class is in charge of updating the Canvas with the current game
a few times a second. We're going to get more into this and double buffering next. A few more variables have to be covered first though. The code snippet below has been re-ordered from it's
original form to better group the variables.
/**
* Holds a reference to all game screens.
*/
private Hashtable<GameStates, MmgGameScreen=MmgGameScreen> gameScreens;
/**
* The current game screen being displayed.
*/
private MmgGameScreen currentScreen;
/**
* Game state helper class, the previous game state.
*/
private GameStates prevGameState;
/**
* Game state helper class, the current game state.
*/
private GameStates gameState;
public static Hashtable<String, Object=Object> VARS = new Hashtable();
public static String FPS = "Drawing FPS: 0000 Actual FPS: 00";
public static String VAR1 = "** EMPTY **";
public static String VAR2 = "** EMPTY **";
public static GameType GAME_TYPE = GameType.NEW_GAME;
/**
* Paint helper class, used to draw Mmg API objects.
*/
private final MmgPen p;
/**
* Paint helper class, used in the paint drawing routine.
*/
private Graphics2D g2d;
private Graphics2D backgroundGraphics;
private Graphics2D graphics;
private Graphics2D bg;
private Graphics2D g;
private Canvas canvas;
private BufferStrategy strategy;
private BufferedImage background;
private final double scale = 1;
private final Font debugFont;
private Font tmpF;
private int updateTick = 0;
private long now;
private long prev;
public int lastX;
public int lastY;
public long lastKeyPressEvent = -1;
public enum GameType {
NEW_GAME,
CONTINUED_GAME
}
We're going to be exposed to some new classes, not all of which we'll have time to cover here. The first line of the remaining variable entries is,
private Hashtable<GameStates, MmgGameScreen=MmgGameScreen> gameScreens. A Hashtable is a data structure used to store dynamic data.
It allows you to add new entries into the table without having to know exactly what you're adding ahead of time. You may be thinking that this sounds less efficient than if we had just
hardcoded an array to the list of game states. If so, you'd be right. There will be many cases in game programming where you could choose to implement something in a
less dynamic faster way via hardcoding and arrays, or use data structures like lists and tables because they are more dynamic. The answer will depend on your code design and
what you're trying to do. The main thing here is to be aware that less advanced code implementations have their place in game development if they are more efficient.
For a quick review of how a Hashtable is used go here.
The next class member we see is our currentScreen it is also an instance of the MmgGameScreen class, and you'll notice that out gameScreens hash table
takes an MmgGameScreen object and associates it with an instance of our GameStates enumeration. So a lot is going on here even though it seems simple and direct.
The gameScreens member is designed to associate a game screen with a given game state. Starting to sound familiar? This is how our GamePanel instance will
help keep track of the state of the game.
Next up we see two variables gameState and prevGameState, hmm seems like we could do something like this with our class members so far, currentScreen = gameScreens.get(this.gameState).
Ahhh, such a thing of beauty. The next four class members are variables for debugging. The first gives us a dynamic centralized place to get and set text.
This VARS hash table can be used to pass along information for debugging and logging. The next three strings FPS and VAR1, VAR2 are for debugging
some foundational information and also to provides quick debugging in the form of printing out the contents of VAR1 and VAR2. Finally we have our gameType, which is an instance of an enumaration
and represents what type of game we're playing a new game or a continued game. A lot of these variables we're not going to be using in this tutorial but it's important you begin to understand the
design of the code.
Remember the variable listing above is slightly out of order when compared with the original project to group variables we want to talk about together, well, together.
The next set of variables in this class have a lot to do with the underlying drawing classes provided by Java's Swing API. Let's take a look at our MmgPen instance p.
We'll have to only cover some of these classes in brief here, but we'll get more familiar with them in later tutorials. The MmgPen class wraps the underlying Java Swing Pen class.
Basically we abstract the functionality of the lower level Pen class because we want to hide exactly how it works. An example would be porting our MmgPen class to Android.
The underlying drawing classes won't be the same as in Java Swing but you can bet there will be similar functionality like drawing lines, images, fonts, etc. So if you could imagine we did complete such a
port of our class then we don't really care how the underlying drawing routines work we just want our MmgPen to hide that from us and expose the same methods. This way we can use that class the same way on Java or
on Android. The significance of the MmgPen class is that it does the drawing for us. You can find out more about the MmgPen class here.
private Graphics2D g2d;
private Graphics2D backgroundGraphics;
private Graphics2D graphics;
private Graphics2D bg;
private Graphics2D g;
This part is pretty straight forward the Graphics2D instances shown above are classes that contain our graphics context. They basically allow a Pen to draw on an image within the Java Swing API.
Makes sense? If you have an image you want to draw you need a Pen to draw it but you need to know where on the paper, what kind of paper, etc, to draw. These variables are private because they are most likely
class instances that we don't want garbage collected. We will see in more detail how these variables are used in our class.
private Canvas canvas;
private BufferStrategy strategy;
private BufferedImage background;
Our Canvas class instance canvas, big surprise there, is a Java Swing class that allows a blank canvas to be plugged into the Java Swing windowing classes we mentioned earlier.
We are essentially registering a blank canvas Java Swing control with our Java Swing window so that we can draw our game on that blank canvas. The next variable has to do with lower level Java Swing API calls, think of it
as a way to represent what kind of image buffering strategy we want to use when we create new buffered images, the use of buffer here indicates that these images exist in memory as images before they are used by our game.
We can gloss over the strategies for now, the BufferedImage instance background is an important class variable. It's going to be the main image that we draw on. It's where we will render one frame of our game.
Then that image will be drawn on the canvas in one step. This is a kind of double buffering.
Double buffering is a technique that came into use in the old days of programming. You see if you draw your frame directly
to the video memory, in peices as the frame is being rendered, you run the risk of flickering, as the drawing and the display hardware interact. To prevent this, the game's next frame is drawn to memory and is only drawn onto the screen,
in this case our canvas, when it's ready. This lowers or prevents that flicker we mentioned earlier. The remaining variables in the class are internal variables for drawing debugging strings, keeping track of some user interface events,
etc. We'll see the important ones in action as we go over the important methods in our GamePanel class.
/**
* Constructor, sets the MainFrame, window dimensions, and position of this
* JPanel.
*
* @param Mf The MainFrame class this panel belongs to.
* @param WinWidth The target window width.
* @param WinHeight The target window height.
* @param X The X coordinate of this JPanel.
* @param Y The Y coordinate of this JPanel.
*/
@SuppressWarnings({"LeakingThisInConstructor", "OverridableMethodCallInConstructor"})
public GamePanel(MainFrame Mf, int WinWidth, int WinHeight, int X, int Y) {
mf = Mf;
winWidth = WinWidth;
winHeight = WinHeight;
sWinWidth = (int) (winWidth * scale);
sWinHeight = (int) (winHeight * scale);
myX = X;
myY = Y;
sMyX = myX + (winWidth - sWinWidth);
sMyY = myY + (winHeight - sWinHeight);
now = System.currentTimeMillis();
prev = System.currentTimeMillis();
canvas = new Canvas(MmgBmpScaler.GRAPHICS_CONFIG);
canvas.setSize(winWidth, winHeight);
MmgApiUtils.wr("GamePanel Window Width: " + winWidth);
MmgApiUtils.wr("GamePanel Window Height: " + winHeight);
MmgApiUtils.wr("GamePanel Offset X: " + myX);
MmgApiUtils.wr("GamePanel Offset Y: " + myY);
MmgScreenData screenData = new MmgScreenData(winWidth, winHeight, GamePanel.GAME_WIDTH, GamePanel.GAME_HEIGHT);
MmgApiUtils.wr(MmgScreenData.ToString());
MmgFontData fontData = new MmgFontData();
MmgApiUtils.wr(MmgFontData.ToString());
debugFont = MmgFontData.CreateDefaultFontSm();
p = new MmgPen();
MmgPen.ADV_RENDER_HINTS = true;
gameScreens = new Hashtable(20);
gameState = GameStates.BLANK;
SwitchGameState(GameStates.SPLASH);
canvas.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseDragged(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
}
@Override
public void mouseMoved(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
}
});
canvas.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
//MmgApiUtils.wr("KeyTyped: " + e.getKeyCode() + " KeyChar: " + e.getKeyChar() + " X: " + lastX + " Y: " + lastY);
}
@Override
public void keyPressed(KeyEvent e) {
//MmgApiUtils.wr("KeyPressed: " + e.getKeyCode() + " KeyChar: " + e.getKeyChar());
}
@Override
public void keyReleased(KeyEvent e) {
//MmgApiUtils.wr("KeyReleased: " + e.getKeyCode() + " KeyChar: " + e.getKeyChar());
}
});
canvas.addMouseListener(new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
ProcessClick(e.getX(), e.getY());
}
@Override
public void mousePressed(MouseEvent e) {
ProcessPress(e.getX(), e.getY());
}
@Override
public void mouseReleased(MouseEvent e) {
ProcessRelease(e.getX(), e.getY());
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
});
}
Let's take a look at the constructor of our GamePanel class below. Take a moment to read through it before we go over the individual pieces.
mf = Mf;
winWidth = WinWidth;
winHeight = WinHeight;
sWinWidth = (int) (winWidth * scale);
sWinHeight = (int) (winHeight * scale);
We set local references to the MainFrame class and store the WinWidth and WinHeight variables. This is straight forward, we need a reference to the MainFrame
class because we want connectivity between those two pieces of the game. The window height and window width are drawing instructions passed in from our lower level static main, and its
configuration loading code (remember?). There is a feature to the drawing routines here that we are not going to use. You'll notice there is a scale variable in this class. The value should be set to one to turn off scaling.
The scaling done here would be done at the highest level in the game after the game frame has been rendered. Which isn't terrible, but it's usually more efficient to scale the individual assets once on load.
And draw the game as usual, unscaled, rather then scale each frame. It is kind of a cool feature to have so I left the code in there and kind of just turned it off.
myX = X;
myY = Y;
sMyX = myX + (winWidth - sWinWidth);
sMyY = myY + (winHeight - sWinHeight);
now = System.currentTimeMillis();
prev = System.currentTimeMillis();
Here we store local copies of the target X and Y position we want our GamePanel drawn. Again notice the scaling offset calculation based on the scaled dimensions calculated earlier.
The values for the scaled position offset should be the same as the non scaled offsets because all the dimensions are the same, so (winWidth - sWinWidth) is equal to zero. The next two lines of code
just initialize the time tracking variables to a value, no big deal.
canvas = new Canvas(MmgBmpScaler.GRAPHICS_CONFIG);
canvas.setSize(winWidth, winHeight);
MmgApiUtils.wr("GamePanel Window Width: " + winWidth);
MmgApiUtils.wr("GamePanel Window Height: " + winHeight);
MmgApiUtils.wr("GamePanel Offset X: " + myX);
MmgApiUtils.wr("GamePanel Offset Y: " + myY);
The next few lines instantiate our canvas variable and set it's dimensions to the values passed into our constructor. We also set the graphics configuration for the Canvas object
when we instantiate it. This is like telling the canvas class how we plan to draw on it so it can be ready to do what we need it to do.
MmgScreenData screenData = new MmgScreenData(winWidth, winHeight, GamePanel.GAME_WIDTH, GamePanel.GAME_HEIGHT);
MmgApiUtils.wr(MmgScreenData.ToString());
MmgFontData fontData = new MmgFontData();
MmgApiUtils.wr(MmgFontData.ToString());
debugFont = MmgFontData.CreateDefaultFontSm();
p = new MmgPen();
MmgPen.ADV_RENDER_HINTS = true;
gameScreens = new Hashtable(20);
gameState = GameStates.BLANK;
SwitchGameState(GameStates.SPLASH);
Here is some interesting code. We're instantiating a quirky class called MmgScreenData, more information here.
We instantiate it but don't keep the instance. This is because the internal of the MmgScreenData class are static, so we don't really need to keep a copy floating around right now.
We can see that the screen data class is tracking the dimensions of the actual canvas class as well as the dimensions of the window, GamePanel class. The debugging call at the end
of the screenData initialization lines will give us an indication of what that class is doing. I'll post a copy of the output a little later in this tutorial. After the screen data section we
have some font stuff going on. We instantiate a copy of the game APIs font class and use it to create a basic small sized font. We keep a local copy of this font in our variable debugFont.
We'll be using debugFont to draw some debug data to the GUI output of our little game foundation code. Coming up next we instantiate and store our local MmgPen, we'll be using this for drawing
our game frame. The advanced render hints boolean is optional, but I think I have it set to the best setting for a 2D windowed Java game. Next up we instantiate our gameScreens class, expecting around
20 different game states. We set the current game state to a null state, and then we switch the game into the splash screen state, although this function doesn't do anything just yet.
canvas.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseDragged(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
}
@Override
public void mouseMoved(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
}
});
canvas.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
//MmgApiUtils.wr("KeyTyped: " + e.getKeyCode() + " KeyChar: " + e.getKeyChar() + " X: " + lastX + " Y: " + lastY);
}
@Override
public void keyPressed(KeyEvent e) {
//MmgApiUtils.wr("KeyPressed: " + e.getKeyCode() + " KeyChar: " + e.getKeyChar());
}
@Override
public void keyReleased(KeyEvent e) {
//MmgApiUtils.wr("KeyReleased: " + e.getKeyCode() + " KeyChar: " + e.getKeyChar());
}
});
canvas.addMouseListener(new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
ProcessClick(e.getX(), e.getY());
}
@Override
public void mousePressed(MouseEvent e) {
ProcessPress(e.getX(), e.getY());
}
@Override
public void mouseReleased(MouseEvent e) {
ProcessRelease(e.getX(), e.getY());
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
});
The output from the MmgScreenData class shows that it's mainly used to track positioning of the game window within a larger screen.
ScaleX: true ScaleY: true
Found X Scale: 1.0, ResF: 32.0, ResI: 32.0, Diff: 0.0, Count: 0
Screen Width: 854
Screen Height: 596
Game Width: 854
Game Height: 416
Game Offset X: 0
Game Offset Y: 90
Scale X: 1.0
Scale Y: 1.0
Font Size: 22
Target Pixel Height (Unscaled): 22
Target Pixel Height (Scaled): 22
Last but not least, event handling! While we aren't going to go into details here, you can see that we register the event handlers against our Java Swing API Canvas class instance.
Event handling, like graphics drawing is handled at the lower level GUI API, in this case Java Swing, but we have to "bubble" up that functionality to our game API so that we can take advantage
of it as part of our game. That is where the game API comes into play, it provides classes like MmgPen, MmgFont, MmgBmp that wrap and abstract the lower level underlying
GUI API. This enables us to make a portable library for making games. As long as we implement our API the same way on a new platform, Android anyone?, we can use it the same exact way.
The event handler registrations will pass mouse and keyboard events into local GamePanel methods which will be routed to the currentScreen for handling in our game.
public final void PrepBuffers() {
// Background & Buffer
background = create(winWidth, winHeight, false);
canvas.createBufferStrategy(2);
do {
strategy = canvas.getBufferStrategy();
} while (strategy == null);
backgroundGraphics = (Graphics2D) background.getGraphics();
}
// create a hardware accelerated image
public final BufferedImage create(final int width, final int height, final boolean alpha) {
return MmgBmpScaler.GRAPHICS_CONFIG.createCompatibleImage(width, height, alpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE);
}
I'd be remiss in my duty as a tutorialist, is that a thing?, if I didn't skip over all the event handling hooks and jump right to some of the finer points of how the image buffers are created.
If you look closely you can see that our background buffered image is created with a call to create, which uses MmgBmpScaler.GRAPHICS_CONFIG to initialize a compatible
bitmap image for drawing. Remember we used MmgBmpScaler.GRAPHICS_CONFIG when we instantiated our canvas class? See how the graphics configuration is being used any time we need to
create a class that deals with drawing? Now remember we mentioned double buffering previously? Notice we are setting the canvas buffer strategy to 2, 2 buffers... double buffers... get it? Lol.
The loop following the line setting the buffer strategy is for making sure that each buffer is ready, and looping a little until it is. Finally we grab an instance of the graphics context associated with
our background image, this is a Graphics2D instance and is used as a graphics context for a Pen or MmgPen, inside the class, to use when drawing.
/**
* A generic event, GenericEventHandler, callback method. Used to handle
* generic events from certain game screens, MmgGameScreen.
*
* @param obj
*/
@Override
public final void HandleGenericEvent(GenericEventMessage obj) {
if (obj != null) {
MmgApiUtils.wr("HandleGenericEvent " + obj.GetGameState());
/*
if (obj.GetGameState() == GameStates.LOADING) {
if (obj.GetId() == ScreenLoading.EVENT_LOAD_COMPLETE) {
SwitchGameState(GameStates.MAIN_MENU);
}
} else if (obj.GetGameState() == GameStates.SPLASH) {
if (obj.GetId() == ScreenSplash.EVENT_DISPLAY_COMPLETE) {
SwitchGameState(GameStates.LOADING);
}
}
*/
}
}
Oh man, we're almost at the main drawing methods,but first, we have to cover our generic event handler. If you look closely at the GamePanel class declaration you'll notice this little
piece of information, implements GenericEventHandler, which if you recall from earlier tutorials means that our GamePanel class has to implement a specific set of methods to satisfy the requirement
that it implements GenericEventHandler. Which methods do we need to implement you might ask? Let's take a look at our documentation here.
Well lo and behold we have our generic event handling method implemented above. I even left some old game code in there so you can see how it might be used. You can see that it is a simple, general event hub that reacts to events
fired from certain game screens, you can think of game states and game screens as the same thing. Next up, our main drawing methods and what is the basis for our game loop.
private boolean UpdateScreen() {
graphics.dispose();
graphics = null;
try {
strategy.show();
Toolkit.getDefaultToolkit().sync();
return (!strategy.contentsLost());
} catch (Exception e) {
return true;
}
}
public final void UpdateGame() {
updateTick++;
prev = now;
now = System.currentTimeMillis();
// update game logic here
if (currentScreen != null) {
currentScreen.MmgUpdate(updateTick, now, (now - prev));
}
}
public final void RenderGame() {
if (PAUSE == true || EXIT == true) {
//do nothing
} else {
UpdateGame();
}
// Update Graphics
do {
bg = GetBuffer();
g = backgroundGraphics;
g2d = (Graphics2D) g;
//if (currentScreen == null || currentScreen.IsPaused() == true || currentScreen.IsReady() == false) {
//} else {
//clear background
g.setColor(Color.DARK_GRAY);
g.fillRect(0, 0, winWidth, winHeight);
//draw border
g.setColor(Color.WHITE);
g.drawRect(MmgScreenData.GetGameLeft() - 1, MmgScreenData.GetGameTop() - 1, MmgScreenData.GetGameWidth() + 1, MmgScreenData.GetGameHeight() + 1);
g.setColor(Color.BLACK);
g.fillRect(MmgScreenData.GetGameLeft(), MmgScreenData.GetGameTop(), MmgScreenData.GetGameWidth(), MmgScreenData.GetGameHeight());
p.SetGraphics(g2d);
p.SetAdvRenderHints();
//currentScreen.MmgDraw(p);
if (MmgApiUtils.LOGGING == true) {
tmpF = g.getFont();
g.setFont(debugFont);
g.drawString(GamePanel.FPS, 15, 15);
g.drawString("Var1: " + GamePanel.VAR1, 15, 35);
g.drawString("Var2: " + GamePanel.VAR2, 15, 55);
g.setFont(tmpF);
}
//}
// thingy
if (scale != 1) {
bg.drawImage(background, sMyX, sMyY, sWinWidth, sWinHeight, 0, 0, winWidth, winHeight, null);
} else {
bg.drawImage(background, myX, myY, null);
}
bg.dispose();
} while (!UpdateScreen());
}
We're going to start with the RenderGame method. This is the main entry point to our game loop. You may be wondering how the game loop is going to be called?
Infinite for loop? We'll be covering that in the next tutorial. As we look over the RenderGame method you can see that the
method does nothing if either the EXIT or PAUSE variables return true. This is because we want the this method to exit quickly and do no work if EXIT or PAUSE is true,
this will allow us to break out of our game loop cleanly so we can exit the game or pause it cleanly if we need to stop the game without closing it.
Now if EXIT and PAUSE are both false then we want to run our update method called UpdateGame. You guessed it, this is the main update call. The current screen in the game
gets a chance to update itself, for instance move game graphics around, test collisions, etc. We keep track of the number of times update is called and pass that in to the current screen's
MmgUpdate method. The MmgUpdate method is an MMG API level call, it connects our game API to the base of our game loop. You'll notice that the time in between update calls is tracked.
And the difference between those times is also tracked. You can see here, currentScreen.MmgUpdate(updateTick, now, (now - prev)), that the current screen's MmgUpdate method gets the unique update index,
the current time in milliseconds, and the difference in milliseconds between this update call and the last update call. This information can be used by game screens to help determine how much time has passed in between
update calls and to animate graphics accordingly.
bg = GetBuffer();
g = backgroundGraphics;
g2d = (Graphics2D) g;
//if (currentScreen == null || currentScreen.IsPaused() == true || currentScreen.IsReady() == false) {
//do nothing
//} else {
//clear background
g.setColor(Color.DARK_GRAY);
g.fillRect(0, 0, winWidth, winHeight);
//draw border
g.setColor(Color.WHITE);
g.drawRect(MmgScreenData.GetGameLeft() - 1, MmgScreenData.GetGameTop() - 1, MmgScreenData.GetGameWidth() + 1, MmgScreenData.GetGameHeight() + 1);
g.setColor(Color.BLACK);
g.fillRect(MmgScreenData.GetGameLeft(), MmgScreenData.GetGameTop(), MmgScreenData.GetGameWidth(), MmgScreenData.GetGameHeight());
p.SetGraphics(g2d);
p.SetAdvRenderHints();
//currentScreen.MmgDraw(p);
if (MmgApiUtils.LOGGING == true) {
tmpF = g.getFont();
g.setFont(debugFont);
g.drawString(GamePanel.FPS, 15, 15);
g.drawString("Var1: " + GamePanel.VAR1, 15, 35);
g.drawString("Var2: " + GamePanel.VAR2, 15, 55);
g.setFont(tmpF);
}
//}
// thingy
if (scale != 1) {
bg.drawImage(background, sMyX, sMyY, sWinWidth, sWinHeight, 0, 0, winWidth, winHeight, null);
} else {
bg.drawImage(background, myX, myY, null);
}
bg.dispose();
This last little bit is where all the magic happens. The drawing buffer is retrieved, and a Graphics2D object is casted from the graphics paramter.
The buffer is then cleared with a dark grey color. There is some code in place for drawing a border around the game screen area.
These are done with lower level graphics calls because they are run every frame of the game. We want them to be as quick as possible. We could have used
equivalent calls at the MMG API level but these would just be wrappers around the lower level calls anyway. Next up we set our MMG API pen to have access
to the lower level graphics object. This is where the MMG API will take over. I left the call commented out but you can see that the next step is to ask the current
game screen to draw itself. After that there are a few debugging calls, these allow us to write some data to the game while we play it.
Build and run your project at this point. And bam! You should see the above window with an actual frame rate (thread sleep),
a theoretical maximum frame rate (no thread sleep), and two variable slots for debugging. Ready to make some games?!