Friday, February 25, 2005

Stage 5 - 2 : Animation, The Return




I think animation works pretty well in game. Code wise the implementation is a little sloppy. we want to load files that contain all the animation data, also with data for cutting a texture into frames. From this the code automatically generates the animation information.




Note that each time we do something like this we constrain what type of game this is going to be. This is a necessary requirement or we'd end up with no game at all, but it's worth noting the way we do things is not the only way nor necessarily the best.



Another new project!




I'm calling my new project Animate it will be empty and I'll simply copy over the code I was using before. Also I'll add the crap load of references we were making use of of.




Once you have it all compling we can begin the tinkering!



What are our raw materials?




We have


  • a bitmap usually defined in powers of 2 which becomes a texture

  • a vertex buffer where we can cut the texture up, and reference the frames by offset





To make this simple we don't want to create too much without testing and compiling but because this is such a leap we need to do a lot of reshuffling. Therefore we'll make some big architectural changes, while keeping the old logic too. Then we'll hard code inputs just to confirm that they work.



The plan




For memory reasons (that really don't apply to our game for the most part) it's handy to have a central collection of "big files" and then have everything else just have a reference to that memory address. For the most part C# takes care of this just by the way you must code, but sometimes unfortunately you have to think about it still.




Textures are big pieces of code, that's why we have the texture manager to care for them. The problem comes when we make frames. We cut the texture into slices, and push all the slices into a vertex buffer. Each slice starts from a given number we call the offset.



The Slices Data Structure



What it represents




The division of a texture into frames, or tiles or the little pictures. These in turn are mapped to 3D shapes. Namely vertices in the form of quads in our case. All these 3D shapes with the texture painted on are stored in the VertexBuffer. A vertex offset is used to say where each shape begins.



What wild and needless complicated scheme have you masterminded?




I must say this isn't some simple minor changes. In fact those weak of heart may wish to turn back now. This is the kind of change that causes rethinks and crying. It's the kind of change that sends ripples through the world. For those of you who've read a bit of Robin Hobb it's a bit like that, the kind of change that may well change our world. Okay enough scarying you - let's get down to buisness.




The slices code is currently "magic" that is it's all hard coded to support only our tile class. This is for simplicity purposes, we'll unmagic tile after we've confirmed it works for this one case. Bear this is mind when reviewing the below code though!





using System;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using System.Collections;

namespace Animate.Animation
{
///
/// A vertexBuffer that slices a texture into frames
///

public class Slices
{
private VertexBuffer vertexBuffer;
private Hashtable frames = new Hashtable();
int verticesNumber;


public Slices(Device device)
{
//MAGIC
verticesNumber = 4 * 2;
IntializeVertexBuffer(device);
}

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

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

//Add a tile shape to a VertexBuffer return the next free position
static private CustomVertex.PositionTextured[] AddTileShape(
ref CustomVertex.PositionTextured[] verts,
int addFrom)
{
verts[addFrom].SetPosition(new Vector3(0.25f,0,1f));
addFrom++;
verts[addFrom].SetPosition(new Vector3(0.25f,-0.25f,1f));
addFrom++;
verts[addFrom].SetPosition(new Vector3(0,-0.25f,1f));
addFrom++;
verts[addFrom].SetPosition(new Vector3(0,0,1f));

return verts; //Next free space
}

private void OnCreateVertexBuffer(object sender, System.EventArgs e)
{
frames.Clear(); // Clear the hash table

VertexBuffer buffer = (VertexBuffer)sender;

CustomVertex.PositionTextured[] verts
= new CustomVertex.PositionTextured[verticesNumber];


frames.Add("Stone", 0);
AddTileShape(ref verts, 0);


verts[0].Tu = 0.125f;
verts[0].Tv = 0; // Top Right Hand Corner

verts[3].Tu = 0;
verts[3].Tv = 0;

verts[1].Tu = 0.125f;
verts[1].Tv = 0.125f;

verts[2].Tu = 0;
verts[2].Tv = 0.125f;

int next = 4;
frames.Add("Grass", next);
AddTileShape(ref verts, next);

verts[next].Tu = 0.125f * 2f;
verts[next].Tv = 0; // Top Right Hand Corner

next++;
verts[next].Tu = 0.125f * 2f;
verts[next].Tv = 0.125f;

next++;
verts[next].Tu = 0.125f;
verts[next].Tv = 0.125f;

next++;
verts[next].Tu = 0.125f;
verts[next].Tv = 0;

buffer.SetData(verts, 0, LockFlags.None);
}

//offset for VB
public int GetOffset(string name)
{
try
{
return (int) frames[name];
}
catch(Exception e)
{
Console.WriteLine("error getting hash code: " + e.ToString());
return 0;
}
}

public VertexBuffer GetVertexBuffer()
{
return vertexBuffer;
}

}
}



Yes it's long. I've bolded some of the more interesting points. So what this does is for each different Texture we create a "Slice" where a slice is a few vertices in the vertex buffer and a number for where they begin. So we have two tiles, therefore we create two offsets. The offsets are named grass and stone. Using these names we can be render them at the correct time.



My head hurts what does it all mean




I'm far to lazy to draw the much needed diagram but I'm going to anyway because it's that complicated :D.



Pretty


Well I think this is testemant to not makng diagrams when you don't want to. It's very pretty but I don't think it gets the idea across. Currently we've not even got the above, rather we have a basterdized version that will only work with tile, and is working alongside tiles own game object rendering crap.




So we need a couple of more classes and a few changes. We're going to take it to the stage where this archiecture is used to render the tile sprites. Then with that working we'll generalize. Then we'll do it to the Actor and GameObject. Then we'll cut awat the dead meat. I know, no visual changes for ages, this is where that iron will comes in.




TextureSet ... why is this seperate - because I'm thinking too far into the future that's why now shut your pie hole. In truth it's probably a bad idea to have this seperate but we may want different slices of the same texture map (of course this could be done by adding more vertices and offset - i.e. I'm an idiot). For now all one and we'll all be very happy about this. So here's the code:




namespace Animate.Animation
{
///
/// Containing texture and frame information
/// As well as the number of times referenced
///

public class TextureSet : ISprite
{
private Texture texture;
public int refno;
private Slices slices;

public TextureSet(string loadPath, Device device)
{
texture = TextureLoader.FromFile(device, @"C:\plains.tga");
slices = new Slices(device);
refno = 1;
}
#region ISprite Members

public VertexBuffer GetVB()
{
return slices.GetVertexBuffer();
}

public int GetOffset(string name)
{
return slices.GetOffset(name);
}

public Texture GetTexture()
{
return texture;
}

#endregion
}
}



Yup it uses an interface and does the rather bad coding practice of passing on values that are passed on from another class. Close eyes sing alalal and we'll resculpt later when we actaully have some clay. Just believe me when I say that the system we put in place now will give us more flexibility with little cost to efficency.




The constructor is also rather hard coded - we ignore that for now too. We'll come back. What about this sprite interface? It's mainly so other classes don't have to deal with all the crap I've put in here, and to make sure that it's used correctly - this is very good programming style.



Let's have a look at ISprite




namespace Animate
{
public interface ISprite
{
VertexBuffer GetVB();
int GetOffset(string name);
Texture GetTexture();
}
}


Whoo, getOffset takes a tilename returns an offset for the sprite. Vertex buffer is the vetex data for the sprite.



From ground work to messy around with preexsting classes ( yes snappy I know)




public class TextureManager
{

static private ArrayList textureList = new ArrayList();
static private Hashtable textureSetTable = new Hashtable();

Yup, let's hit the old TextureManager get a few alternative functions and variables in there!




static public ISprite BufferTextureSet(string textureSetName)
{
TextureSet tS ;

if(textureSetTable.Contains(textureSetName))
{
tS = (TextureSet)textureSetTable[textureSetName];
tS.refno++;
return tS;
}
else
{
tS = new TextureSet(textureSetName, device);
textureSetTable.Add(textureSetName, tS);
return tS;
}
}




This code is pretty nice and is unlikely to change. Notice how cool the hashtable works compare to having to but in bools and stuff before to see if something was referenced a large number of times.




Next let's alter tile a bit so it can take all our new cool stuff.




public ISprite sprite;


public void setFlavour(string flavourName)
{

vbOffset = sprite.GetOffset(flavourName);
}



Hook these babies in. In the constructor we buffer our TextureSet, in the final code the map might do this and then pass the sprite to the tiles.




public Tile(Texture texture, Map mapIn) : base(texture, mapIn)
{
sprite = TextureManager.BufferTextureSet("Plains");
Tile.vertexBuffer = sprite.GetVB();
}



Speaking of map, we have to make a few changes to the GeneratePlain function as shown below:




public void GeneratePlain()
{
TextureManager.BufferTextures("Plain");
for(int i = 0; i < area; i++)
{
tiles[i] = new Tile(TextureManager.RequestTexture("Grass"), this);
tiles[i].setFlavour("Grass");
}

((Tile)tiles[22]).texture = TextureManager.RequestTexture("Stone");
((Tile)tiles[22]).setFlavour("Stone");
((Tile)tiles[22]).block = true;
}



Finally we need to change the PlayingGameState process bit. Note how messy the tile code is here, it should be in map and it should be using all the tiles Render function that they recieve from being a game object.



        //this is a hack!
device.SetStreamSource( 0, map.tiles[0].sprite.GetVB(), 0);

device.VertexFormat = CustomVertex.PositionTextured.Format;

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




Compile and it should run nicely, note though that I made all these changes in one big undocumented go - and me not being quite God may have missed one or two places. Please hunt down at your lesiure. If, I am in fact more divine that I suspect everything will be working and you'll be feeling a little scared and a little happy. So what's next - we need to carve out all the crap but first generalization is required. We need to be able load TextureSet files.



TextureSet files




For now these files are going to tell us how a particular texture file .tga is divied into a sprite class. How it's sliced up and what the names of the various bits are!




Simple first, how about we add IStorable to textureSet. We'll have simple read and write methods.




public class TextureSet : ISprite, IStorable



Okay we'll just put the skeletons in for now:




#region IStorable Members

public void Read(object o)
{
String inputFile = (string) o;

using (StreamReader sr = new StreamReader(inputFile))
{
}
}

public void Write(object o)
{
// TODO: Add TextureSet.Write implementation
}

#endregion



Remember to include using System.IO; as well. Now to load and save things we need a file path, but currently everythings rather hard coded, so let's work on making things a tiny bit more flexible - tiny steps!




private Slices slices;
private string path;

public TextureSet(string loadPath, Device device)
{
path = @"C:\" + loadPath + ".tga";
texture = TextureLoader.FromFile(device, path);
slices = new Slices(device);
refno = 1;
}



Okay we need to make one quick alteration to the loading code in Tile as well for this to execute succesfully.




public Tile(Texture texture, Map mapIn) : base(texture, mapIn)
{
sprite = TextureManager.BufferTextureSet("plains");



Okay, I know that we'll be loading a "texture set" file but for now we can make do with just loading a texture by name. So to the batmobile ... slash the write and read functions in TextureSet.




These functions will be super basic, much like the map functions they will slowly be updated as more flexibility is required.




I'm defining the files in such at way that the first line of the file is going to the path of the texture file we're going to be cutting up.




plains.tex
----------

\plains.tga
...



So i'm going to create a file in the root of C (I'm not a tidy hard drive person) and I'm calling it plains.tex and in it will be one line namely plains.tga.




At some point in the not-too-distant future we may wish to have some kind of editor. We may wish to save .tex files. We'll need to be able to store the texturePath therefore, so I'm going to stick in a string to hold such a thing. There are never going to be a create amount of texture sets so we can be pretty extensive with what odds and ends we might wish to include. Anyhoo add this:




private string path;
private string texturePath;



Then we'll make the read function look like so:



public void Read(object o)
{
String inputFile = (string) o;

using (StreamReader sr = new StreamReader(inputFile))
{
texturePath = sr.ReadLine();
}

}



So yes very simple. Let's edit te constructor and test if this is going to work!




public TextureSet(string loadPath, Device device)
{
path = @"C:\" + loadPath + ".tex";
Read(path);
texture = TextureLoader.FromFile(device, texturePath);
slices = new Slices(device);
refno = 1;
}



Works for me! Trying fully qualifying the path if you're having trouble, or stick the .tex file next to your executable. (probably created in the debug directory of your project).




So I think a matching save function should be added for completeness.




public void Write(object o)
{
String outputFile = (string) o;


using (StreamWriter sw = new StreamWriter(outputFile))
{
sw.WriteLine(texturePath);
}
}



We're striding towards flexability! Next I believe we will have frame after frame of information that slices will load. So let's add the old IStorable interface over there too.



CANNOT get enough OF the STORING groove




public class Slices : IStorable
{


Always remember the using System.IO jazz.



For now we're seperating the Reading code from the VertexBufferIntialization code. Just to make things clearer and simplier which is good. Clear and simple in this case comes wih a minor cost - we need to add another varaible. This s because we need some where to store the data we've read in before we use it to intialize the VertexBuffer. (If you're a real stickler you could have this intermediate as a return type, or set if to null after you've created the VertexBuffer). Okay let's see:




public class Slices : IStorable
{
private VertexBuffer vertexBuffer;
private Hashtable frames = new Hashtable();
private CustomVertex.PositionTextured[] verts;
private int verticesNumber;



Now for a new constructor. This constructor is going to read from a file stream (that is passed by TextureSet (after it's read the texture path)). It will read the number of Vertices that will fill in the VertexBuffer, then it will read in named sets of Vertices in the verts variable. These currently are assumed to come in fours.



After this data is read into the hashtable : frames and the verts. Then the VertexBuffer is created from the data that has been read in. As with all IO there could be many problems and we're assuming a perfect file and not-accounting for anything else.




public Slices(Device d, StreamReader r)
{
Read(r);
IntializeVertexBuffer(d);
}



Now it may be worth actually looking at the read function.




public void Read(object o)
{
frames.Clear();

StreamReader reader = (StreamReader) o;

verticesNumber = int.Parse(reader.ReadLine());

int offset = 0;

verts = new CustomVertex.PositionTextured[verticesNumber];

while(reader.Peek() != -1)
{
//Read in all the shape descriptions
string name = reader.ReadLine();
frames.Add(name, offset);

string option = reader.ReadLine();

if(option.Equals("tileshape"))
{
AddTileShape(ref verts, offset);
}
else
{
//Read four points manually
}

//Read texture data
float x,y;

x = float.Parse(reader.ReadLine());
y = float.Parse(reader.ReadLine());

verts[offset].Tu = x;
verts[offset].Tv = y;

offset++;

x = float.Parse(reader.ReadLine());
y = float.Parse(reader.ReadLine());

verts[offset].Tu = x;
verts[offset].Tv = y;

offset++;

x = float.Parse(reader.ReadLine());
y = float.Parse(reader.ReadLine());

verts[offset].Tu = x;
verts[offset].Tv = y;

offset++;

x = float.Parse(reader.ReadLine());
y = float.Parse(reader.ReadLine());

verts[offset].Tu = x;
verts[offset].Tv = y;

offset++;
}



}



It's obvious this is describing a file format, so here what a test file looks like:




\plains.tga
8
Stone
tileshape
0.125
0
0.125
0.125
0
0.125
0
0
Grass
tileshape
0.250
0
0.250
0.125
0.125
0.125
0.125
0



So the first line is read by TextureSet, then the rest is handled by slices. We have a small labour saving device here called "tileshape" if this string appears then we call the predefined function AddTileShape to fill in the positions.




So read sets up the Hashtable and the verts variable. Then IntializeVertexBuffer and OnCreateVertexBuffer make use of this data, let's check those functions now:




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

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


and




private void OnCreateVertexBuffer(object sender, System.EventArgs e)
{
VertexBuffer buffer = (VertexBuffer)sender;
buffer.SetData(verts, 0, LockFlags.None);
}



So we saved space in some places while expanding it in others. This base work seems pretty solid (although we really should merge the Slices and TextureSet classes but not now :P). Let's hook this up - higher up!



We need to make some additions to TextureSets Read function - and alter the constuctor slightly ...




Device device;

public TextureSet(string loadPath, Device d)
{
device = d;
path = @"C:\" + loadPath + ".tex";
Read(path);

refno = 1;
}



now the Read function.




public void Read(object o)
{
String inputFile = (string) o;

using (StreamReader sr = new StreamReader(inputFile))
{
texturePath = sr.ReadLine();

texture = TextureLoader.FromFile(device, texturePath);
slices = new Slices(device, sr);


}

}



That's everything, we're no longer hard coding everything - we're actually loading data from a .tex file! Running it everything should work wonderfully!



Getting Actor to accept sprite too




We're ignoring animation for now, all we want to do is get Actor based off sprite. Then we'll do animations. First we need one of those .tex files - at some these will be generated in an editor, or least creatable in a GUI. For now hard hard coding. So to save you some effort:




\playerrun2.tga
36
idle
base character shape
0.25
0
0.25
0.25
0
0.25
0
0
down1
base character shape
0.50
0
0.50
0.25
0.25
0.25
0.25
0
down2
base character shape
0.75
0
0.75
0.25
0.50
0.25
0.50
0
faceright
base character shape
1
0
1
0.25
0.75
0.25
0.75
0
right1
base character shape
0.25
0.25
0.25
0.50
0
0.50
0
0.25
right2
base character shape
0.50
0.25
0.50
0.50
0.25
0.50
0.25
0.25
faceleft
base character shape
0.75
0.25
0.75
0.50
0.50
0.50
0.75
0.25
left1
base character shape
1
0.25
1
0.50
0.75
0.50
0.75
0.25
left2
base character shape
0.25
0.50
0.25
0.75
0
0.75
0
0.50



So all strainght forward apart from we added another time saver called "base character shape" without having code to handle it :o. So maybe we should fix that up now. We need to editor Slices

Read function:




if(option.Equals("tileshape"))
{
AddTileShape(ref verts, offset);
}
else if(option.Equals("base character shape"))
{
AddBaseCharShape(ref verts, offset);
}

else
{
//Read four points manually
}



We now need to create the AddBaseCharShape(ref verts, offset); function. It looks like this:




static private CustomVertex.PositionTextured[] AddBaseCharShape(
ref CustomVertex.PositionTextured[] verts,
int addFrom)
{
verts[addFrom].SetPosition(new Vector3(0.25f,0,1f));
addFrom++;
verts[addFrom].SetPosition(new Vector3(0.25f,-0.50f,1f));
addFrom++;
verts[addFrom].SetPosition(new Vector3(0,-0.50f,1f));
addFrom++;
verts[addFrom].SetPosition(new Vector3(0,0,1f));

return verts;
}


Modifying Actor




First off let's add a sprite reference to the Actor class:




public class Actor : MapObject
{
protected PointF position = new PointF(0,0);
protected VertexBuffer vertexBuffer;
protected PointF[] boundary;
protected Sprite sprite;



Now let's put some hard codin' in the constructor and see what happens:




public Actor(Device device, Map mapIn, Texture actorTexture)
: base(actorTexture, mapIn)
{
//IntializeVertexBuffer(device);

sprite = TextureManager.BufferTextureSet("player");
vertexBuffer = sprite.GetVB();
this.texture = sprite.GetTexture();



All the offset stuff for animation is the same, it's just we're slowly moving in this new base work. (Notice because we're hard coding that the NPC reverts to the player texture - proof it's all working as we wish)



Altering Game Object




So we now have our new archectecture working for Actor and Tile ... but they're hacked in there at the moment. To cement them in we need tothrow out all the old code.




public abstract class GameObject
{
//public Texture texture; /**POOF**/
public abstract ISprite GetSprite();



If we make the above change we need to change the Render function too.




public void Render(Device device)
{
ISprite s = GetSprite();
QuadMatrix = Matrix.Identity;
device.SetStreamSource(0,s.GetVB() ,0);
device.RenderState.AlphaTestEnable = true;
device.RenderState.AlphaFunction = Compare.NotEqual;
device.SetTexture(0, s.GetTexture());
QuadMatrix.Translate(GetDXPosition().X,GetDXPosition().Y, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.DrawPrimitives(PrimitiveType.TriangleFan,vbOffset, 2);
}



So this kind of change is likely to break a few things - I find the easiest way to find them is to compile and hunt them down.




... of course, the first error to come up is that we have not implement "GetSprite" in Actor or Tile ...

oops.




public override ISprite GetSprite()
{
return sprite;
}



Add this to both.



Next Error: The Game Objects constructor, it requires a texture, we can remove that requirement now

so:




public GameObject(Texture gameObjectTexture)
{
texture = gameObjectTexture;
}


Can be changed to:





public GameObject()
{
}



These changes obviously fall down to the MapObject abstract class too so let's make changes there next:




public MapObject(Texture texture, Map mapIn) : base(texture)
{
map = mapIn;
}


too




public MapObject(Texture texture, Map mapIn)
{
map = mapIn;
}



We can also cut texture out too, let's do that now:




public MapObject(Map mapIn)
{
map = mapIn;
}



This will now filter down to the constructor of Actor and Tile and so they need changing too.




//In Actor
public Actor(Device device, Map mapIn, Texture actorTexture)
: base(actorTexture, mapIn)

changes too:
public Actor(Device device, Map mapIn, string TexSet)
: base(mapIn)



So we've made changes here we need to chase up too - i.e. the constructor of the actors. We'll therefore make some changes in the PlayingGameState constructor.




Player = new Actor(device, map, TextureManager.player);
NPC = new Actor(device, map, TextureManager.NPC);

to

Player = new Actor(device, map, "player");
NPC = new Actor(device, map, "npc");



Okay let's follow our debug tree back up and handle those Tile constructors




//Tile class

From:
public Tile(Map mapIn) : base(null, mapIn)
{
}

To:
public Tile(Map mapIn) : base(mapIn)
{
}



and the second constuctor:




public Tile(string TexSet, Map mapIn) : base(mapIn)
{
sprite = TextureManager.BufferTextureSet(TexSet);
Tile.vertexBuffer = sprite.GetVB();
}



This leads us into Map, we're doing simple - "let's make it compile changes" once everything settles we may do a little more shuffling! Yay! Maps creates some tiles, therefore we need to update these calls.




//Map class : GeneratePlain()

public void GeneratePlain()
{
TextureManager.BufferTextures("Plain");
for(int i = 0; i < area; i++)
{
tiles[i] = new Tile(TextureManager.RequestTexture("Grass"), this);
tiles[i].setFlavour("Grass");
}

((Tile)tiles[22]).texture = TextureManager.RequestTexture("Stone");
((Tile)tiles[22]).setFlavour("Stone");
((Tile)tiles[22]).block = true;
}

to:

public void GeneratePlain()
{
//TextureManager.BufferTextures("Plain");
for(int i = 0; i < area; i++)
{
tiles[i] = new Tile("plains", this);
tiles[i].setFlavour("Grass");
}


((Tile)tiles[22]).setFlavour("Stone");
((Tile)tiles[22]).block = true;
}



Note the TextureManager.BufferTextures("Plain");, map buffers the TexSets already, so we may change this later so the Tiles are just passed a sprite. Or stop the map buffering the TexSets



Reading and Writing




Yes we're still debugging, but we've moved into a new area. So why not have a nice new title break up that montony.




We enter the Read function of the Tile. Let's see what needs changing:





if(line1.Equals("Grass"))
{
/**POOF**/
//texture = TextureManager.RequestTexture("Grass");
setFlavour("Grass");

}
else if(line1.Equals("Stone"))
{
/**POOF**/
//texture = TextureManager.RequestTexture("Stone");
setFlavour("Stone");
}




You may notice we can compress the above. So let's do that!




//Tile class : Read : Compressing some internal code

string line2 = reader.ReadLine();

setFlavour(line1);

if(line2.Equals("True"))



Next up is writing the tile. We need to write the "name" of the texture. To do this we'll have to actually store the name earlier! Let's do this now.




//Tile class : Adding a variable, altering SetFlavour function

public class Tile : MapObject, IStorable
{
static public VertexBuffer vertexBuffer;
private bool Blocking = false;
private string flavour;

...

public void setFlavour(string flavourName)
{
flavour = flavourName;
vbOffset = sprite.GetOffset(flavourName);
}



Now we can write the name out for when we want to Save a tile.




//Tile class : Editing Write function

public void Write(object o)
{
StreamWriter writer = (StreamWriter) o;
writer.WriteLine(TextureManager.LookUpTexture(texture));

becomes:

public void Write(object o)
{
StreamWriter writer = (StreamWriter) o;
writer.WriteLine(flavour);


Back to Editing GameObject


we're going to remove two more abstract functions:




//GameObject class : Removing Abstract Definitions

//To provide instances for Static Vertex Buffers
public abstract VertexBuffer GetVertexBuffer();
public abstract void IntializeVertexBuffer(Device device);



Yup kill 'em then let's follow the path through all the inheriters.



Cleaning the Actor Class




So from the Actor class we remove the following:



//Actor class : Removing the vertexBuffer

public override VertexBuffer GetVertexBuffer()
{
return vertexBuffer;
}


Then we need to remove a load of other VertexBuffer functions:




//Actor Class : Removing the following functions

public override void IntializeVertexBuffer(Device device)
{
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),
36,
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)
{
Vert
...extremely long details removed



Mmmm thats good makes the Actor class a lot leaner, while we're here we should remove the following variable:




//Actor Class : Remove VertexBuffer variable

protected VertexBuffer vertexBuffer; /**BYEBYE**/



Then we have to also remove a line from the constructor:




//Actor class : Removing a line (possibly adding TexSet to if not alreay done)
public Actor(Device device, Map mapIn, string TexSet)
: base(mapIn)
{

sprite = TextureManager.BufferTextureSet(TexSet);
vertexBuffer = sprite.GetVB(); /**POOF**/


Cleaning the Tile Class



Let's kill this function:




//Tile Class : Removing GetVertexBuffer procedure#

public override VertexBuffer GetVertexBuffer()
{
return Tile.vertexBuffer;
}



Then we need to kill all the others too! Kill them all! Remove the following ones:




//Tile Class : Removing various functions

static private void OnCreateVertexBuffer(object sender, System.EventArgs e)
{
...
}

public override void IntializeVertexBuffer(Device device)
{
...
}




Let's also remove a variable!




//Tile Class : Removing a vertexBuffer variable

public class Tile : MapObject, IStorable
{
static public VertexBuffer vertexBuffer; /** POOF **/



Now we must change the constructor by a line:




public Tile(string TexSet, Map mapIn) : base(mapIn)
{
sprite = TextureManager.BufferTextureSet(TexSet);
Tile.vertexBuffer = sprite.GetVB(); /**POOF**/
}



The next change happens all the way over in PlayingGameState once we where using a central tile vertex buffer as a static, now it's all taken care of automatically. Overall it's probably more efficent and definetly easier to code with. I can think of anumber of optimiaztions but I won't put them in unless I need them.




//PlayingGameState Class : The constructor : Removing the following lines

Tile t = new Tile(map); /**POOF**/
t.IntializeVertexBuffer(device); /**POOF**/



That's pretty much it, the last thing to do is create a nre .tex file called "NPC.tex" and put in C or where ever you're storing your files. It's a copy of player with a minor change to which texture should be used.




\NPC2.tga
36
idle
base character shape
0.25
0
0.25
0.25
0
0.25
0
0
down1
base character shape
0.50
0
0.50
0.25
.
.
.
etc


Getting Loading and Saving Up to Speed




This requires a small change in the Map Write function. Let's see what the changes look like:




//Map Class : Write Function : Removing a line and editing another

//TextureManager.BufferTextures("Plain"); /**POOF**/


tiles = new Tile[area];
for(int i = 0; i < area; i++)
tiles[i] = new Tile("plains", this);


Cleaning Texture Manager




Easy! Everything is pretty much back where it was. We need to take a machete to TextureManager though, so let's do that now. Remove the following functions:




//TextureManager class : Removing functions

static public string LookUpTexture(Texture t)
{
...
}

static public Texture RequestTexture(string textureName)
{
...
}

static public void BufferTextures(string textureSet)
{
...
}



And let's edit the SetDevice a little




static public void SetDevice(Device d)
{
device = d;
player = TextureLoader.FromFile(device, @"C:\playerrun2.tga");
NPC = TextureLoader.FromFile(device, @"C:\NPC2.tga");

}

becomes:

static public void SetDevice(Device d)
{
device = d;
}



Things are becoming much tidier and cleaner :D Yay! So let's deal with the variables while we're here:




public class TextureManager
{

static private ArrayList textureList = new ArrayList(); /**POOF**/
static private Hashtable textureSetTable = new Hashtable();


static public Texture player; /**POOF**/
static public Texture NPC; /**POOF**/
static private Device device;
static private bool Plain = false; /**POOF**/

Which results in:

public class TextureManager
{

static private Hashtable textureSetTable = new Hashtable();
static private Device device;



So that's all looking pretty sweet.



One last change then I swear we'll do something different




So currently the Slices read just read until the file ends. This is okay but what if we want to put something at the end of the file (animation information for example, hint hint). We only want to read in the vertices expected by the number at the top of the file so we make the following code changes




//Slices Class : Read Function : Altering the while loop


int offset = 0;

verts = new CustomVertex.PositionTextured[verticesNumber];

while(offset < verticesNumber)
{
string name = reader.ReadLine();
frames.Add(name, offset);


Animate the trousers




We have our cool texture set loader that does everything for us. At some point we need some kind of cool GUI to design monsters and NPcs and stuff but Game Design tools really aren't a pressing concern, first we need at least some semblence of a game.




At the start of this text I promised some major reshuffling, which we've done and very well too I might add. The final step for this piece is to support loadable animations. This like most things we have done of late isn't the easiest things in the world - so we'll only define one goal, that being that it works. Fanciness can come later!




So an animation is going to have:


  • A name

  • Array of Offsets

  • Current index in Array





That's right sounds like new class time. We seemed to have called the namespace .Animation I don't know if we can have a class with the same name - one way to find out ... and no we can't so we need a different name. We'll use the word Anima it gives the same feel.




Here it is in all it's loveliness:




public class Anima
{
private int[] offsets;
public string name;
private int offsetIndex = 0;

public Anima(string animaName, int[] arrayOfOffsets)
{
name = animaName;
offsets = arrayOfOffsets;
}

public int Advance()
{
offsetIndex++;
if(offsetIndex >= offsets.Length)
offsetIndex = 0;

return offsets[offsetIndex];
}

public int Reset()
{
offsetIndex = 0;
}
}



So as you know we use clock to call animations periodically and on this occasion we'll call advance and update the vertexBufferOffset used by the current sprite. It would be nice to have a little more control over the time but for now this will do!




So a texture set could have a few different animations. What exciting data structure are we going to use? Why a hashtable, if only for the fine convience of accessing things by their name. (I know it's not as efficent as a pointer or number but I don't care so nyah)




So let's amend Slices:




private int verticesNumber;
public Hashtable Anims = new Hashtable();



Hash table seems good, now to add the Animation reading code!




//Class Slices : in Read method : Adding animation reading ability.
offset++;
}

//Read in animations
if(reader.Peek().Equals(-1))
return;
else
{
//Animations Ahoy
while(!reader.Peek().Equals(-1))
{
int[] tempOffsetArray;
int length;
string name;

name = reader.ReadLine();
length = int.Parse(reader.ReadLine());

tempOffsetArray = new int[length];

for(int i = 0; i < length; i++)
{
string getName = reader.ReadLine();
tempOffsetArray[i] = (int)frames[getName];
}

Anima a = new Anima(name, tempOffsetArray);
Anims.Add(name, a);
}
}



}



To test out this new loading code we'll need to append some data at the end of our player class, which is probably for the best as it's looking rather ominous at the moment:



Den den dur!


So to get increase the filesize we'll add one test animation by appending these lines:




walking down
3
idle
down1
down2



Yes I am quite aware how cool and groovy it is to use the individual frame names to create the animation.




That works with out a hitch but we can't really test it :( ... yet. First we need to add the following function to the TextureSet class.




//TextureSet Class : Add the function below

public Anima GetAnimation(string name)
{
return (Anima) slices.Anims[name];
}



Then in ISprite we add an abstract for the above function




//ISprite Class : Adding the following prototype function

Anima GetAnimation(string name);




Then we can try out the walking down animation in Actor. So let's have a look a Animate() in Actor. We'll just be altering the walking down animation as a test.




private void Animate()
{
switch(currentState)
{
case States.standing:
{
vbOffset = 0;
}break;

case States.down:
{

vbOffset =
sprite.GetAnimation("walking down").Advance();
}



Compile and it should all work perfect! Yay animations no longer have to be hard coded. They're pretty simple at the moment but for the forseeable future they should be fine! There is also no error checking, so that also needs to be addressed.




It's really simple to add the rest of the animations the bottom of player.tex should look like so:




walking down
3
down1
idle
down2
walking right
3
right1
faceright
right2
walking left
3
left1
faceleft
left2



The code to support this in Actor looks like so:




switch(currentState)
{
case States.standing:
{
vbOffset = 0;
}break;

case States.down:
{
vbOffset =
sprite.GetAnimation("walking down").Advance();
}break;

case States.right:
{
vbOffset =
sprite.GetAnimation("walking right").Advance();
}break;

case States.left:
{
vbOffset =
sprite.GetAnimation("walking left").Advance();

}break;
}



Yes I still haven't put a walking up animation or made the right and left ones look at all respectable.



And that's animation code - wasn't too hard at all really! Still it's very basic and very fragile but its enough to keep us going for now!



Improvements




  • Read function should read the actual vertex number and not just assume there is four

  • How would a map support a two different texture sets during intialization

  • TextureSet and Slices should probably be a single class




None of these are too pressing but it's worth making a note so we can come back later when we're doing some polishing!

2 comments:

Anonymous said...

Wow,
That was much more difficult thant the others. I have just one problem with the Loading file of the map here :
[CODE] public void setFlavour(string flavourName) {
flavour = flavourName;
vbOffset = sprite.GetOffset(flavourName);
}
[/CODE]
There isn't any SetOffset (for the map objects), can it be the problem?

And it seems you have paste the 2 map texture in one. It is a good idea to mention it :)

Unknown said...

First of all: great tutorial. This part however was a little unclear to me. The beginning part with the slices class was missing a mention of the changed bitmap.

It seems you've created a texture map of 8x8, so 256x256px. Is that correct, or did I make a mistake somewhere?