Monday 23 March 2015

Using Java & Eclipse to Write a Game - Pt 1

In this series, I'm going to use Java to produce a simple 2D dungeon crawl type game, animating sliding counters from square to square, rolling random values to determine results and implementing a simple fog of war... Players start off as a lone adventurer and move through the randomly generated dungeon.  Very simple, and step by step, we'll build this game in Java.

First things first, the development environment, I'm going to use Java SE Development Kit 8u40 (the latest one at the time of writing)... My download for this comes from here:


The IDE I'm going to use is Eclipse for Java, so this the LUNA release from the Elipse website:


When I first start Eclipse, it's going to ask where my workspace it, I'm going to put this into a specific path:


As it first opens we're going to be into the Eclipse welcome screen, so we just need to switch back to the workbench to get something done..


Our next job is going to be creating the new project we're going to work upon:


And then with the settings we're going to leave it all alone and just enter the name of "dungeon", in lower case:


With that name entered, at the bottom, we select "next" and we are shown the source code root point, which will be "dungeon" with "src" as the first folder.  At the bottom again we can also see "Default output Folder" our java classes will end up in that folder later.


Next, below the "src" folder I want to add folders which represent different parts of our code, so I'm going to right click on the "src" in the Package Explorer, and add a new folder called "display", the name in lower case again.

Inside this "display" folder, I'm now following up to add a new Java Class, so I'm setting the source folder to /dungeon/src/display.

Next, I'm setting the superclass to be the java.awt.Canvas, as this is the class we want to extend from, and we also want to interact with the mouse and window events, so I'm adding those listeners as interfaces:


I also opt to add the public static void main to this class and I select finish.

Once the code arrives, I'm going to go into the Project menu and turn off "Build Automatically" as that doesn't support my style of coding.

However, Immediately, and poorly for this tutorial I made a mistake, the package for the MainWindow.java should read:

package display.display;

At the top of the file.  This should then build, but the next problem is the style of the code, I personally hate K&R style, I much prefer Allman style.


Therefore, we need go into the Project properties, and then into the Java Code Style settings and select "Enable project specific settings", and then use "Edit":


Once in there, I select the braces tab and set everything thus:


Now to do a test of the code itself, lets add "Hello World", and build, then run the application... You should see this now in your main function, and below the "Terminated" program with just the output text we expected:


In reality, as per our ols Bubbles java example, all our main function is going to do is create an instance of the MainWindow class, and run that instance:


Changing our main function, we can then create the "Run" function, and inside this we add the need for two new things, the first is a function to "Yeild", this is called once each time through the loop and is going to be set out to stop the application taking up all the CPU available on our machine (so avoiding locking the machine into our loop whilst running).

And we'll also need a boolean flag to indicate the game loop is still running.


For Yield, we're just going to sleep the thread:


If we build this now we have no problems, but we do have an issue if we run the application, we go into a loop and sit there, but we see nothing on screen... Meaning we have no way of exiting the application...

What we need next is to create a constructor for our MainWindow, which actually puts the game window on screen.


Our constructor is a new function, without a return type... And it has the same name as the Class, so it's called "MainWindow" highlighted above with the blue box.

We set our constructor private here to stop anyone other than the functions inside the class itself calling the constructor... (if it were public, anyone could call and create a "MainWindow", but we don't want that, we only want our program to create one).

The "CreateWindow" function we call in the constructor, then just creates a new javax.swing.JFrame instance; which we've added as a member variable called "m_WindowFrame"; it sets the Window Frame visible, but most importantly it sets "this" as the Window Listener.

The Window Listener is what is going to receive the events from the window, so when the user say closes the window we can receive the message to that effect and do something.

Looking back at our code, we "implemented" the java.awt.events.WindowListener" as part of our class creation, and that gave us lots of little "@override" functions.

One of those is "windowClosing", inside of which we need to set something to exit the game loop... By settings the m_GameRunning boolean to false, which may look like this:


However, if we run this code, how do we know we were inside the "Run" loop?... Simply we don't.

Lets change our code:


Running this now, lets see what the console says...


Hmm, we saw "Hello World", we saw run start and then exit... But the window is still open?

So our loop opened and closed instantly, why did it do this?

Well, we never initialised "m_GameRunning", it was never set to a value, it's just blank or zero.  Which happens to mean "false" for a boolean.

In other languages, such as C++, this would be warned to you by the compiler, that you're using "m_GameRunning" without initialising it.  Other languages, like C# can actually error and stop compilation when you do this.  However, in the current settings in Eclipse with Java, it's allowed it and we have run into our first bug...

Now we could fix this two ways, one is to initialise the variable in the constructor - I would insist on this if this were C++ - however in Java here I don't like this idea, I would much prefer to set (and potentially reset) the m_GameRunning flag to "true" as the first line of the "Run" function.

We called "Run", therefore the game is running...


Building and running this application now, we can see the Run function reports starting, but does not exit... The Window is open....


If we close the window now, it sets the flag, which causes the loop to exit:


Lets make this window more useful, first of all, it opens as a tiny slit, we need some space into which we can draw.  Now I'm going to start it out as 800x600 pixels, so I'll define two constant (or final) values in the code to represent these widths:

Running this now, we see the window has gotten large and is a big grey area in the middle.

What we actually need in that middle is the Canvas... What Canvas?... Well our actual "MainWindow" class is defined to be an extension of java.awt.Canvas, so when we're talking about "this" class we need to add it to the Window Frame...

Just wrap your head around that a moment, the class we're running is a Canvas, and we're going to add it onto the main area of a JFrame window we just opened... It's a self reference, to make our drawing and controlling of drawing much more simple later.


So, the panel we added the canvas to was just the Frame's already existing content pane.  Running the code now, the window is still just a big grey area?... So how do we make it a colour?  Say Blue?

Well, the first thing we need do is create a buffering strategy, what do we mean by buffering?  Well, when we look at the frame we are looking at a buffered image of the control... If we directly drew graphics to that visible area we'd see it flickering and flashing, because actually drawing things is slow... Much better is to draw things to somewhere off screen, and then in one quick exchange draw the whole completed image to the screen.

This is simply called "double buffering", there is going to be one off screen buffer, to which we draw, and then a second on screen buffer wchich we see.  Drawing to the off screen and then flipping or presenting to the on screen buffer in a single stroke.

This will smooth out the perceived quality of the drawing on screen, there will be no flickering and we can start to control how things look.


So the actual code going into the "CreateBufferingStrategy" is:

Traditionally, other programmers, would remember there is a window event here called "paint", if the JFrame we created gets the "Paint" event then it's going to draw something on screen.  However, as we've now got a buffering strategy we want to remove that call...


So, how do we now make a call to draw something?

Well, we jump to our running loop in "Run" and from our strategy eacn time through the loop we ask it for a graphics area which we can draw with... The Buffering Strategy then handles everything else for us, it will swap the finished drawing to the screen and everything.... Neato...


These four lines of code then, get us the graphics content, set the colour to blue, fill the whole canvas "this" with colour and then we call to show this frame.

We don't need to worry about blitting, or copying, we just "Show" what we did, very simple, very neat.

We're not quite through though... Let us add a simple boolean flag, and if this flag is on we clear everything blue, and if not we clear everything yellow... Like this:


So, logically the code should show blue, then 10 milliseconds later show yellow, then blue again and yellow and so on right?... Right?...

Nope....

As we can see in this video, and see better if you run the code, sometimes it's blue flashing yellow, sometimes yellow flashing blue, sometimes there appears to be a long gap between changes, sometimes it's so fast to change you could almost think it looks green (not)... Why is this?

Well, if we didn't have the yield function, it would be that the whole program was always running, stopping anything else on the machine getting any time.

As such that would be bad, but the colours would animate more, but not perfectly... We added the yield, but the gaps are not a perfect 10 milliseconds.... Why is that?

Well, on windows, as well as MacOS and Linux when we "sleep" a thread we don't say to the system "leave my code now and do something else for X amount of time", what we actually say is "leave my code and come back sometime after X amount of time".

The difference is subtle and often lost on rookie programmers, they use sleep and think their code will come back at exactly that amount of time.  This feeling in older programmers is usually born from their; like me; starting out on single thread, single application systems.  Where the machine was only doing the one task you assigned it, so it would go away and come back nearly always after the exact right amount of time; the machine had nothing else to do!

But with windows and other modern machines have many things to do at the same time, hundreds if not thousands of processes could be twittering away on your machine, taking more or less time each cycle, so something inside the operating system, called the scheduler, has to manage switching between tasks to give each a little bit of time to run.

Without the yield your program would hog the system and still act badly as critical parts of the system had to come and run, with your yield you are at the mercy of the varying level of business in the machine.

The challenge therefore is only to draw after a set interval, and that interval be long enough as to smooth out the task switching, but short enough to fool the human eye that we're smoothly animating or drawing.... This is called the frame rate, and we're going to stick to a target of 30 frames per second, of rendering every 33.3 milliseconds.


With this code we are going to sleep between 2 and 4 times between each frame, this does end up smoothing the rate at which we're drawing out, however it's not perfect, because we sleep for 10 milliseconds, this is quite a large proportion of the 33 milliseconds target, so we could be half way through our fourth sleep (for an unknown length of time) when we tally a total of 33 milliseconds or more... This means some frames maybe delayed still.

To smooth this over, we need to lower the yield, to a smaller value.  This is called the "granularity" of the sleep, think of an ege timer with sand above and below the glass pinch point, depending on how big the grains of sand are they will will flow more smoothly if they are smaller...

We can't change the pinch point (which is really the rate of the scheduler in the operating system) so we much change our sand... Lets try 2 millisecond sleeps.


This helps a lot, and smooths things right out... We now check if it's time nearly 5 times as often as before, but still we yield time on the machine for other programs.

This completes our first part of this tutorial, below is the whole code so far, see you next time.

----------

package display.display;

import java.awt.Canvas;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;

public class MainWindow extends Canvas implements MouseListener, WindowListener 
{
// The minimum Resolution Values
private final int c_MinWidth = 800;
private final int c_MinHeight = 600;
// The master game loop exit flag
private boolean m_GameRunning;
// The window frame
private javax.swing.JFrame m_WindowFrame;
// The drawing strategy (refreshing)
private java.awt.image.BufferStrategy m_Strategy;
// Constructor - can be private, we only internal
// members of this class (i.e. main) will be calling
// to create a main window.
private MainWindow ()
{
CreateWindow();
AddPanelAndCanvas();
CreateBufferingStrategy();
}
// Create the buffering strategy
private void CreateBufferingStrategy()
{
this.setIgnoreRepaint(true);  // Ignore the windows paint event
this.createBufferStrategy(2); // Two buffers, one on and one off screen
m_Strategy = this.getBufferStrategy();
}
// Adds a panel to the JFrame, and this panel
// fills the whole area of the window, into that
// we then add our Canvas, which is "this"
private void AddPanelAndCanvas()
{
// The panel in the middle area
javax.swing.JPanel l_Panel = (javax.swing.JPanel)m_WindowFrame.getContentPane();
l_Panel.setPreferredSize(new java.awt.Dimension(c_MinWidth, c_MinHeight));
l_Panel.setLayout(null);
// Set our Canvas properties of size
this.setBounds(0,  0, c_MinWidth, c_MinHeight);
// Add the canvas to the panel
l_Panel.add(this);
}
// Create the Window - we're going to use Swing
private void CreateWindow()
{
m_WindowFrame = new javax.swing.JFrame("Dungeon"); // This is the window title text 
m_WindowFrame.addWindowListener(this); // Adding "this" to the frame
// means our MainWindow code as
// it is running will receive the
// Window events, like close & move
m_WindowFrame.setSize(c_MinWidth, c_MinHeight);
m_WindowFrame.setVisible(true);
}
@Override
public void windowActivated(WindowEvent arg0) 
{
// TODO Auto-generated method stub

}

@Override
public void windowClosed(WindowEvent arg0) 
{
// TODO Auto-generated method stub

}

@Override
public void windowClosing(WindowEvent arg0) 
{
// TODO Auto-generated method stub
m_GameRunning = false;
}

@Override
public void windowDeactivated(WindowEvent arg0) 
{
// TODO Auto-generated method stub

}

@Override
public void windowDeiconified(WindowEvent arg0) 
{
// TODO Auto-generated method stub

}

@Override
public void windowIconified(WindowEvent arg0) 
{
// TODO Auto-generated method stub

}

@Override
public void windowOpened(WindowEvent arg0) 
{
// TODO Auto-generated method stub

}

@Override
public void mouseClicked(MouseEvent arg0)
{
// TODO Auto-generated method stub

}

@Override
public void mouseEntered(MouseEvent arg0)
{
// TODO Auto-generated method stub

}

@Override
public void mouseExited(MouseEvent arg0)
{
// TODO Auto-generated method stub

}

@Override
public void mousePressed(MouseEvent arg0)
{
// TODO Auto-generated method stub

}

@Override
public void mouseReleased(MouseEvent arg0)
{
// TODO Auto-generated method stub

}
/// The function we "run" from the main
/// function, to go into a loop, rendering
/// the game
private void Run ()
{
m_GameRunning = true;
System.out.println("Starting Run");
boolean l_IsBlue = true;
// Game timing/update delta
long l_Time = System.currentTimeMillis();
long l_now;
long l_delta;
while (m_GameRunning)
{
// Get the time now, and work out the difference
l_now = System.currentTimeMillis();
l_delta = l_now - l_Time;
// Only if we are at the time, draw
if ( l_delta > 33 )
{
// Get the graphics into which
// we can draw
java.awt.Graphics2D l_Graphics = (java.awt.Graphics2D)m_Strategy.getDrawGraphics();
// Clear to blue
if ( l_IsBlue )
{
l_Graphics.setColor(java.awt.Color.blue);
}
else
{
l_Graphics.setColor(java.awt.Color.yellow);
}
l_IsBlue = !l_IsBlue;
l_Graphics.fillRect(0, 0, this.getWidth(), this.getHeight());
// Show this frame of graphics
// we've drawn
m_Strategy.show();
// because we did a draw, set time to now
l_Time = l_now;
}
Yield();
}
System.out.println("Exiting Run");
}
// Quick and dirty yield function, to
// release the CPU time whilst in the
// run loop
private void Yield()
{
try
{
Thread.sleep(2);
}
catch (Exception e)
{
System.out.println("Exception [" + e + "]");
}
}
public static void main(String[] args)
{
// TODO Auto-generated method stub
System.out.println ("Hello World");
MainWindow l_Instance = new MainWindow();
l_Instance.Run();
}
}

No comments:

Post a Comment