Game States
At the end of this we wanted to be able to move something - i.e. the player around the map. Currently we have no concept of map, or player for that matter, or even a tile. Some of this stuff needs generalizing and classes need to be created. This is the part of design where the code can start looking awfully complex, so it's worth trying to get some good artitecural structure in for the get-go.
Note: The tiles are currently 0.32% of x and y extents of the window - I do not believe this adds to a nice round number, maybe 0.25 would be a better fit, something to think about a little later!
Architecture
Before we starting thinking too much about maps and things, like get some nice basic solid game architecture set up. The first thing we need think about are game states :- credits, intro screen, option, gameplay screen and all that jazz. How can we handle this elegantly - by looking at Programming Role Playing Games with DirectX by Jim Adams, he's provided some patterns for dealing with game creation!
yay!
Where I go in to mindless and possibly needless detail to explain some simple concepts - but at least once you learn these they can be applied everywhere. Read: How to do funky stuff with interfaces
States and Processes
So we want to hanlde our various game states in a powerful and elegant manner. We want to be able to handle the situation where, for instance, the user presses the [esc] key. On the title screen this may prompt a quit dialog. In the game it may jump you to the start screen. Input is handled differently for different states.
We don't want the main render loop to have a piece of code like the one showjn below, rather we wish it to be more elegant
switch(CurrentState) {
case STATE_TITLESCREEN:
DoTitleScreen();
break;
case STATE_MAINMENU:
DoMainMenu();
break;
case STATE_INGAME:
DoGameFrame();
break;
}
Basically we'll have a pointer to a function, or stack of pointers to function and call this stack. So the stack will call the. let's say
Render()
function of each state on the stack. So let's have look at what we want the stack to do and then implement it.Data Types Stacks and Stackees
On this stack we're going to have 'states' such as Playing,Title Screen, Credits etc. But how I here you cry - HOW!? We need to define some kind of state object that can be used to stack. So what's the very least we need to say something is a state - probably a render function. What we're trying to say in C# is "if it has a Render() function then it's a state". We're going to do this by creating a state interface. State is a very broad word but I want something a little more precise. How about
GameState
that's better!OKay so that's how the states are going to work. What about the stack. First what is stack? well a stack is one of the basic data types and the name is unusally quite descriptive with regards to its function.
It has two functions push and pop. The most common example used for this is a stack of plates, poping the stack you remove the top plate. Pushing the stack means you put a new plate on top. There's a little ambiguity when you attempt to pop an empty stack but I'm sure a clever programmer like yourself will be able to deal with it. So why do we need a stack? As far as I'm concerned, for my program I don't need a stack!
I know shock horror!
But Programming Role Playing Games uses a stack - why aren't you? Well I think it's personally preference but it's worth looking at why it is done in the book. It is quite clever, you have your stack maybe intialized with a Start Screen okay so let's say from the start screen you select Play Game, then the Play Game State is added to the top of the stack. Each frame you call process in the stack, it starts from the bottom and tells the start screen to render / process what ever and it does this. Then it goes to the last state - the playing game and this renders everything to the screen. This may seem to beg the question why use a stack at all why not just use a single function pointer?
Well you may want 'states' that do not take up the entire screen, for instance imagine that Play Game State is not at the top, instead we've pushed another state called Message Box. So the game is rendered to the screen and then a message box is rendered above this! This might say "Connection to server broken" or whatever. Note that this message box could also appear on the title screen - or if there was another state like a chat room lobby this could also benefit from message boxes. One can see that you could have inventory screens and all types of GUI goodness.
For me though, for now I want to keep it simple so only one function pointer. If things get complicated then later we may introduce the stack. If messages boxes and inventory screens are needed - like they probably will be. Then they will be confined to the game play state - so a stack can be placed here.
Okay I hope you understand all the choices avaliable now. Vaguely the same class structure will be usedso we'll have a manager class. Lets see how we get the code employed!
Code Division
Generally I like to seperate code out, I don't want to have too much that I have to look at at once, so generally for each class a seperate .cs file. Namespaces can be spread across files. Also if I'm testing a new concept - like this architecture we see here, I like to start a new project and create it seperately therefore I've got something to look back on and play with until I get it perfect.
So we'll start up a nice fresh project I've called it ArchitectureI whitch seems possibly mispelt. Then we add a nice new class called Arch. Remember to add
System.Windows.Forms
and then we'll make sure we're using
it. Okie a few keytaps later and Arch is inheriting from form, then we rip the previous code we wrote in lesson one to get a game loop up.One quick note about this game loop before we get to the code - the game loop is not as efficient as it could be. This isn't directly our fault as we've basically copied what is suggested in the mircosoft examples. But
Application.DoEvents()
actually creates and deletes more objects than strictly necessary. But ... who really cares! The central rule of game programming is MAKE IT WORK! most people fail at this rule because they attempt to first attempt to apply the second rule of 'make it fast' (or even attempt to do this in parrallel.) To partially quote some of my old lecture notes:
If you think this is a big problem you are refered to a famous 'quote' from the early days of computing :
Programmer one says 'My program is faster than yours.' Programmer two replies - 'Yes but my program works.'
Okay enough tangent let's have a look at this base code. The code we're going to build our snappy architecture from!
using System;
using System.Windows.Forms;
namespace ArchitectureI
{
public class Arch : Form
{
static void Main()
{
Arch arch = new Arch();
arch.Show();
while(arch.Created)
{
Application.DoEvents();
}
}
}
}
Cool so this gives us our little window. Of course most of architecture code is gonna be happening in the background.
Okay so let's go up to the
Project
menu and select add new item choosing a new .cs file. We'll call this GameState, yes very creative. Then we tap in the basic interface that we intend to use to identify our game states. So we're now dealing with two files. Lets have a quick mooch at the Interface.
using System;
namespace ArchitectureI
{
public interface GameState
{
void render();
}
}
There we go! Pretty simple hey?
Now to create the manager, lets give it the name GameStateManager, a new class, so a new file. As before create a file and then let's poke around with the code. Minutes of poking later we'll have a basic class skeleton like below!
I wonder where this code went?
We want to write this code with possible revisions in mind - imagine we wanted to turn it into a stack. It shouldn't take too much work, so this class will strieve to be lightweight. We want a
Process
method and a Switch
method. The switch method will return the current state and replace it with the new state passed in. So we give the Manger the state we'd like to now use and we get the state that was being used. (this way if we go to options or back to the title screen we can keep a reference of the playing game state and return the user there without too much trouble)
using System;
namespace ArchitectureI
{
public class GameStateManager
{
private GameState state;
public void process()
{
}
public GameState switchState(GameState newState)
{
}
}
}
So that's the basic class, if you try and compile you will probably get a few errors about not returning types. We need to fill in these functions to provide functionality.
public void process()
{
if(state != null)
state.render();
}
Quite simple, we might want to do a little more programming about the situation of having a null pointer
but for now we'll keep it simple (and probably a bit lacking in programming wisdom. Tangent: We might
want to do a try and catch. Or might want to just exit if there is no state)
That's workable , so let's move on to
switchState
function.
public GameState switchState(GameState newState)
{
GameState oldState = state; //store current state
state = newState; //replace current state
return(oldState); //return old value
}
This function is pretty simple. Now we probably want to add a nice construcor function, that will allow
us to load in an intial state. Shouldn't be too stressful.
public GameStateManager(GameState intialState)
{
state = intialState;
}
Right that is all the classes required, now to piece it together and have a look at some examples. We
want to keep this quite simple because if we later modify it, this is the project we'll be looking at. So
we're not going to pull in DirectX devices and the like, rather we'll use a string as the device and
we'll display different things depending on state. Visually it looks a little like below.
Putting it all together
To get an example we'll need to do some fiddling with the Arch class. We'll also need to make some
States that will inherit from GameState! First lets look at the modifications.
using System;
using System.Windows.Forms;
using System.Drawing;
Remember to include the reference for System.Drawing aswell!
namespace ArchitectureI
{
public class stringholder
{
public string output;
public void change(string newString)
{
output = newString;
}
}
Where has this class from? Why is it here? Well allow me to explain it's a reasonably dirty hack that
stops me messing about with references of basic types like strings. If you want to find out why I've done
this try simplifying it so it works with a string okie!
public class Arch : Form
{
GameStateManager gamestateManger;
stringholder pretendDevice;
So we make a manager and a string, we're going to intialize the string and the Manager (but we'll do that
later). So let's throw a constructor in.
public Arch()
{
pretendDevice = new stringholder();
pretendDevice.change("No State");
//yes if we added a constructor this could have been done in one
}
static void Main()
{
Arch arch = new Arch();
arch.Show();
while(arch.Created)
{
Application.DoEvents();
}
}
Okay we want to be able to output something to the window - this way we can show when states are changed!
protected override void OnPaint(PaintEventArgs pea)
{
pea.Graphics.DrawString(pretendDevice.output,this.Font, Brushes.Black, 0,0);
}
}
}
Some quick overriding later and we have our basic set up. This will produce something similar to the
window shown below. Now we need to create a few states to play with.
Time to create an example state or two - in this case we'll create one file but to classes! I wouldn't
normally do this but these classes are going to be small and only used for example purposes so it's not
really worth creating two seperate files to store them.
So let's add a new code file and call it Examples. How is it all going to work? Well the example classes
will be given access to the stringholder and if the states are called then they change what the string
said. Right I'm going to dump the first Example down now.
using System;
namespace ArchitectureI
{
public class TitleScreen : GameState
{
stringholder s;
public TitleScreen(stringholder output)
{
s = output;
}
public void render()
{
s.change("The Wonderful State Game");
}
}
}
As you can see we inherited from GameState and implemented the render function where we change the string
value. The example is all pretty basic, but to see how the states are going to work we need to go back
and fiddle with Arch again.
public Arch()
{
pretendDevice = new stringholder();
pretendDevice.change("No State");
gamestateManager = new GameStateManager(new TitleScreen(pretendDevice));
}
static void Main()
{
Arch arch = new Arch();
arch.Show();
while(arch.Created)
{
gamestateManager.process();
Application.DoEvents();
}
}
The bolded bits are the bits we've changed. We load in the basic state in the constructor, then during
each frame the process is called and this calls the render function of the state loaded. Great -yeh? I
thought as much, so our state loaded in during in the construtor sets the text to "The Wonderful State
Game". I hope that's all pretty clear now. Let's make another Example state (the best way to do this is
copy it and paste it and then change the text - yes redundancy! But clarity! In the fight of illustration
clarity wins!
Let's see the next example! It's a quick copy paste with the title and text changed
public class PlayingGameState : GameState
{
stringholder s;
public PlayingGameState(stringholder output)
{
s = output;
}
public void render()
{
s.change("You are now enjoying a wonderful game of game state!");
}
}
So we have a Title Screen and we have a Playing Game. Let's assume in this simple game you press "ENTER" to start playing. Enter would of course change state! How is this magically journey going to end? Hold on to your keyboard.
Back to the main code in Arch and let's do a little more overriding
protected override void OnKeyPress(KeyPressEventArgs e)
{
if((int)e.KeyChar == (int)Keys.Enter)
{
//MessageBox.Show("You pressed Enter");
gamestateManager.switchState(new PlayingGameState(pretendDevice));
Invalidate();
}
}
What enter a couple of times and you'll change from the title screen to the playing the game.
!Important! Is this code any good?
The architecture is good as is the base code but to get the example then NO. There's a number of
nasty bits. We'll calling on paint and Application.DoEvents() - it's kinda of one or the other!. Also
when taking input we'd pass the input to the state and then the state would take care of it's own input.
This would make everything perfect and it's what we'll be doing next.
Okay so we got
GameState
, GameStateManager
and these will transfer over to DirectX without too much problem - and we'll be much more careful about conflicts (no Application.DoEvents
and OnPaint
, much straighter!) Now we have the files to look back on as well and refer to.Mixing the Architecture up with DirectX and our previous progress
Time for another new project! Yay! Let's call this one LearningToCrawlI (walking is still a long way off). Let's list what we've got to do here!
- Set the basic game loop
- Get the DirectX linked to the Window
- Get a few States in there
- Abstract a tile class
- Tile the window
- Render a character
- Add movement
We'll start off by adding a class that we'll call Crawl. Then get our main loop sorted and work from there.
From our previous project
ArchitectureI
we can copy paste the files that contain the classes GameState
and GameStateManager
. Either copy the files over from one project directory to our current one and then add existing item and choose them. Or just create two new .cs files and copy the code into them. Whatever you feel most comfortable doing! (Just remember to change the namespace from ArchitectureI
to LearningToCrawlI
, or whatever you've called it.) After a frenzy of activity you should have a similar setup as that shown below.We now have a version of our pure state architecture ready to rumble. We can now modify it as the need arises. For instance we might want to change the GameState interface so it will take in information about input. But we'll get to that later! Or we may have second thoughts and actually change the GameStateManage to a stack - it's world of endless possibility.
Fleshing out Crawl with a bit of Game Looping and DirectX!
We need to add references for:
- System.Drawing
- System.Windows.Forms
- Microsoft.DirectX
- Microsoft.DirectX.Direct3D
Then the basics of DirectX and the game loop need adding in. So that's just putting the game loop code in, and making sure the device is there and adding the code to set it up. We're not putting the VetexBuffer in because we want that to be associated directly with the tile rather than been some random variable in the main class. Therefore we have a
OnCreateDevice
function but it's blank for now. So after this what does the code look like? Well it looks like the code pasted in below.
using System;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace LearningToCrawlI
{
public class Crawl : Form
{
Device device = null;
public Crawl()
{
}
static void Main()
{
Crawl form = new Crawl();
form.InitializeGraphics();
form.Show();
while (form.Created)
{
Application.DoEvents(); //Let the OS handle what it needs to
}
}
public void InitializeGraphics()
{
try
{
// Now let's setup our D3D stuff
PresentParameters presentParams = new PresentParameters();
presentParams.Windowed=true;
presentParams.SwapEffect = SwapEffect.Discard;
device = new Device(0, DeviceType.Hardware, this, CreateFlags.
HardwareVertexProcessing, presentParams);
device.DeviceReset += new System.EventHandler(this.OnResetDevice);
}
catch (DirectXException e)
{
MessageBox.Show(null, "Error intializing graphics: " + e.Message, "
Error");
Close();
}
}
public void OnResetDevice(object sender, EventArgs e)
{
Device dev = (Device)sender;
dev.RenderState.Lighting = false;
}
}
}
Notice how the
IntializeGraphics
has been ripped wholesale from previous lessons! Yay! Now we have the basics of what we have done in the two previous lessons - we now need to get them to work with each other. This means defining some states and a little more hacking with the game loop.State Managment into the GameLoop
We're goin to be using our shiny new
GameStateManager
so we need this declared in the form. We're going to be calling it from the main loop and because the main loop is static, we need to define it as static, or call it through
form
that has already been created. It's takes little effort to get things set up as below:
public class Crawl : Form
{
GameStateManager gameStateManager;
Device device = null;
public Crawl()
{
}
static void Main()
{
Crawl form = new Crawl();
form.InitializeGraphics();
form.gameStateManager = new GameStateManager(null);
form.Show();
while (form.Created)
{
form.gameStateManager.process();
... etc
We intialize GameStateManager just after intializing the device - currently just with
null
. This means that once run very little is going to happen, it's nice to see that it doesn't crash though :). To see some action we need to craft some game states.Crafting a 'title screen' game state
As before we will have to create a game state using our game state interface. So Project>Add New Item and choose a nice new code file. I'm calling mine
TitleScreenState
. First we'll type out the skeleton structure of our class, so we will end up with something like:
namespace LearningToCrawlI
{
public class TitleScreenState : GameState
{
public void render()
{
}
}
}
So what do we want to do when we call render? Display our title screen of course and give instructions for what the player should be doing. Down to the basics this means displaying some graphics so cue VertexBuffers, textures all that crazy jazz.
So in this game state we'll throw in a nice VertexBuffer variable, we can keep this private. Of course we also want to include the necessary using ... lines of code.
At this point to create more order classes I capitalized the
Render()
in GameState. I should go back and alter the tutorial but for now this warning note will suffice.We also need to drop in a constructor - we're going to have to setup the vertex buffer there and get access to the D3D
device
. So the inner guts of this new State look a bit like below:
private VertexBuffer vertexBuffer;
private Device device;
public TitleScreenState(Device d)
{
device = d;
vertexBuffer =
new VertexBuffer(typeof(CustomVertex.PositionTextured),
4,
device,
0,
CustomVertex.PositionTextured.Format,
Pool.Default);
}
public void Render()
{
}
Reasonably standard so far - infact so standard it's almost tempting to start playing around with Abstract Classes as well. Put for now let's keep thing simple and avoid tangents. At this point we can jump back to Crawl.cs and add TitleScreenState in as the default state to start up with.
static void Main()
{
Crawl form = new Crawl();
form.InitializeGraphics();
form.gameStateManager =
new GameStateManager(new TitleScreenState(form.device));
form.Show();
...etc
Once again this runs without crashing, all we need do is tweak our Render function so we actaully begin to display something. Back to TitleScreenState!
At this point it would be nice to put in some kind of check to see if everything is working as we hope. Let's scroll on down to our
Render()
function. We'll stick some basic code in here, to set the background colour and present the results to the screen. We're going for a moody black title screen colour so the code looks as below:
public void Render()
{
if (device == null)
return;
device.Clear(ClearFlags.Target,
System.Drawing.Color.Black, 1.0f, 0);
device.BeginScene();
device.EndScene();
device.Present();
}
With this in place we get:
Perfect, eveything is working well
The next step is to get the VertexBuffer full of vertices to show the expectant user. A lot of this is copying code we've written before. So let's put in those vertices, we'll make a rectangle that covers the entire window, and put a pretty starting screen there. So we need to go to the constructor and add a few lines and put in a delegate.
public TitleScreenState(Device d)
{
device = d;
device.DeviceReset +=new System.EventHandler(deviceReset);
vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionTextured),
4,
device,
0,
CustomVertex.
PositionTextured.Format,
Pool.Default);
vertexBuffer.Created += new System.EventHandler(this.OnCreateVertexBuffer);
b>
this.OnCreateVertexBuffer(vertexBuffer, null);
}
We've made reference to an EventHandler (
OnCreateVertexBuffer
) that we have yet written, so that's what should be tackled next. In this event handler we'll set up Vertex Positions, as we want to
use the entire window there isn't all that much to think about.
private void OnCreateVertexBuffer(object sender, System.EventArgs e)
{
VertexBuffer buffer = (VertexBuffer)sender;
CustomVertex.PositionTextured[] verts = new CustomVertex.PositionTextured[4];
verts[0].SetPosition(new Vector3(2,0,0));
verts[1].SetPosition(new Vector3(2,-2,0));
verts[2].SetPosition(new Vector3(0,-2,0));
verts[3].SetPosition(new Vector3(0,0,0));
buffer.SetData(verts, 0, LockFlags.None);
}
This code is very much the same as before with only changes in the vertex positions. Next we want to
publish this to the screen - onward to the
Render()
procedure!
public void Render()
{
if (device == null)
return;
Matrix QuadMatrix = new Matrix();
QuadMatrix = Matrix.Identity;
QuadMatrix.Translate(0,0, 0f);
device.Clear(ClearFlags.Target, System.Drawing.Color.Blue, 1.0f, 0);
device.BeginScene();
device.SetStreamSource( 0, vertexBuffer, 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
device.SetTransform(TransformType.World, QuadMatrix);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
device.EndScene();
device.Present();
}
For now we'll just whack in the above code - before we'll be able to see too much though we're going to
need a texture! So I'm going to fire up MSPaint and create a beauty this very instant.
Truely it beings a tear to my eye. Let's get this wonder in to a texture format ASAP. Cue copy lots of
code we've previously written!
private VertexBuffer vertexBuffer;
private Texture titleTexture;
private Device device;
In the variables we add a nice new texture variable appropiately named. Next we'll take the
LoadTexture
function we wrote previously and slightly modify to load our new texture.
public void LoadTextures()
{
try
{
System.Drawing.Bitmap title = (System.Drawing.Bitmap) System.Drawing.
Bitmap.FromFile(@"C:\titlescreen.bmp");
titleTexture = Texture.FromBitmap(device, title, 0, Pool.Managed);
}
catch(Exception e)
{
MessageBox.Show("There has been an error loading the textures:" + e.
ToString(), "oops");
}
}
Of course we have to add a call to this in the constructor as well!
public TitleScreenState(Device d)
{
device = d;
device.DeviceReset +=new System.EventHandler(deviceReset);
LoadTextures();
...etc
Cool now just to knock in those good old Tu and Tv values then a small edit to the
Render
procedure and we're good to go!
AS I'm sure you will recall Tu and Tv control where each vertice is going to be placed on the texture,
then the veterices and joined up and that shape is cut out. In this case we want a square shape from a
square texture so it's very simple. So in the
OnCreateVertexBuffer
function before the lock we add the following texture information:
verts[0].Tu = 1.0f;
verts[0].Tv = 0.0f; // Top Right Hand Corner
verts[3].Tu = 0.0f;
verts[3].Tv = 0.0f;
verts[1].Tu = 1.0f;
verts[1].Tv = 1.0f;
verts[2].Tu = 0.0f;
verts[2].Tv = 1.0f;
Before running - there's needs to be some minor cleaning. We want at this stage to be able to resize the
window and just have the graphics resize. Also we need the lighting to be turned off, especially when
reset, briefly we cut the constructor down to the following:
public TitleScreenState(Device d)
{
device = d;
device.DeviceReset +=new System.EventHandler(deviceReset);
deviceReset(d, null);
LoadTextures();
}
Then we build up the deviceReset function so that it looks like below:
private void deviceReset(object sender, System.EventArgs e)
{
Device d = (Device) sender;
d.RenderState.Lighting = false;
vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionTextured),
4,
d,
0,
CustomVertex.PositionTextured.Format,
Pool.Default);
vertexBuffer.Created += new System.EventHandler(this.OnCreateVertexBuffer);
OnCreateVertexBuffer(vertexBuffer, null);
}
And we get this: (currently there's an annoying line of corruption, that I can't see why it should be there hopefully I stumble across the answer at some point)
Still it's looking pretty spiffy and we can be very pround of how far we have come!
Aside: Annoying Line of corruption
Currently I'm writing on a computer without hardware acceleration, upon setting up the Device I set it up for hardware accleration - hence problems!
device = new Device(0, DeviceType.Hardware, this, CreateFlags.HardwareVertexProcessing, presentParams);
Should have been:
device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams);
Huzzah!
Code for TitleGameState
using System;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace LearningToCrawlI
{
public class TitleScreenState : GameState
{
private VertexBuffer vertexBuffer;
private Texture titleTexture;
private Device device;
public TitleScreenState(Device d)
{
device = d;
device.DeviceReset +=new System.EventHandler(deviceReset);
deviceReset(d, null);
LoadTextures();
}
public void Render()
{
if (device == null)
return;
Matrix QuadMatrix = new Matrix();
QuadMatrix = Matrix.Identity;
QuadMatrix.Translate(-1f,1f, 0f);
device.Clear(ClearFlags.Target, System.Drawing.Color.Beige, 1.0f, 0);
device.BeginScene();
device.SetStreamSource( 0, vertexBuffer, 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, titleTexture);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
device.EndScene();
device.Present();
}
private void deviceReset(object sender, System.EventArgs e)
{
Device d = (Device) sender;
d.RenderState.Lighting = false;
vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionTextured),
4,
d,
0,
CustomVertex.PositionTextured.Format,
Pool.Default);
vertexBuffer.Created += new System.EventHandler(this.OnCreateVertexBuffer);
OnCreateVertexBuffer(vertexBuffer, null);
}
private void OnCreateVertexBuffer(object sender, System.EventArgs e)
{
VertexBuffer buffer = (VertexBuffer)sender;
CustomVertex.PositionTextured[] verts = new CustomVertex.PositionTextured[4];
verts[0].SetPosition(new Vector3(2,0,1f));
verts[1].SetPosition(new Vector3(2,-2,1f));
verts[2].SetPosition(new Vector3(0,-2,1f));
verts[3].SetPosition(new Vector3(0,0,1f));
verts[0].Tu = 1;
verts[0].Tv = 0; // Top Right Hand Corner
verts[3].Tu = 0;
verts[3].Tv = 0;
verts[1].Tu = 1;
verts[1].Tv = 1;
verts[2].Tu = 0;
verts[2].Tv = 1;
buffer.SetData(verts, 0, LockFlags.None);
}
public void LoadTextures()
{
try
{
System.Drawing.Bitmap title = (System.Drawing.Bitmap) System.Drawing.
Bitmap.FromFile(@"C:\titlescreen.bmp");
titleTexture = Texture.FromBitmap(device, title, 0, Pool.Managed);
}
catch(Exception e)
{
MessageBox.Show("There has been an error loading the textures:"
+ e.ToString(), "oops");
}
}
}
}
How about another state?
We need two things:
- A game state
- Some method for passing from one state to another - input handling!
For now we'll stick to what we know and bash out another state, basically we can copy
TitleGameState
and then patch it with bits of our previous project to get to the stage where we show a few tiles with different textures.Once again we go to Project>Add New Item and choose a nice empty .cs file. This one we'll call
PlayingGameState
. Then we copy and paste our title screen because at this stage it makes a great base to work from. If we'd used an abstract class maybe we would have just been implementing a few things here but what are you going to do :)We need to make a few changes, first we'll rename the class and constructor to 'PlayingGameState'. We also need two textures - at least. At some point we'll add a texture management class which will probably use a dictionary datatype, maybe a hash table. Seems to make sense but when the time comes we'll also consult the experts!
Let's get back to good old grassTexture and stoneTexture, our variables will look like thus:
private VertexBuffer vertexBuffer;
private Texture grassTexture;
private Texture stoneTexture;
private Device device;
Next it makes sense to alter the LoadTexture function, so we load the correct textures.
public void LoadTextures()
{
try
{
System.Drawing.Bitmap grassBitmap = (System.Drawing.Bitmap) System.Drawing.Bitmap.FromFile(@"C:\grass.bmp");
System.Drawing.Bitmap stoneBitmap =
(System.Drawing.Bitmap) System.Drawing.Bitmap.FromFile(@"C:\stone.bmp");
grassTexture = Texture.FromBitmap(device,
grassBitmap,
0,
Pool.Managed);
stoneTexture = Texture.FromBitmap(device,
stoneBitmap,
0,
Pool.Managed);
}
... etc
It's important to ensure that the bitmaps are where you say they are. You can just give the names if they'll be in the same directory as where you executable file (.exe) is generated.
The vertex buffer needs altering so the tiles won't be humongous.
verts[0].SetPosition(new Vector3(0.25f,0,1f));
verts[1].SetPosition(new Vector3(0.25f,-0.25f,1f));
verts[2].SetPosition(new Vector3(0,-0.25f,1f));
verts[3].SetPosition(new Vector3(0,0,1f));
All the rest is the same, we're still taking the entirity of the bitmaps image, so nothing else to change here. Just a tweak or two to the Render procedure and we'll be where we where before.
device.BeginScene();
device.SetStreamSource( 0, vertexBuffer, 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, grassTexture);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
QuadMatrix.Translate((-1 + 0.25f),1f, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, stoneTexture);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
...etc
So far so good - we need to test all of this though. Just to test we'll change a bit of code in the Crawl class so that
PlayingGameState
is loaded by default.
form.gameStateManager = new GameStateManager(new TitleScreenState(form.device));
to:
form.gameStateManager = new GameStateManager(new PlayingGameState(form.device));
Okay that seems cool, see how I seemed to have change the background colour to beige at some point. Now we flick back to title screen and ponder, ponder ponder ponder.
TUPNI
Okay so our last challenge is to recognise that the Enter key has been pressed and then change states. So two things, we want the states to handle their own input annnnndddd we want the states to be able to switch state - I know mind bogglingly but I assure you it's posible just follow.
Hoops
We need to add another
device
to the Crawl class. This device though is a DirectInput Device and we are already using a variable type called Device namely the Direct3D - oh noes. Well the simplest way around this is to add
Microsoft.DirectX.Direct3D.
to the start of all Devices in crawl. This can be done using a Find Replace, find all 'Device' replace with 'Microsoft.DirectX.Direct3D.Device'. Make sure you get the case correct if you do this or you are in for trouble. Not only do you have to do this for device but you must do it for DeviceType. So do another replace 'DeviceType' with 'Microsoft.DirectX.Direct3D.DeviceType' then everything should work.
If everything doesn't work you will get errors saying 'Device' is an ambiguous reference . If this happens you need to go to the line in error and see if it's a DirectInput device or a Direct3D device and make sure it starts with either Microsoft.DirectX.Direct3D. or Microsoft.DirectX.DirectInput. accordingly. So thats a little bit of trouble we have to overcome.
DirectInput device
Let's start off with a small tip - if you don't want to have to be typing out Microsoft.DirectX.DirectInput. all the time, as it seems we may have to due to name clashes, you can lever the
using
keyword and make an alias. So at the top of our Crawl.cs file, (or whatever you've called the .cs file where you house the object with the Main function) we can write:
using DXInput = Microsoft.DirectX.DirectInput;
Now we can just use
DXInput
to play with our stuff. Groovy.Okay so lets stop talking about this device and actually add it to the code.
public class Crawl : Form
{
GameStateManager gameStateManager;
Microsoft.DirectX.Direct3D.Device device = null;
DXInput.Device deviceInput = null;
Next we need a IntializeInput function, so we'll write a call in for one next to the IntializeGraphics function like so:
static void Main()
{
Crawl form = new Crawl();
form.InitializeGraphics();
form.InitializeInput();
...etc
Now we're calling it, it may be a good idea to write the actual code. Basically it's setup code, where a lot of interesting things an be done, but we just want standard safe keyboard use ... and this is how we get it:
public void InitializeInput()
{
deviceInput = new DXInput.Device(DXInput.SystemGuid.Keyboard);
deviceInput.SetCooperativeLevel(this,
DXInput.CooperativeLevelFlags.Background,
DXInput.CooperativeLevelFlags.NonExclusive);
deviceInput.Acquire();
}
First we create the input device, its need a unique identifying number which we get from DirectXInput.
Then we set the CooperativeLevel, Background means that if the window looses focus we don't unaquire the keyboard. NonExclusive means other programs can use the keyboard - imagine some was playing your game but also wanted to using an instant messenger program, well there you go. There are a few other flags - such as turning off the windows key but these can looked up in the MSDN library at your leisure. Finally we aquire the device, which if succesful means that we are now ready to go.
Poking the GameStates
We need access to the Input device and GameStateManager - this will allow us to swap states on a keypress. Of course you know what this means? Yes that's right name conflicts abound - I suggest heavy use of alias to make things easier to handle.
We're going to start off with the
TitleScreenState
first removal of ambiguity. If using DXInput = Microsoft.DirectX.DirectInput;
is added then upon running all the graphics ambiguities will come out of the wood work as errors and can be corrected. Here I'm also going to alias the Graphics as DXGraphics.
using Microsoft.DirectX.Direct3D;
using DXGraphics = Microsoft.DirectX.Direct3D;
using DXInput = Microsoft.DirectX.DirectInput;
If you do the above the ambiguities should be reduced to zero!
So once it's compiling without ambiguity let's fiddle with the constructor and variables.
...
private Device device;
private GameStateManager gameStateManager;
private DXInput.Device inputDevice;
public TitleScreenState(Device d, GameStateManager g,
DXInput.Device dInput)
{
device = d;
device.DeviceReset +=new System.EventHandler(deviceReset);
deviceReset(d, null);
gameStateManager = g;
inputDevice = dInput;
LoadTextures();
}
...etc
Now, of course, some minor alterations must be made to Crawl.
static void Main()
{
Crawl form = new Crawl();
form.InitializeGraphics();
form.InitializeInput();
form.gameStateManager = new GameStateManager(null);
TitleScreenState titleScreenState =
new TitleScreenState(form.device,
form.gameStateManager,
form.deviceInput);
form.gameStateManager.switchState(titleScreenState);
...etc
Everything runs okay, the same changes should be made to PlayGameState also so it has access to the same things as the title screen. They are so similar it will not take too long. (So similar clever people will probably have reduced their coding using some kind of abstract class but I'm going to resist this for now, because it's not absolutlely necessary.
Now we want to be able to check the input each frame - to do this we need to put a procedure into the Render function, which as it's no longer just associated with Rendering we should rename to Process - just use a find replace, or don't bother as its only syntatic sugar. (If you do though you may overright RenderState, and you'll have to rewrite these ones - though it's much less work :)
On to the function - in
PlayingGameState
we want to act on keypresses so we need a function to look at the keyboard. We start with a simple skeleton function like so:
private void UpdateInput()
{
}
Then we need to put in some code to find out what's up with the keyboard and if Enter has been pressed.
private void UpdateInput()
{
DXInput.KeyboardState state =
inputDevice.GetCurrentKeyboardState();
if (state[DXInput.Key.Return])
{
/* Enter was pressed */
MessageBox.Show("It works");
}
}
Okay let's hook this up, at the bottom of the
Process()
procedure (or as it used to be called Render
) we add a call to the UpdateInput
procedure.
...etc
device.Present();
UpdateInput();
}
Okay now upon running and pressing enter a message box will pop up saying it works - cool we have some basic input stuff working - over different game states no less - well nearly.
Yes the pictured image says something different - that's because I can use my uberl33t programming skills to alter the code!
The final test
private void UpdateInput()
{
DXInput.KeyboardState state =
inputDevice.GetCurrentKeyboardState();
if (state[DXInput.Key.Return])
{
gameStateManager.switchState(new PlayingGameState(device,
gameStateManager,
inputDevice));
}
}
If you're looking at this thinking - wait my PlayingGameState constructor isn't like that - then I suggest you change it. I only made a brief comment early that it should be done. Just do the basic changes as in intialize the variables and expand the constructor and that's all that is required for now.
I think everyone must agree though the end result is pretty polished and cool. One could imagine a number of options on the start menu say 'Continue' where a new PlayingGameState would be created but perhaps with an extra arguement to load a saved game.
We have now learnt the crawling. We have our nice expandable archetiture in place and it works well. Maybe a little tidying up could be done with the input - possibly some kind of intermediate abstract class - but that could always be done in refactoring. It's quite tight and pleasent at the moment.
One could easily imagine encorparating things like intro - maybe the StateManager itself could have a number of fades and wipes included in it.
Next!
So what is next?!? How could we possibly top this. Well we're going to actual tile the window! That's correct! To do this we'll make a Tile datatype, something pretty simple and probably use an array to handle the map for now. Hopefully that shouldn't be too trying. Then we'll put in a man or something similar and use the keys to move him round the screen. If we really want to show off, we'll animate him though this means a small tangent into the world of TIME and then that will pretty much wrap up this section of making a tile based roleplaying game in Direct3D and C#.
No comments:
Post a Comment