Tuesday, February 22, 2005

Walking: First Steps




The architecture is down and looking pretty good. Now we need to work on our playing game state and get those tiles covering the screen. This is a reasonably large shift of focus so it's a good idea to start a new project. We can jus copy all the previous files over and start from where we left off.




Quickly Getting us to the First Stage



If Vs.net isn't booted up then do so. File>New Project and let's think of a snappy name: 'FirstStep'. A brand new empty project, we'll copy over all the .cs files from our previous project - then select Project>Add Exsiting Item and select all those files we've put in the directory.




Then we need to add all the references! And for the final touch I'm going to rename the main class names so it's sits better with our New Project name (i.e. 'Crawl' -> 'Step'). This should provide us with a new fully featured project from which to start our cool new changes.




Soon I'll read some basics about Source Control and there's probably some way to avoid all this hoo-har



Let's try a Tile Class




Okay what do we need from a tile class - well it needs to store shape information and texture. All tiles are going to be square so we can have a static VertexBuffer. Tiles are mainly going to change by texture so we can have a reference to a texture. Possibly we could also put in an emueration but this smacks of hard coding a soultion - which is sometimes a good thing, in this case I feel it's best avoid. We'll stick with these two things and see what happens.



Add a new .cs file that we'll call 'Tile' and here we'll develop the Tile Class.




using Microsoft.DirectX.Direct3D;

namespace FirstStep
{
public class Tile
{
static public VertexBuffer vertexBuffer;


}
}



The VertexBuffer is static meaning that all tiles will share the same VertexBuffer, which will be intialized using a static function, that we'll call IntializeVertexBuffer.




static public void IntializeVertexBuffer()
{

}



The code for this we'll (for now) copy over from the PlayingGameState class. When everything works and we tidy up at the end we can delete the code from PlayingGameState. So copy paste we'll have something like below:




static public void IntializeVertexBuffer(Device device)
{
device.RenderState.Lighting = false;
vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionTextured),
4,
device,
0,
CustomVertex.PositionTextured.Format,
Pool.Default);

vertexBuffer.Created += new System.EventHandler(OnCreateVertexBuffer);
OnCreateVertexBuffer(vertexBuffer, null);
}

static private void OnCreateVertexBuffer(object sender, System.EventArgs e)
{
VertexBuffer buffer = (VertexBuffer)sender;

CustomVertex.PositionTextured[] verts = new CustomVertex.PositionTextured[4];
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));

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);
}



When will we want the tiles to have a structure? When we start up the game - so we should put a call to IntializeVertexBuffer into the constructor of PlayingGameState.




public PlayingGameState(Device d, GameStateManager g, DXInput.Device dInput)
{
device = d;
device.DeviceReset +=new System.EventHandler(deviceReset);
deviceReset(d, null);

Tile.IntializeVertexBuffer(device);

gameStateManager = g;
inputDevice = dInput;

LoadTextures();
}



Textures need to exist outside the tiles - this way they can be referenced. Currently we have no data structure to handle the texture storage. I don't want to get into building one right now so we'll leave all texture information in PlayingGameState. In the tiles we'll just have a reference to a texture. So we add a new variable:




public class Tile
{
static public VertexBuffer vertexBuffer;

public Texture tileTexture;

...etc



While we're here let's add a constructor - the construtor should accept a texture that the tile will use. So something simple like this:




public Tile(Texture texture)
{
tileTexture = texture;
}



And for now that's pretty much our tile class, now we should use it. We'll be using it in the PlayingGameState class. We need some kind of structure to store the map - we'll be using an ArrayList for simplicity. We need to add using System.Collections; and then we add to the variables the following:




...
private Texture stoneTexture;
private Device device;

private ArrayList Map;

private GameStateManager gameStateManager;
...etc



We need to create an appropiate map - in a real game we'd most likely be loading the data from disk. In this case we'll just throw in some random data. Let's do some arthemtic first - the tiles are 0.25 by 0.25, our window is 2 by 2. So we can fit 8 tiles along the top and 8 along the side - so in total we'll have 8x8 tiles and need an array of size 64. With this knowledge in mind let's create an IntializeMap function.




private void IntializeMap()
{
Map = new ArrayList();
/**
* Bad Code warning!
* Were does the magic number 64 come from?
* Tiles are .25 x .25 and 64 can fit in our 2 by 2 window
**/
for(int i = 0; i < 64; i++)
{
Tile t = new Tile(grassTexture);
Map.Add(t);
}
}



(We're counting from 0 so that's why it's do until less than 64, rather than do until equals 64)
Okay now we need to add this to the constructor as well.




...
gameStateManager = g;
inputDevice = dInput;

LoadTextures();
IntializeMap()
}



Right we have our tiles, all that needs to be done is to mess with the Render loop to get these tiles laid down.




device.BeginScene();

device.SetStreamSource( 0, Tile.vertexBuffer, 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;

float x = -1f; //Remember we're using Cartesian
float y = 1f;
for(int i = 0; i < 64; i++)
{
Tile t = (Tile) Map[i];
QuadMatrix.Translate(x,y, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, t.tileTexture);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);

if(((i+1) % 8) == 0)
{
x = -1f;
y -= 0.25f;
}
else
{
x += 0.25f;
}
}

device.EndScene();



Most of the stuff there - i.e. code to plot two tiles, has been removed or moved. Now we loop through an array of 64 tiles and place each tile. We set the StreamSource as the Tile shape which all tiles use. Then we enter a loop - we put a tile down and then we move over the length of one tile and place one next to it. That is unless we have just placed a row of eight tiles.



How do we know when we've placed a row of eight tiles?




[ 0][ 1][ 2][ 3][ 4][ 5][ 6][ 7]
[ 8][ 9][10][11][12][13][14][15]
...
[56][57][58][59][60][61][62][63]



We are counting from 0 not 1, and we're putting down rows of 8 tiles at a time. So if we add 1 to the last column we get all the multiples of 8. 8 divides into them with no remainder. So that explains:



if(((i+1) % 8) == 0)


okay so what's that funny little percentage symbol? well that's modulus and is quite useful. It divides (i+1) by 8 and then gives the remainder. If 8 divides perfectly into (i+1) then we know that we have the end of a row and that the remainder will equal zero.




If this is the case then we move x back to the far side of the screen -1f (remember we're using Cartesian Coordinates) and remove a tile length from the y value (remember we are using Cartesian Coordinates - to go down you take away from y).




In this manner we tile the entire window. Because of the thoughtful way we've coded we can expand and resize the window and everything inside resizes with it - wunderbar!



A grass field


Look at our beautiful grass field - it obvious the texture does not tile as well as it might have been hoped. It would probably look better if you stole some tiles from a commerical game!



Tidying Up




We now need to remove a load of the junk left over in the PlayingGameState class that was handling tiles.




From the variables we can remove the line private VertexBuffer vertexBuffer. From the constructor we can remove these two lines:




device.DeviceReset +=new System.EventHandler(deviceReset);
deviceReset(d, null);



The entire function deviceReset can be removed. As can the the OncreateVertexBuffer function. This results in the code being a lot cleaner and more focused though the PlayingGameState is still acting as a Map class and a TextureManager class.



Temptations to Resist




You may wish to put the Map information and the Rendering of the map into the Tile class I would advise against this. The Tile class represents a single tile, while the map represents a collection and should really have its own class - currently the bet place for it, is where it is.



Player Character




Before movement we need something to move so I have slaved away in photoshop to generate something that resembles a sprite. At this I cannot imagine being able to animate in even a vaguely satisfactory way. But oh well - so we have our sprite.




It's saved in TGA because this allows us to save ... ALPHA INFORMATION! Yes, that means transparency ahoy. Please do the conversion to tga yourself because I can not host it :(



Super excellent sprite!!


Our hero will for now go in reside in the PlayingGameState class. So we'll need a VertexBuffer and all the stuff that goes along with it. Much of this we can rip off from the TitleScreenState. So let's start with the variables:




private Texture grassTexture;
private Texture stoneTexture;
private Texture spriteTexture;
private VertexBuffer vertexBuffer;



A note here - the Sprite itself is taller than it is wide therefore this must be represented in the VertexBuffer. Apart from that these are merely copied directly across. So we'll have a quick look as those functions now:




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(0.25f,0,1f));
verts[1].SetPosition(new Vector3(0.25f,-0.50f,1f));
verts[2].SetPosition(new Vector3(0,-0.50f,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);
}



Okay we need to make some calls to these functions and we do that in the constructor. So all basic stuff that we've done before:




public PlayingGameState(Device d, GameStateManager g, DXInput.Device dInput)
{
device = d;
Tile.IntializeVertexBuffer(device);
device.DeviceReset +=new System.EventHandler(deviceReset);
deviceReset(d, null);


gameStateManager = g;

...etc



Then we need to make sure the Texture is loaded. This bit is a bit different than the method we used to load bitmaps infact it's shorter and easier! We could load the bitmaps this way also if the need took us.




...
System.Drawing.Bitmap stoneBitmap = (System.Drawing.Bitmap);
System.Drawing.Bitmap.FromFile(@"C:\stone.bmp");

spriteTexture = DXGraphics.TextureLoader.FromFile(device, @"C:\sprite.tga");
...etc



This time the texture file is loaded directly into the Texture datastructure. Now we have everything ready to present to the screen so the last thing left to do is fiddle a bit with the render loop. Our texture file has taken the Alpha information that was stored in the tga automatically.




Just after the tiling loop in the Process function we need to stick in this chunk:




device.SetStreamSource(0, vertexBuffer,0);
device.RenderState.AlphaTestEnable = true;
device.RenderState.AlphaFunction = Compare.NotEqual;
device.SetTexture(0, spriteTexture);
QuadMatrix.Translate(0,0, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);



So we flick the StreamSource to our sprite. We enable AlphaTest which means eash pixel is tested to see if it should be transparent. The function that this test takes is NotEqual, I'm quite sure why but thats how it is. Then we set the new texture, but him somewhere central in our field and then draw him.



Looking good

Now I don't know about you, but to me this is starting to look like a must own game!



Encapsulating the Sprite in a sprite class





There's a few things to think about before diving in to this



Animation



one is animation. Something worth baring in mind - how is going to be achieved, well we can just have a number of different textures and call setTexture a lot, or we can have a big texture and load in a load of vertices that are mapped to different parts. Then depending on the frame give draw primatives different arguments. This probably seems the wisest approach. We won't be adding animation just yet though.



Position




Another is position are we going to have the position in tile co-ordinates so x = [1..8] and y = [1..8], or pixel coordinates so it could be anywhere in our view port? These are fine grain questions and it comes down to what type of game you want. It is generally best to avoid generalizing and attempting to 'have it all' because the work load becomes overbearing even with the best motivation enthusasm will wane.



Abstraction




Sprites are just one type of the many object we have in our game, we want to avoid rewriting the same code. So can we throw in interfaces, abstract classes and the like.

Refactoring



If we make any mistakes we can always refactor. I tend to do this when I sart a new project build. You have to look at your code, like it asn't your child and mercilessly point all the places where it sucks and then reorder and rebuild them to be better.






K.I.S.S.




Okay a basic class for object may look something like below.




public class GameObject
{
protected Texture texture;
protected VertexBuffer vertexBuffer;
protected Device device;
protected Matrix QuadMatrix = new Matrix();

public float posX, posY = 0; //DirectX Position i.e. middle of the screen
//There should be queries to retrieve the above!
//Tiles will be queried through Map


public GameObject(Device d, string texturePath)
{
device = d;

try
{
texture = TextureLoader.FromFile(device, texturePath);
}
catch(Exception e)
{
MessageBox.Show("Error loading Actor texture: " + e.ToString(), "Error Creating Actor");
}
}

public void Render()
{

QuadMatrix = Matrix.Identity;
device.SetStreamSource(0, vertexBuffer,0);
device.RenderState.AlphaTestEnable = true;
device.RenderState.AlphaFunction = Compare.NotEqual;
device.SetTexture(0, texture);
QuadMatrix.Translate(posX,posY, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);

}


}



The only slight worry I have with the above is that evey object is going to have a vertexbuffer is might be better if they all shared one - but this will do until a problem of speed arises.




So a game object has a VertexBuffer to store it's shape, it has a texture, a reference to the device so it can dispay itself. A matrix to use to give's it's position in the Render procedure. We also need some kind of setup but this is going to be different for different type of objects.




Currently this suits our needs but its something that we may need to revise later. For now let's have GameObject as an abstract class.




public abstract class GameObject
{



Also let's stick in an abstract intialization function.




public abstract void IntializeVertexBuffer();



Now we make a new class called Actor which inherits from GameObject and we'll store everything in here and finally encapsulate our sprite. So same ol' new class new .cs file.




public class Actor : GameObject
{
public Actor(Device device, string texturePath) : base(device, texturePath)
{

}

public override void IntializeVertexBuffer()
{

}

}



For now we stop all our elegant inheritance and we just want to be able to use Actor to render and control the particular sprite we have developed. So let's start copying and pasting.




public override void IntializeVertexBuffer()
{
device.DeviceReset +=new System.EventHandler(deviceReset);
deviceReset(device, null);
}

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(0.25f,0,1f));
verts[1].SetPosition(new Vector3(0.25f,-0.50f,1f));
verts[2].SetPosition(new Vector3(0,-0.50f,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);
}

}



That's some nice copy paste action. Let's see if it works, a quick test in the process function of PlayingGameState like so:




/**
device.SetStreamSource(0, vertexBuffer,0);
device.RenderState.AlphaTestEnable = true;
device.RenderState.AlphaFunction = Compare.NotEqual;
device.SetTexture(0, spriteTexture);
QuadMatrix.Translate(0,0, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
**/

Actor a = new Actor(device, @"C:\sprite.tga");
a.Render();



Yay it does work that's good to know. Now let's clean up playing game, state, first we can remove what I brackets out above. The intialization line for Actor should deleted and will write it in a better manner. Get rid of the variables:







private Texture spriteTexture;
private VertexBuffer vertexBuffer;



Also get rid of the code the loads the .tga file into the spriteTexture variable. It's in the LoadTextures procedure.




Replace with the variable: private Actor Player; . Then in the constructor we need to intialize this so at the end of the constructor we should slip in the following line of code:




Player = new Actor(device, @"C:\sprite.tga");



While we're in the constructor we can remove the old Sprite setup code:




device.DeviceReset +=new System.EventHandler(deviceReset);
deviceReset(d, null);



Then in the Process procedure remember to change 'a' to 'Player'. Then to finish tidying up we can get rid of deviceReset and OnCreateVertexBuffer. This makes PlayingGameState look a little leaner.



MOVEMENT!




We can add basic non-tile based movement really really easily now. So let's do it. Go to the function private void UpdatePosition and make it look like below:




private void UpdateInput()
{
DXInput.KeyboardState state = inputDevice.GetCurrentKeyboardState();
if (state[DXInput.Key.Return])
{
/** Enter has been pressed **/
}

if(state[DXInput.Key.RightArrow])
{
Player.posX += 0.1f;
}

if(state[DXInput.Key.LeftArrow])
{
Player.posX -=0.1f;
}

if(state[DXInput.Key.UpArrow])
{
Player.posY += 0.1f;
}

if(state[DXInput.Key.DownArrow])
{
Player.posY -= 0.1f;
}
}




Okay now run it! - Now that's pretty fecking cool. Try holding two keys at once and he'll move diagonally. Speed is quite fast but we haven't done anything about Time yet. Also there is no animation - once again this is because we're missing Time and because I'm too lazy to try and draw all those frames. But currently it's looking pretty damn good.






So what happen is you press a Key, gameState see's this and it moves the player. We could do things another way and have the player handle his own input but that's not what we have chosen for the moment. We alter the players position by 0.1 so this is quite a reasonably chunky step that the guy is taking but that's easy to reduce and really should be stored somewhere.



Where are we now?




I think this ends a reasonably good chunk of building your own CRPG. We've learnt a lot and with a little more refining now and then this basic code could provide you with an excellent base to build your own RPG.



What next?




Well currently the program is currently crying out for time, so we'll get timing in and make it fit with the concepts of states. Then we'll have a good look at what has been written so far and see if it can't be put in a more elegant form that will make it easier for us to progress further.


References



  • Direct X 9 C# KickStart

  • Focus on 2D in Direct3D

Post a Comment