The third week of our Java Game Tutorial, and hopefully you've had a lot of time to play with the growing Dots, the 2D draw, maybe you've had a play and made the rendering loop draw other things...
Because this week, we're going to look at something dungeon related, the grid or map, we're going to make our play area.
First things first though, we no longer want the background to react to clicking and go red, so we need remove the "m_LeftButton" flag, and it's used in the mouse pressed & mouse released functions. Then we need remove it from the rendering loop.
We draw the background all black now, because we can't "see" anything. In a moment we'll draw a grid of squares, or tiles, which we will move characters around on; but later we'll also be looking at how far those characters can see, and so how much of the black they push back.
Of course, drawing a grid so, we also don't want those seeds to grow, but we'll only stop them planting, so we can go to the mouse pressed function and just comment out the call to "AddSeed".
Commenting out means we leave the code in place, but the compiler when it builds the code into a program will skip that line. To make it a comment therefore we add "//" at the from.
There are other ways to do comments, but for now stick to this method only, we'll cover the pit falls of other problems.
So, onto the new Grid code, lets create the Grid class....
And give it a name...
And so we get the new text file as a tab to edit...
Next time we create a class, I won't be documenting all these parts of the process again, I'll just say we're creating some named class.
So, what do we need to know about our grid?.. Well a size, so we need a width and a height, then we need to pass these values into the constructor.
To draw the grid we need to pass the Graphics2D context to it, do we'll add that function, and how about an Update function too, so we can animate things on the grid later.
The problem now is what to draw?... Well, we've not defined any data for the Grid yet, so how about we create a new class called a Tile.
Inside the tile, what is is going to represent?.. Well It's going to be a square on our grid... Which characters stand on, lets say they stand on a stone floor, the tile need to have a type inside it, something to tell us it is meant to represent a Stone Slab...
So how do we do this? Well, there are things called "enums" which are a bit like a new type for us to use, and they are all the same "type" datawise, but they can have different labels inside, so we might have "TileType" and inside "Wall", "Floor" and so forth.
We've already seen one enum used, this was the "java.awt.Color", which has different colours inside.
We'll create our "TileTypes" enum, as follows:
And then we'll just add two types, "Blank" and "UNKNOWN". We will always keep "UNKNOWN" as the last type in the list.
We can now use the "TileTypes" enum just like any other type in the language, such as "int" or "string" or boolean!
In our code though we also want to draw the tile, and perhaps also update the time, so we'll add those two functions now...
We need to be a little coy now, we can't draw a tile yet, because we don't know where it is on the screen, nor how big it is, but lets say that a tile is going to always be a fixed width and height, we can define those as constants, or finals...
Included in that we now have a top and a left for the tile, and can now define what we draw...
This is a new structure, where we can quickly jump down to the code we want to act upon, based on the value of the TileType. So, when this is "BLANK" from the enum, we jump to set the colour to black, otherwise the colour is white.
Now we just draw a square...
Jumping back to the grid we now need a set of tiles to draw, for this we'll use a multi dimensional array, so each entry on the first set of the array is another array.... Meaning we can have "m_Tiles[1][3]" and it will go to that tile!
Of course we've just created the space for tiles, we've not gone through and set each tile to any type...
But, we need to know where to place each tile... This is going to be based on the size of the tile...
The first thing you should see with adding this code change however, is that from inside the "Grid" you can't get at the two constants "c_Width" or "c_Height" inside the "Tile"... How come my code shows no error?
Well, I have gone back to the Tile and I've set those two constants "Static", this means I can access those as values of the type or class without actually needing to be inside a copy of the clas (it means more than that, but for now, roll with it).
Right, then... We have a grid of tiles, the tiles will draw themselves, lets flesh out the grid draw function...
And the update function...
Finally, jumping back to the main window, we now need a grid... lets make it create as a 10 x 6 area... And add a call to draw it...
Then to draw it...
Lets have a look what that looks like...
Everything is black... I've cleared the background to black, and I've made the tiles draw black... Hmmm, what should we do?
Well, I don't want the tiles to worry about drawing their outside or border, I want the grid to draw the borders, so we can turn them on or off later, and so that we can highlight the selected tile on the grid.
Lets jump to the Grid drawing function and make it now draw the borders....
How does that look?...
Was that what you were expecting?... It was what I expected, but perhaps you didn't see this coming?... What's going on?...
Well, as we move across the grid we draw and redraw the tiles, so each tile draws a black square, then the grid draws the border, the next tile then draws it's black and they overlap by a pixel, because we've not spaced them out.
So that black next tile scrubs out the border of the previous drawing.
To get around this we need to do one of two things, or eventually just move the tiles, but for now, we jump back to the grid class and change the border drawing...
So, we draw all the tiles, then draw the borders, this means we go over the loops twice, but does it fix our drawn image?...
Yes!
This is a good lesson to learn, we've done two passes over the same loops, to draw one set of tiles. Avoiding too many repeats, especially as the data/grid size gets bigger, is important.
Still, our tiles don't look how I really want them, I'd like them to be a bit larger. This is easy to fix, we just jump to the Tile code and change the width and height constants.
Because we've used constants and made them static for use in all the other classes wanting to know the size of a tile, we can now just change the constants once, rebuild the code, and it'll all fall into place... Lets try some different sizes.
Or...
I like that last size, so we'll stick with that... But we've already got another problem, the tiles to the right and bottom rows are now slipping off the screen.
I'm not going to worry about that directly, we next week we're going to look at scrolling the view around to see huge maps, 100x80 or 1000x800!
However, to help me understand what's happening now, and what might happen when we scale up to scrolling, I'm going to add some text to tell me the index of each tile. This means drawing text.
First off, from the existing Clear Tiles in the grid, we now want to just tell the Tile the X and Y position, we'll let the tile itself calculate it's intended left and top inside.
And the Tile itself...
Running this code up now, gives us the same as the last run, but now we can jump to the Tile Draw and put the text for the "m_X" and "m_Y" values into the tiles top left corner!
So, we set the colour to yellow, build the coordinates as a string, and draw them just down and slightly to the right inside the tile.
Now, when we come to scrolling the grid around we're fore armed with the numbers of the grid in the tiles so we'll know where we are.
Final code clean ups, I'm going to go into the MainWindow and define two final static constants for the grid size to use, so we'll not have 10 and 8 coded directly into the MainWindow call to "new Grid", instead we'll have these two new constants.
We've changed the size of the tiles, we've can now change the size of the grid... Why not have a few experiments...
We can also go to the Run function and remove the "Starting Run" and "Ending Run" calls. And the same println call in the main function, just to tidy up our code. We can remove the rest of the Seed & Dot stuff as well...
---- MainWindow ----
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 default grid size
private final int c_GridWidth = 100;
private final int c_GridHeight = 80;
// The master game loop exit flag
private boolean m_GameRunning;
// The seeds of our clicks
private java.util.Queue<java.awt.Dimension> m_Seeds = null;
// The list of expanding dots
private java.util.ArrayList<Dot> m_Dots = null;
// The window frame
private javax.swing.JFrame m_WindowFrame;
// The drawing strategy (refreshing)
private java.awt.image.BufferStrategy m_Strategy;
// The grid
private Grid m_Grid = null;
// 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();
m_Grid = new Grid(c_GridWidth, c_GridHeight);
}
// 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
// Direct the mouse events to the
// canvas itself
this.addMouseListener(this);
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
}
// Adds a seed to the list of places the
// mouse was clicked, since we last
// updated
private void AddSeed(int p_X, int p_Y)
{
if ( m_Seeds == null )
{
m_Seeds = new java.util.LinkedList<java.awt.Dimension>();
}
m_Seeds.add(new java.awt.Dimension(p_X, p_Y));
}
@Override
public void mousePressed(MouseEvent p_Event)
{
// Left click
if ( p_Event.getButton() == MouseEvent.BUTTON1 )
{
//AddSeed(p_Event.getX(), p_Event.getY());
}
}
@Override
public void mouseReleased(MouseEvent p_Event)
{
if ( p_Event.getButton() == MouseEvent.BUTTON1 )
{
}
}
// Add a Dot
private void AddDot(Dot p_newDot)
{
if ( m_Dots == null )
{
m_Dots = new java.util.ArrayList<Dot>();
}
m_Dots.add(p_newDot);
}
// Update the scene
private void Update(long p_Milliseconds)
{
// There are seeds
if ( m_Seeds != null )
{
// if we have seeds, add them
if ( !m_Seeds.isEmpty() )
{
// Dequeue each seed point and
// create a new expanding Dot
java.awt.Dimension l_Location = m_Seeds.remove();
Dot l_newDot = new Dot((int)l_Location.getWidth(), (int)l_Location.getHeight());
// They maybe called Width and Height, but
// really we're using them as two numbers...
AddDot(l_newDot);
}
}
if ( m_Dots != null && !m_Dots.isEmpty() )
{
// For each dot, update them
for (int i = 0; i < m_Dots.size(); ++i)
{
m_Dots.get(i).Update(p_Milliseconds);
}
}
}
// Draw the dots
private void DrawDots(java.awt.Graphics2D p_Graphics)
{
if ( m_Dots != null && !m_Dots.isEmpty())
{
for (int i = 0; i < m_Dots.size(); ++i)
{
m_Dots.get(i).Draw(p_Graphics);
}
}
}
/// The function we "run" from the main
/// function, to go into a loop, rendering
/// the game
private void Run ()
{
m_GameRunning = 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 )
{
Update(l_delta);
// Get the graphics into which
// we can draw
java.awt.Graphics2D l_Graphics = (java.awt.Graphics2D)m_Strategy.getDrawGraphics();
// Clear the map background as black
l_Graphics.setColor(java.awt.Color.black);
l_Graphics.fillRect(0, 0, this.getWidth(), this.getHeight());
// Draw the grid
if ( m_Grid != null )
{
m_Grid.Draw(l_Graphics);
}
// After the update of the background colour,
// call to draw the dots
DrawDots(l_Graphics);
// 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();
}
}
// 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)
{
MainWindow l_Instance = new MainWindow();
l_Instance.Run();
}
}
---- Dot.java ----
package display.display;
public class Dot
{
private final int c_MinimumSize = 1;
private final int c_PixelsPerSecond = 2;
private int m_OriginX, m_OriginY;
private long m_Age;
public Dot(int p_X, int p_Y)
{
m_Age = 0;
m_OriginX = p_X;
m_OriginY = p_Y;
}
public void Update(long p_Delta)
{
m_Age += p_Delta;
}
public void Draw(java.awt.Graphics2D p_Graphics)
{
// Calculate the radius
int l_RadiusInPixels = c_MinimumSize + ((int)(m_Age/1000)*c_PixelsPerSecond);
int l_half = (int)(l_RadiusInPixels / 2);
int l_startX = m_OriginX - l_half;
int l_startY = m_OriginY - l_half;
p_Graphics.setColor(java.awt.Color.pink);
p_Graphics.fillOval(l_startX, l_startY, l_RadiusInPixels, l_RadiusInPixels);
}
}
---- Grid.java ----
package display.display;
public class Grid
{
// Size
private int m_Width, m_Height;
private Tile[][] m_Tiles;
// Draw options
private boolean m_DrawBorders = true;
// Constructor
public Grid(int p_Width, int p_Height)
{
m_Width = p_Width;
m_Height = p_Height;
m_Tiles = new Tile[m_Width][m_Height];
ClearTiles(TileTypes.BLANK);
}
private void ClearTiles(TileTypes p_Type)
{
for (int y = 0; y < m_Height; ++y)
{
for (int x = 0; x < m_Width; ++x)
{
m_Tiles[x][y] = new Tile(p_Type, x, y);
}
}
}
public void Draw(java.awt.Graphics2D p_Graphics)
{
// Just draw the tile contents
for (int y = 0; y < m_Height; ++y)
{
for (int x = 0; x < m_Width; ++x)
{
m_Tiles[x][y].Draw(p_Graphics);
}
}
if ( m_DrawBorders )
{
// Draw all borders
for (int y = 0; y < m_Height; ++y)
{
for (int x = 0; x < m_Width; ++x)
{
// Draw a border
p_Graphics.setColor(java.awt.Color.gray);
int l_Left = x * Tile.c_Width;
int l_Top = y * Tile.c_Height;
p_Graphics.drawRect(l_Left, l_Top, Tile.c_Width, Tile.c_Height);
}
}
}
}
public void Update(long p_Milliseconds)
{
for (int y = 0; y < m_Height; ++y)
{
for (int x = 0; x < m_Width; ++x)
{
m_Tiles[x][y].Update(p_Milliseconds);
}
}
}
}
---- Tile.java ----
package display.display;
public class Tile
{
// The width and height of a tile is fixed
public static final int c_Width = 80;
public static final int c_Height = 80;
// The type of the tile
private TileTypes m_Type;
// The X Y of the tile
private int m_X, m_Y;
// The top left corner position of the tile
private int m_Top, m_Left;
// Constructor
public Tile(TileTypes p_Type, int p_X, int p_Y)
{
m_Type = p_Type;
m_X = p_X;
m_Y = p_Y;
m_Top = m_Y * c_Height;
m_Left = m_X * c_Width;
}
public void Draw (java.awt.Graphics2D p_Graphics)
{
switch (m_Type)
{
case BLANK:
p_Graphics.setColor(java.awt.Color.black);
break;
default:
p_Graphics.setColor(java.awt.Color.white);
}
p_Graphics.fillRect(m_Left, m_Top, c_Width, c_Height);
// The X Y Text
p_Graphics.setColor(java.awt.Color.yellow);
String l_Coords = "[" + m_X + ", " + m_Y + "]";
p_Graphics.drawString(l_Coords, m_Left + 5, m_Top + 15);
}
public void Update(long p_Milliseconds)
{
}
}
---- TileTypes.java ----
package display.display;
public enum TileTypes
{
BLANK,
UNKNOWN
}