Stage 5 - 3 : Bush and a Rock
What we are going to do!
In this section we're going to add some eye candy, notably a bush and a rock!
- Cleaning, always cleaning!
- Sorting Animation and refining the TextureManager
- Lots of other good stuff
Do we have tiles hold gameobjects too? Tile based objects this is probably a good idea.
After I reach stage 1 of my Masterplan I will construct a vaguely pretty (I say pretty here when I more accurately mean is won't be drawn in MSPaint using only the colours red and yellow). So I've added ROCK to the plain tile set, though it's not a tile on it's own, it's more like tile eye candy. So the question is do we have it as a "GameObject" or do we have it as tile object. If it's a tile object the collisions can be done so much more easily so ... let's do that!
First you may need a hand adding it to the tile class. It's not the easiest thing in the world at the moment we really need to have a go at some groovy tools soon. Add a nice ROCK image next to the grass and stone tiles. I've drawn mine like so:
I'm not putting the .tga here because I'm lazy muhahaha ... maybe when / if I review these pages.
Okay we need to do things ... first let's update the old .tex file so our fantastic game engine can handle this new "tile". Super extra fantastic. We need to append the following:
Rock
tileshape
0.375
0
0.375
0.125
0.250
0.125
0.250
0
Also if you've given your new texture set a new name (I've called mine plains2.tga) then thou must remember to change the reference at the top of this file. Get some pratice about how the computer must feel you ungrateful swine.
That's it it's loaded in and working (try it out by setting a tiles flavour to "Rock" and then amaze your friends. So if you do that you'll note the black background - that's right we need to have this "tile" placed on an actually tile. But how Holmes how?! Well we'll give the class tiles a shot in the arm that will bring it shooting into the tehnological future! We'll make an arraylist of occupiers! (which will be oh so private!)
First some 'shuffling'
Yes getting to hate that word aren't you?
Basically the selfish tile class has a monopoly on blocking code! We're going to bump the blocking stuff up to the map object and let it take care of it, so then actors can have their blocking info turned off and the like.
First to the MapObject
class.
//MapObject Class : Adding some stuff
public abstract class MapObject : GameObject, IComparable
{
protected Map map;
protected bool blocking = false;
public bool block
{
set
{
blocking = value;
}
}
abstract public bool IsBlocked();
So this is all cut and pasted over from the Tile class. Apart from the IsBlocked function which is just made into a prototype for the MapObject class. Also a changed the B on blocking to lower case so it fits my coding style, which results in a few changes to the Tile and Actor class too - we need to add the following function to both:
//Tile and Actor Classes : Add this
public override bool IsBlocked()
{
return blocking;
}
Okay the blocking flag - isn't taken into account yet by the Actor class so we should fix that while it's still fresh in our memories.
First let's make sure our default values are up to scratch:
public Actor(Device device, Map mapIn, string TexSet)
: base(mapIn)
{
blocking = true;
Most actors are going to block so we make that standard. We also need to handle the case where an actor might now be blocking, this occurs in the obstruction code - therefore in the Map class - let's check it out.
public bool movementObstructed(PointF[] boundary, GameObject focus,
PointF pos)
{
bool intersect= false;
foreach(GameObject g in MapObjects)
{
if (g.Equals(focus))
continue;
We have this so we need to attempt to cast to MapObjects and then check blocking stuff. First we check if the focus is a blocking by doing the following:
public bool movementObstructed(PointF[] boundary, GameObject focus,
PointF pos)
{
bool intersect= false;
MapObject m;
m = focus as MapObject;
if(m!=null)
{
if(!m.IsBlocked())
return false;
}
foreach(GameObject g in MapObjects)
{
Then we'll do it for each GameObject too.
foreach(GameObject g in MapObjects)
{
if (g.Equals(focus))
continue;
m = g as MapObject;
if(m!=null)
{
if(!m.IsBlocked())
break;
}
Okay that's the blocking code in for Actor and Tiles now - great!
We can do a quick test by turning the NPCs blocking off and seeing if we can walk through him!
//PlayingGameState Class : Constructor
Player.SetDXPosition(0.5f,0.5f);
map.AddMapObject(Player);
map.AddMapObject(NPC);
NPC.block = false;
}
and we can run through him again - truely GURU programming. Now let's get back to the problem of adding that rock!
Occupancy List
We'll allow any items on the old occupancy list, any items that inherit from GameObjects, that is. So let's do that now! Let's go to the tile class!
//Tile Class adding variables
public class Tile : MapObject, IStorable
{
private string flavour;
private ArrayList Occupants = new ArrayList();
public ISprite sprite;
We need some Add/Remove code now too.
Blocking is a state that might change, for instance a door way might open and go from blocking to not blocking, the tile needs to consider this fact about it's occupiers list! So there's every reason we should be considering whether things block - each loop or each time obstruction is called. So we're going to write the following functions:
//Tile Class
public void AddOccupant(GameObject g)
{
Occupants.Add(g);
MapObject m = g as MapObject;
if(m != null)
{
if(m.IsBlocked())
this.blocking = true;
}
}
public void RemoveOccupant(GameObject g)
{
Occupants.Remove(g);
}
public bool DoOccupantsBlock()
{
bool local_blocking = blocking;
foreach(MapObject m in Occupants)
if(m.IsBlocked().Equals(true))
local_blocking = true;
return local_blocking;
}
The we modify the IsBlocked function like so:
public override bool IsBlocked()
{
blocking = DoOccupantsBlock();
return blocking;
}
So we have a cool new occupancy list - what next. Well how should rocks really be represented - a scenery, eyecandy or item class. Well probably but I fancy making the rock and actor for now and so that's what we're going to do. Cos we can have anything that's a a GameObject - and god help me I'm feeling a little bit crazy!
But maybe we're running ahead of the old hypothetical horse here - first we need to be renderin' these Occupiers! Currently the Tile rendering code is a mess, it's all in the GamePlayingState, it's like map has it's intestines half inside and half outside it's body - it's not nice. But for the moment we don't care we just wanna see that fine fine rock on a nice tile. So let's do this the quick and dirty way!
We have to turn this public:
public class Tile : MapObject, IStorable
{
private string flavour;
public ArrayList Occupants = new ArrayList();
Then over in PlayingGameState we have to do this:
map = new Map(8,8);
Player = new Actor(device, map, "player");
NPC = new Actor(device, map, "NPC");
Actor rock = new Actor(device, map, "plains");
Player.SetDXPosition(0.5f,0.5f);
map.AddMapObject(Player);
map.AddMapObject(NPC);
map.AddMapObject(rock);
}
First we're adding the Actor to the GameObjects just because we want to take small steps.
On running this you may notice everythin going odd - it's because idle state for actors is 0, always! :o
Then notice how there's a flashing tile, it's an error due to the idle state in Actor assuming the idle state will always be true. This is not necessarily correct so this is something we'll change.
To do this require a bit of minor tinkering with actor. In the future we'll need to pull out the big guns and make an animation/state class that can be loaded into Actors. For now we'll simply overload the constructor.
public class Actor : MapObject
{
protected PointF position = new PointF(0,0);
protected PointF[] boundary;
protected ISprite sprite;
int idle = 0;
That's going to be the offset for idle.
private void Animate()
{
switch(currentState)
{
case States.standing:
{
vbOffset = idle;
}break;
Then we add this constructor:
public Actor(Device device, Map mapIn, string TexSet, string Idle)
: base(mapIn)
{
blocking = true;
sprite = TextureManager.BufferTextureSet(TexSet);
Clock.AddTimeEvent(
new TimeEvent(95, new TimeEvent.Call(Animate)));
Clock.AddTimeEvent(
new TimeEvent(45, new TimeEvent.Call(Move)));
boundary = new PointF[4] {new PointF(+0.04f, -0.41f),
new PointF(+0.04f, -0.49f),
new PointF(+0.19f, -0.41f),
new PointF(+0.19f, -0.49f)};
idle = sprite.GetOffset(Idle);
}
This code will get the rock in:
map = new Map(8,8);
Player = new Actor(device, map, "player");
NPC = new Actor(device, map, "NPC");
//Actor rock = new Actor(device, map, "plains");
Actor rock = new Actor(device, map, "plains", "Rock");
//rock.vbOffset = rock.GetSprite().GetOffset("Rock");
//map.tiles[1].AddOccupant(rock);
Player.SetDXPosition(0.5f,0.5f);
rock.SetDXPosition(0f, 0.5f);
map.AddMapObject(Player);
map.AddMapObject(NPC);
map.AddMapObject(rock);
}
Okay more problems, the boundary is the Actor boundary for the stadard NPC style size! Maybe we need some kind of text file to define NPCs too - or maybe we're using a really stupid class to represent a rock :D So the Actor boundary is below the rock - where it's feet would be. We must bear in mind that this really does need changing but for now we need to do two things:
New Class and Cleaning up Maps rendering
Let's get maps crap out of PlayingGameState.
Below is all the crap.
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);
if(((i+1) % map.rowLength) == 0)
{
x = -1f;
y -= 0.25f;
}
else
{
x += 0.25f;
}
}
map.MapObjects.Sort();
foreach(MapObject m in map.MapObjects)
{
m.Render(device);
}
In map we will create a wonderful skeleton function called Render - not like GameObjects render it will differ slightly. Den-den-dur ...
//Somewhere ever far far away in the Map Class
//x,y are DX coords
public void Render(float x, float y, Device device)
{
}
Yes groovy indeed. Now we transfer crap for PlayingState to this function and the world becomes a happier place. (may need to add various references).
map.Render(-1f,1f);
... (crap we've yet to move)
First stage let's get the tile rendering out of there! So in PlayingState we have the following:
public void Process()
{
if (device == null)
return;
device.Clear(ClearFlags.Target, System.Drawing.Color.Black, 1.0f, 0);
device.BeginScene();
map.Render(-1f,1f, device);
map.MapObjects.Sort();
foreach(MapObject m in map.MapObjects)
{
m.Render(device);
}
device.EndScene();
device.Present();
UpdateInput();
}
Okie, let's give get our Map render function a gander:
//x,y are DX coords
public void Render(float x, float y, Device device)
{
device.SetStreamSource( 0, tiles[0].sprite.GetVB(), 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
Matrix QuadMatrix = new Matrix();
QuadMatrix = Matrix.Identity;
for(int i = 0; i < tileTotal; i++)
{
Tile t = (Tile) 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);
if(((i+1) % rowLength) == 0)
{
x = -1f;
y -= 0.25f;
}
else
{
x += 0.25f;
}
}
}
Okay now try moving the map. Like so:
public void Process()
{
if (device == null)
return;
device.Clear(ClearFlags.Target, System.Drawing.Color.Black, 1.0f, 0);
device.BeginScene();
map.Render(-0.9f,1f, device);
Notice the glaring error!
We're reset X to -1f, not the value we feed in, but as we're Captain Fixit this proves a minor problem by doing the following:
First a new variable.
public void Render(float x, float y, Device device)
{
float xLimit = x;
Then let us use this fine piece of variable power:
for(int i = 0; i < tileTotal; i++)
{
Tile t = (Tile) 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);
if(((i+1) % rowLength) == 0)
{
x = xLimit;
y -= 0.25f;
}
Now we're rollin' try the following in PlayingGameState
.
map.Render(-0.9f,0.9f, device);
Cool we get the following picture, as expected:
But why you may think - why why why why why - patience. Maybe we wish to stitch together several maps! Or do some thing groovy and special. Notice how we move the map yet most stuff doesn't crap up isn't it wonderful! Of course it would crap up if we moved if far enough.
- Problem1: GameObjects are not strongly linked to the map
They have DX coords - being map objects they should have something different, like a tile number and offset or similar!
Map should be feeding the render coordinates to it's childen
Parent Child Bonding
We must note if something leaves a map! (maybe don't give DXx,y at start but something that creates the X,y for you from map info)
So we may want to set a tile objects position in reference to a tile on the map. So we can do this in X,Y ints or maybe passing a tile. The way we should really do this is to have each tile store it's X,Y because it would make our life easier and the computer can take it (we're no longer programming for the C64). But obviously this time I decided easy wasn't good enough and went for moderately hard. Each tile should be able to tell us it's X,Y DX position in fact I even wrote the code for this.
Of course I never checked it and therefore the code is wrong. Only slightly wrong though. So go into the tile class and replace your function with the below:
public override System.Drawing.PointF GetDXPosition()
{
int index = map.GetTileIndex(this);
float tileLength = 2f/map.rowLength; // in directX co-ords
/*
* It's 2f because X goes from -1 to 1
*/
int ylevel = System.Math.Abs(index / map.rowLength);
int xlevel = index - (ylevel * map.rowLength);
PointF p = new PointF( -1 + (xlevel * tileLength) , //DXcoords
-ylevel * tileLength +1);
return p;
}
So each tile can now tell us where it is in DX co-ords. Or at least tell us where its top corner is, and we can infer the rest.
So in actor to complement SetDXPosition, we're going to have SetMapPosition. It's going to look a little like below:
public void SetMapPosition(int tileX, int tileY)
{
int tileIndex = tileX + (map.rowLength * tileY);
position = map.tiles[tileIndex].GetDXPosition();
}
To check this was working okay. I went into playing game state and used the OnPressingEnter code and played about with some values. There is no out of bounds checking or anything like that which would be nice to have!
private void UpdateInput()
{
DXInput.KeyboardState state = inputDevice.GetCurrentKeyboardState();
if (state[DXInput.Key.Return])
{
Player.SetMapPosition(7,7);
}
It currently takes the actors top left as the area to move but really we want the area just above the feet. So let's look at our new function in Actor again.
public void SetMapPosition(int tileX, int tileY)
{
try
{
int tileIndex = tileX + (map.rowLength * tileY);
position = map.tiles[tileIndex].GetDXPosition();
position.Y -= boundary[0].Y + 0.10f;
//should take in Actor height.
}
catch(Exception e)
{
Console.WriteLine("SetMapPosition produced out of bounds");
}
}
We use the boundary to get the top part of the feet but because the boundary is rather tight this doesn't place the feet into the center of the tile :( So I add a little to pad it out. This is rather hacky. (A better soultion would be to place from the middle of the tile.
Little bit of neating
In playing GameState let's use this new function to set the position of our actors - of course Rock will be incorrect because it's boundary is wrong.
Player.SetMapPosition(4,4);
NPC.SetMapPosition(4,6);
rock.SetMapPosition(5,5);
map.AddMapObject(Player);
map.AddMapObject(NPC);
map.AddMapObject(rock);
}
Okay to get these Actors to sit on tiles and move with the tiles when we move the map we need to do two things:
- Have an tile offset, distance from top corner
- New render method that uses X,Y in GameObject
I think it be time to stratify the class hierarchy a little more. First came GameObjects, which where a huge success. Then MapObjects, also highly useful and now we may have TileObjects! A skeleton may look like below:
namespace Animate
{
public abstract class TileObject : MapObject
{
PointF offset = new PointF(0,0);
public TileObject(Map mapIn) : base(mapIn)
{
}
}
}
And what a nice skeleton class it is too. We're going to do nothing with it for the moment though. First to GameObject and let's add another Render function.
public abstract void Render(Matrix QuadMatrix, Device device);
Then of course this needs to be implemented in both Tile and Actor. We'll make a skeleton function in both of them for now:
public override void Render(Matrix QuadMatrix, Device device)
{
}
Once these are in place our code will compile again.
Let's start tinkering with the tile one.
public override void Render(float X, float Y, Device device)
{
device.SetStreamSource( 0, sprite.GetVB(), 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
Matrix QuadMatrix = new Matrix();
QuadMatrix = Matrix.Identity;
QuadMatrix.Translate(X,Y,0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, sprite.GetTexture());
device.DrawPrimitives(PrimitiveType.TriangleFan,vbOffset, 2);
foreach(GameObject g in Occupants)
g.Render(X,Y, device);
}
Things are starting to look good! Of course we also need to tinker with Maps render loop and remove a few lines of code from there.
public void Render(float x, float y, Device device)
{
float xLimit = x;
for(int i = 0; i < tileTotal; i++)
{
Tile t = (Tile) tiles[i];
t.Render(x,y, device);
if(((i+1) % rowLength) == 0)
{
x = xLimit;
y -= 0.25f;
}
else
{
x += 0.25f;
}
}
}
We're calling SetStreamSource a lot more now that upsets me - but it's the simplest way to support a large number of texture sets. I'm sure there's a clever way where the tiles are rendered in "chunks" according to which texture set they use. If speed becomes a problem then this will be an area worth examining for bottle necks.
So now each tile render will also render anything that's occupying it. At the moment though nothing is occupying any of the tiles and Actors render function is only a skeleton. Let's just throw in some of the code we used for the tile rendering.
public override void Render(float X, float Y, Device device)
{
device.SetStreamSource( 0, sprite.GetVB(), 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
Matrix QuadMatrix = new Matrix();
QuadMatrix = Matrix.Identity;
QuadMatrix.Translate(X,Y,0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, sprite.GetTexture());
device.DrawPrimitives(PrimitiveType.TriangleFan,vbOffset, 2);
}
Now let's change rock so that's it's not a map object but it's an occupier of a tile.
map.AddMapObject(Player);
map.AddMapObject(NPC);
map.tiles[22].AddOccupant(rock);
Cool that works. The only problem is it won't work so well for our Actors, as the move pixel by pixel they change the tile that they're on! Should we even have them as tile occupiers? Maybe not for now. Of course Rock isn't perfect because it's a blocking Actor on the tile, the tile turns to blocking as but this gives a the rock at much larger blocking range. Maybe this is for the best maybe not. It's something we can leave for now and just have tile blocking. Let's change the generate plain function:
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;
}
So we have a nice rock that's on tile and it blocks our passage. Seems pretty good to me.
The actor class though is overkill for the rock, it's totally static. What we'd rather have is a scenery class. Something bare bones that can maybe have a script attached if necessary. I can see three classes currently for the map: Actors, Scenery, Items. Very simple.
Quick Aside
Currently we're tinkering about and seeing that perhaps the code would be better done in different ways. It works for now and I want to only change previous code gradually as more and more gets done. Anywho while experiment I noticed a bug in the Actor class, namely the intersect method the code should be as following:
public override bool Intersect(PointF p)
{
PointF pt0 = new PointF(position.X + boundary[0].X, position.Y + boundary[0].Y);
PointF pt1 = new PointF(position.X + boundary[1].X, position.Y + boundary[1].Y);
PointF pt2 = new PointF(position.X + boundary[2].X, position.Y + boundary[2].Y);
PointF pt3 = new PointF(position.X + boundary[3].X, position.Y + boundary[3].Y);
bool inX = (p.X >= pt0.X && p.X <= pt2.X);
bool inY = (p.Y <= pt0.Y && p.Y >= pt1.Y);
return inX && inY;
}
If you where on the exact same y you could walk one Actor through another - oops. Good job I caught it now!
Scenery Class
We want something pretty stripped down for this.
Yes animations would be nice but we're not putting any of that code in for now. All we want is a sprite and a render method. That will do.
The GameObject
We want the gameobject to be as general as possible. So it's worth reviewing at this time where we're thinking about extending its lineage.
public abstract class GameObject
{
public abstract ISprite GetSprite();
protected Matrix QuadMatrix = Matrix.Identity;
Two things here, first we should just assign this to the Identity rather than having it empty. Second in the tile class and possible Actor class we use a variable of the same name - we can get rid of that!
//Tile Class : Render(x,y,device) method : Removing unecessary code
public override void Render(float X, float Y, Device device)
{
device.SetStreamSource( 0, sprite.GetVB(), 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
//Matrix QuadMatrix = new Matrix(); /**POOF**/
//QuadMatrix = Matrix.Identity; /**POOF**/
QuadMatrix.Translate(X,Y,0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, sprite.GetTexture());
device.DrawPrimitives(PrimitiveType.TriangleFan,vbOffset, 2);
foreach(GameObject g in Occupants)
g.Render(X,Y, device);
}
and as Actor is basically a copy we can do the same removal.
public override void Render(float X, float Y, Device device)
{
device.SetStreamSource( 0, sprite.GetVB(), 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
//Matrix QuadMatrix = new Matrix(); /**POOF**/
//QuadMatrix = Matrix.Identity; /**POOF**/
QuadMatrix.Translate(X,Y,0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, sprite.GetTexture());
device.DrawPrimitives(PrimitiveType.TriangleFan,vbOffset, 2);
}
Okay let's continue looking through GameObject.
protected int vbOffset = 0;
Let's close down that access. Next ...
public abstract PointF GetDXPosition();
public abstract bool Intersect(PointF p);
public bool Intersect(float X, float Y)
{
return Intersect(new PointF(X,Y));
}
Okay every GameObject should have a DX position. And intersect is very useful, say to detect if the mouse is over any of our game objects! So we'll keep that in but let's get rid of the code and have the children implement both of these. That way its more consitent and less clutter.
public abstract bool Intersect(PointF p);
public abstract bool Intersect(float X, float Y);
and in Actor we have:
public override bool Intersect(PointF p)
{
return Intersect(p.X,p.Y);
}
public override bool Intersect(float X, float Y)
{
PointF pt0 = new PointF(position.X + boundary[0].X, position.Y + boundary[0].Y);
PointF pt1 = new PointF(position.X + boundary[1].X, position.Y + boundary[1].Y);
PointF pt2 = new PointF(position.X + boundary[2].X, position.Y + boundary[2].Y);
PointF pt3 = new PointF(position.X + boundary[3].X, position.Y + boundary[3].Y);
bool inX = (X >= pt0.X && X <= pt2.X);
bool inY = (Y <= pt0.Y && Y >= pt1.Y);
return inX && inY;
}
and in tile something similar:
public override bool Intersect(float X, float Y)
{
return (this.Equals(map.tiles[map.GetTileIndex(X,Y)]));
}
public override bool Intersect(System.Drawing.PointF p)
{
return Intersect(p.X,p.Y);
}
Let us continue to look at GameObject.
public GameObject()
{
}
Constructor that does nothing - don't really need that so remove it.
Then:
public abstract void Render(float X, float Y, Device device);
Our latest addition very good!
and finally:
public void Render(Device device)
{
ISprite s = GetSprite();
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);
}
This is the only function we have left, we may as well get rid of it and bump it down. Just to make GameObject lighter. MapObject can take care of it for now. So in goes:
public abstract void Render(Device device);
and into MapObject we put:
public override void Render(Device device)
{
ISprite s = GetSprite();
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);
}
This makes GameObject almost an interface, which is really what I wanted it to be in the first place. Something a bit lightweight that everything in the game will have.
The only thing stopping it being an interface are the two variables that are left:
vbOffset
QuadMatrix
vbOffset is going to be used by all GameObjects so I don't really want to get rid of it. It's used internally as well so I don't want to replace it the a get/setter. Maybe I could put it in MapClass. Really it should be bound up in Animation but while Animation is much better than it was the final system isn't finialized yet. For now it stays here!
QuadMatrix can go down to map object, especially as the render functions are now defferred.
//MapObject Class
protected Matrix QuadMatrix = Matrix.Identity;
So the only thing stopping GameObject becoming a funky interface is the development of the animation system. Well it will still be a while until we're prepared to tackle that one. But we can lay the ground work ... to lay the ground work requires a lot of good shuffling and writing without anything new happening :(
Let the crying and wailing begin ... also que poor thinking and bad programming!
Okay we have these Actors but they all have to conform to the same animations and rates of movement! Same speeds etc. We don't want this we want the animations and possible "states" to be all different. Somethings just want to flash. Some things want complicated movements. And that's cool we want that to be possible. So first we'll hard code different types of modules yet leave it open if in the future we wish to have dynamic info or loading from files.
We need to have a close look at Actor and see how we can rip a module out of there!
In Actor the module we're thinking about comprises of the following:
States are hard coded in a enumeration, they include Idle | Move_Left | Move_Up ...
. But imagine a machine that spins the only states it might have are Spin | Broken
. Actor isn't helpful here it's hard coded for something that's going to be walking about. We might want something that bobs up and down.
So we can't have a hard coded enumeration, we need something more flexible. In this case we're going to use strings. So we'll have string State
and we'll say if State = "Spin" then advance the spin animation
. If we want a bit of a bob we might say in the move function "if State == "Spin"
then move up a bit". We should be free to do what we like for each type of Actor we produce.
So we have movement, animation and states all in one place. This a good thing and we can call it the Motion module. So we'll do the clever trick we've done before we'll have IMotion
that exposes a few basic functions to the Actor. Then we'll have a few detailed implementations of IMotion and these will be fairly free in their implementation - they might load the data from a file for instance or be hard coded for simple human movement - it's pretty flexible and should be more than powerful enough for our needs.
Defining IMotion
using System;
using System.Drawing;
namespace Animate
{
public interface IMotion
{
void animateUpdate();
void positionUpdate();
void setState(string state);
float X
{
get;
set;
}
float Y
{
get;
set;
}
}
}
The way this works we'll have the Actor keep the blocking code. It will check the new position motion suggests and then see if it's blocked or not, if not it will accept it, otherwise it resets it. This is good because being blocked is not necessarily anything to do with basic motion. Also it limits the amount of stuff we need to pass into the particular motion class we intend to use.
In Actor we can now add:
public class Actor : MapObject
{
protected PointF position = new PointF(0,0);
protected PointF[] boundary;
protected ISprite sprite;
IMotion motion;
CanMove Function
Now we'll add a simple function, that is basically just copied from the end of the Move
function.
private bool CanMove(float newX, float newY)
{
if( map.CanMove(newX + boundary[0].X, newY + boundary[0].Y)
& map.CanMove(newX + boundary[1].X, newY + boundary[1].Y)
& map.CanMove(newX + boundary[2].X, newY + boundary[2].Y)
& map.CanMove(newX + boundary[3].X, newY + boundary[3].Y))
{
if(!map.movementObstructed(boundary,this, new PointF(newX,newY)))
{
return true;
}
}
return false;
}
With that in we can now simplify the Move procedure itself - even though we ultimately intend to delete it!
case States.standing:
{
return;
}
}
//Check the bounding box around the feet
if(CanMove(newX,newY))
{
position.Y = newY;
position.X = newX;
}
}
Implement an IMotion object
Okay the IMotion object we're going to create is not going to big or particularly clever. In fact it's mainly going to be ripped off from parts of Actor - but at this point it really doesn't matter because we're crossing the threshold to modularity.
Let's take a look at the entire beast!
using System;
using System.Drawing;
namespace Animate
{
public class PlayerMotion : IMotion
{
protected ISprite sprite;
protected string currentState = "idle";
protected PointF position = new PointF(0,0);
protected int idle = 0; //Offset for VB idle look
protected int vbOffset;
public PlayerMotion(ISprite s, int idleOffset)
{
sprite = s;
idle = idleOffset;
}
#region IMotion Members
public void positionUpdate()
{
switch(currentState)
{
case "down":
{
position.Y -= 0.045f;
}break;
case "right":
{
position.X += 0.04f;
}break;
case "left":
{
position.X -= 0.04f;
}break;
case "up":
{
position.Y += 0.045f;
}break;
case "standing":
{
return;
}
}
}
public void animateUpdate()
{
switch(currentState)
{
case "standing":
{
//Check previous state and get last facing direction
vbOffset = idle;
}break;
case "down":
{
vbOffset =
sprite.GetAnimation("walking down").Advance();
}break;
case "right":
{
vbOffset =
sprite.GetAnimation("walking right").Advance();
}break;
case "left":
{
vbOffset =
sprite.GetAnimation("walking left").Advance();
}break;
}
}
public void setState(string state)
{
currentState = state;
}
public float X
{
get
{
return position.X;
}
set
{
position.X = value;
}
}
public float Y
{
get
{
return position.Y;
}
set
{
position.Y = value;
}
}
#endregion
}
}
What's worth noting above is the change to strings. Also the lack of blocking code ... instead of blocking code we just update the Position variable. The position variable is accessed via the IMotion interface.
Notes
Okay so we're calling these things all the time and we're comparing all these strings, it maybe better to make things little more efficent but only if it becomes an issue! first make it work (and I'm talking 'bout the entire game) then make it fast!
So now we have a funky ass motion module all ready to be slotted in! We'll do the slotting in slowly, slowly is always best.
Slowly slotting it in ;)
First we need to pull Render(Device device) down from Map. This is for basic consitency but this is also where we're going to be putting out blocking code. It's best to get it out the way first.
Pulling down Render
Put this function into both Tile and Actor and remove it from MapObject!
public override void Render(Device device)
{
ISprite s = GetSprite();
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);
}
Adding to Actor's Constructor
For now we're putting Motion inside the constructor, of course in the future there should be some choice in which motion object you're actually plugging in.
public Actor(Device device, Map mapIn, string TexSet)
: base(mapIn)
{
motion = new PlayerMotion(
TextureManager.BufferTextureSet(TexSet),
0);
There are two constructors. We can cut some code out of the second constructor by doing the following:
public Actor(Device device, Map mapIn, string TexSet, string Idle)
: this(device, mapIn, TexSet)
{
idle = sprite.GetOffset(Idle);
}
So it calls the other constructor and then does it's jazz. Nice simple and tight. So we're creating a nice new Motion
object.
Throwaway function
The next thing we need to do is utilize it and for this we're first going to overload another function in Actor:
public void SetMovementType(string move)
{
motion.setState(move);
}
So this is a total throwaway function that's not really doing anything but for now it's fine.
PlayingGameState Update!
We need to have the key's being pressed send the correct signals to our Actor. Signals in the form of strings!
Slowly slowly at first so the old state update remains as well!
private void UpdateInput()
{
DXInput.KeyboardState state = inputDevice.GetCurrentKeyboardState();
if (state[DXInput.Key.Return])
{
Player.SetMapPosition(0,0);
}
if(state[DXInput.Key.LeftAlt] && state[DXInput.Key.S])
{
map.Write(@"c:\MapFile.txt");
}
if(state[DXInput.Key.LeftAlt] && state[DXInput.Key.L])
{
map.Read(@"c:\MapFile.txt");
}
if(state[DXInput.Key.RightArrow])
{
Player.SetMovementType(Actor.States.right);
Player.SetMovementType("right");
}
else if(state[DXInput.Key.LeftArrow])
{
Player.SetMovementType(Actor.States.left);
Player.SetMovementType("left");
}
else if(state[DXInput.Key.UpArrow])
{
Player.SetMovementType(Actor.States.up);
Player.SetMovementType("up");
}
else if(state[DXInput.Key.DownArrow])
{
Player.SetMovementType(Actor.States.down);
Player.SetMovementType("down");
}
else
{
Player.SetMovementType(Actor.States.standing);
Player.SetMovementType("standing");
}
Now of course we need to make sure that we take the new position given to us by motion. We check we can move there. If we can the update the Actors position to that one.
Adding the Timing Hook
So back to Actor's Constructor and let's do a little fiddling.
We're going to remove Actors local Animate
function as a time event and in it's sted we'll place our motion one.
//Clock.AddTimeEvent(
// new TimeEvent(45, new TimeEvent.Call(Move)));
Clock.AddTimeEvent(
new TimeEvent(45, new TimeEvent.Call(motion.positionUpdate)));
Now motion is being periodically called to produce a new position. Of course we need to act on this information, so to the Render functions - which we'll be able to shorten in the same way we managed with the constructors.
GameObject from Render to Process
public override void Render(float X, float Y, Device device)
{
device.SetStreamSource( 0, sprite.GetVB(), 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
device.RenderState.AlphaTestEnable = true;
device.RenderState.AlphaFunction = Compare.NotEqual;
QuadMatrix.Translate(X,Y,0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, sprite.GetTexture());
device.DrawPrimitives(PrimitiveType.TriangleFan,vbOffset, 2);
}
public override void Render(Device device)
{
if(CanMove(motion.X,motion.Y))
{
position.X = motion.X;
position.Y = motion.Y;
}
else
{
motion.X = position.X;
motion.Y = position.Y;
}
Render(device, GetDXPosition().X, GetDXPosition().Y);
}
This seems a bit odd no? The first function never checks what motions position is! This means motion will build an increasingly inaccurate position. This is okay at the moment because we're using the simpler render device so we always get the up to date motion info - or reset it if it's going to an unmovable area. Well if we move the Actor so he's a tile object we would use the first Render function probably with an offset value - so not to worry for now, a potential piece of code has yet to appear so it looks like an area for possible bugs. Luckily at note like this will remind us what was going on.
Now upon running you'll be in control! Using motions abilities.
Cleaning out the Movement stuff. So we kill actors Move procedure and give ourselves a bit more space. We can also remove the associated TimerEvent in the constructor if we have not already done so.
Honing the Animation
IMotion must communicate Animation changes to Actor. This information will result in index to an offset buffer and texture.
Sprite generally encapsulates information about the current animation of the GameObject. We currently have two sprite references! One is in Actor and one is Motion.
First!
public interface IMotion
{
void animateUpdate();
void positionUpdate();
void setState(string state);
float X
{
get;
set;
}
float Y
{
get;
set;
}
int offset
{
get;
set;
}
}
That's right we're moving the offset into IMotion and away from animation.
So we need to update player motion let's go:
public class PlayerMotion : IMotion
{
protected ISprite sprite;
protected string currentState = "idle";
protected PointF position = new PointF(0,0);
protected int idle = 0; //Offset for VB idle look
protected int vbOffset;
public PlayerMotion(ISprite s, int idleOffset)
{
sprite = s;
idle = idleOffset;
vbOffset = idle;
}
and the all important accessor functions!
public int offset
{
get
{
return this.vbOffset;
}
set
{
vbOffset = value;
}
}
Now let's start storing stuff in that varaible!
public void animateUpdate()
{
switch(currentState)
{
case "standing":
{
vbOffset =
idle;
}break;
case "down":
{
vbOffset =
sprite.GetAnimation("walking down").Advance();
}break;
case "right":
{
vbOffset =
sprite.GetAnimation("walking right").Advance();
}break;
case "left":
{
vbOffset =
sprite.GetAnimation("walking left").Advance();
}break;
}
}
Okay looks good to me, lets hook it into Actor.
First Actor's render function:
public override void Render(float X, float Y, Device device)
{
device.SetStreamSource( 0, sprite.GetVB(), 0);
device.VertexFormat = CustomVertex.PositionTextured.Format;
device.RenderState.AlphaTestEnable = true;
device.RenderState.AlphaFunction = Compare.NotEqual;
QuadMatrix.Translate(X,Y,0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, sprite.GetTexture());
device.DrawPrimitives(PrimitiveType.TriangleFan,motion.offset, 2);
}
Then if you haven't already done so (I can't remember cos I've made so many frickin changes to get something that works and isn't butt ugly) edit Actors constructor like so:
public Actor(Device device, Map mapIn, string TexSet)
: base(mapIn)
{
motion = new PlayerMotion(
TextureManager.BufferTextureSet(TexSet),
0);
blocking = true;
sprite = TextureManager.BufferTextureSet(TexSet);
//Clock.AddTimeEvent(
// new TimeEvent(95, new TimeEvent.Call(Animate)));
Clock.AddTimeEvent(
new TimeEvent(95, new TimeEvent.Call(motion.animateUpdate)));
Run this it should work, if it doesn't it's because I've missed out a step, please treat this as a quiz and see if you can fix it for yourself :D
Cleaning Up Actor
Now actor has junk functions that IMotion objects take care of.
So kill off the commented out TimerEvents in the constructor.
And while we're killing things let's move on to a few function I don't like the look of.
private void Animate()
Goodbye!
Let's go to PlayingGameState and remove the input duality we've been suffering from!
That leaves us with:
if(state[DXInput.Key.RightArrow])
{
Player.SetMovementType("right");
}
else if(state[DXInput.Key.LeftArrow])
{
Player.SetMovementType("left");
}
else if(state[DXInput.Key.UpArrow])
{
Player.SetMovementType("up");
}
else if(state[DXInput.Key.DownArrow])
{
Player.SetMovementType("down");
}
else
{
Player.SetMovementType("standing");
}
So let's get rid of the old SetMovementType
public void SetMovementType(States movementInfo)
We can get rid of all those -oh so- inflexible states too. Bye bye state enumeration.
public enum States
Once they've bitten the dust may as well throw out currentState as well. And that's it for our tidying. Of course there are a few lurking problems (where has our beautiful rock gone?!)
A few notes on IMotion
It needs a little improving. We shouldn't have two references to Sprite in Actor and IMotion. The only way I can think of resolving this is have a get texture function, which I'd rather not do for now. Also IMotion should maybe take care of setting up the Timers.
Actors should load in IMotions.
Where is the rock?
It's gone because in motion we default idle to zero and rock is a 8 or 12 or something. This is a problem due to class intialization.
First things first let's actually have the IMotion implementation take care of the timers!
public PlayerMotion(ISprite s, int idleOffset)
{
sprite = s;
idle = idleOffset;
vbOffset = idle;
Clock.AddTimeEvent(
new TimeEvent(95, new TimeEvent.Call(animateUpdate)));
Clock.AddTimeEvent(
new TimeEvent(45, new TimeEvent.Call(positionUpdate)));
}
Of course remove these from Actor!
Rearrange code so that the sprite is created first.
public Actor(Device device, Map mapIn, string TexSet)
: base(mapIn)
{
sprite = TextureManager.BufferTextureSet(TexSet);
motion = new PlayerMotion(
sprite,
0);
In the second constructor we need to add another motion constructor - yes I know we are creating the object twice - but it's something we should be passing in ready made anyway ... I think.
public Actor(Device device, Map mapIn, string TexSet, string Idle)
: this(device, mapIn, TexSet)
{
idle = sprite.GetOffset(Idle);
motion = new PlayerMotion(
sprite,
idle);
}
And wonderfully the rock returns! Glory Glorly Glorly.
Final Tidying Up
Yes I lied about the bush, we're sticking with the rocks - it's been hard enough as it is :D
So the rock being asked to Update and it has all these animation timers - but it's just freaking rock! It's not going to do crap all ever! (maybe someday we could hit it with a hammer or it could turn into a rock creature, put this is something that can be done without checking if each rock needs to do something each turn!)
New Class NoMotion
We've had player motion - now comes no motion. Yes it is that amazing. You'll also see the awesome power of the IMotion interface and maybe be slightly less inclined to think that I'm batshit-crazy. So hold on to your keyboard tightly here we go.
New class called "NoMotion" jump to to it, Suzi.
(Just like no-e-motion, like robots, like our rock . . . like the future!)
This is the whole class:
public class NoMotion : IMotion
{
int idle;
public NoMotion(int idleOffset)
{
idle = idleOffset;
}
#region IMotion Members
public void animateUpdate()
{
// Nothing as there is "NO MOTION"
}
public void positionUpdate()
{
// Nothing as there is "NO MOTION"
}
public void setState(string state)
{
// NOMOTION NO STATES!
}
public float X
{
get
{
return 0;
}
set
{
// NOMOTION nothing to update
}
}
public float Y
{
get
{
return 0;
}
set
{
// NOMOTION nothing to update
}
}
public int offset
{
get
{
return idle;
}
set
{
idle = value;
}
}
#endregion
}
}
This seems a base class that may be useful especially for testing and building phases such as we're in. In the game most "Actors" will be motive to some degree and so I don't know how often this bare bones class will be used - notice too how our project sprawls we need to get in there with some namespace organization (next chapter!)
To get this bad boy chugging along we need the Actor constructor to take in IMotion objects, rather that create them - so let's do that now!
If we do this it will no longer need the "idle" input so we can replace that! Also a quick look at the Actor constructors and it's apparent that we're passing in Device for no good reason at all so before anything else let's cut that out:
public Actor(Map mapIn, string TexSet, string Idle)
: this(mapIn, TexSet)
{
idle = sprite.GetOffset(Idle);
motion = new PlayerMotion(
sprite,
idle);
}
public Actor(Map mapIn, string TexSet)
: base(mapIn)
{
sprite = TextureManager.BufferTextureSet(TexSet);
motion = new PlayerMotion(
sprite,
0);
blocking = true;
boundary = new PointF[4] {new PointF(+0.04f, -0.41f),
new PointF(+0.04f, -0.49f),
new PointF(+0.19f, -0.41f),
new PointF(+0.19f, -0.49f)};
}
We must also update these changes in PlayingGameState:
{
device = d;
gameStateManager = g;
inputDevice = dInput;
map = new Map(8,8);
Player = new Actor(map, "player");
NPC = new Actor(map, "NPC");
Actor rock = new Actor(map, "plains", "Rock");
Player.SetMapPosition(4,4);
NPC.SetMapPosition(4,6);
rock.SetMapPosition(5,5);
map.AddMapObject(Player);
map.AddMapObject(NPC);
map.tiles[22].AddOccupant(rock);
}
So that's a little neater - okay next we comment out the first smaller constructor and add Imotion m as an argument to the second! You should have something like below:
/**
public Actor(Map mapIn, string TexSet, string Idle)
: this(mapIn, TexSet)
{
idle = sprite.GetOffset(Idle);
motion = new PlayerMotion(
sprite,
idle);
}
**/
public Actor(Map mapIn, string TexSet, IMotion m)
: base(mapIn)
{
sprite = TextureManager.BufferTextureSet(TexSet);
//motion = new PlayerMotion(
// sprite,
// 0);
motion = m;
So now we go to PlayingGameState, and here things get a bit messy. Each Actor needs (generally) and unique IMotion implementation as well as the TexSet name! We could include IMotion all in TexSet. For instance have a different TeXSet address each Actor but for now we're not going! All we do is create the correct sprite objects and pass in the correct stuff.
public PlayingGameState(Device d, GameStateManager g, DXInput.Device dInput)
{
device = d;
gameStateManager = g;
inputDevice = dInput;
map = new Map(8,8);
ISprite s = TextureManager.BufferTextureSet("player");
Player = new Actor(map, "player", new PlayerMotion(s,0));
s = TextureManager.BufferTextureSet("NPC");
NPC = new Actor(map, "NPC", new PlayerMotion(s,0));
s = TextureManager.BufferTextureSet("plains");
Actor rock = new Actor(map, "plains",
new NoMotion(s.GetOffset("Rock")));
Player.SetMapPosition(4,4);
NPC.SetMapPosition(4,6);
rock.SetMapPosition(5,5);
map.AddMapObject(Player);
map.AddMapObject(NPC);
map.tiles[22].AddOccupant(rock);
}
Now everything should compile and rock is just a simple plain ol' rock. It works just as it should but ... but but but but it still has a secret lurking away. It has a "boundary" and this boundary is just as big as would be expected by a player character! That should not be the case! But for now we can deal everything is happy and shiny and while this lesson works let's create a new project and scrape over our code landscape again.
Review
It's nice to see how light the classes such as PlayingGameState are, it makes them pretty easy to follow.
A lot of code introduced this time needs a bit more care and bit more molding. But it works and its what we want. The problems where generally that this isn't something I've tackled before and I definetly blundered into it. It came out okay. The codes feeling more flexible, something that will, once neaten, be able to support a game.
We're getting pretty near one of the major way points. In fact if we reach one I may end these tutorials for the public there - as I don't want my revolutionary game becoming immediately mimiced around the interweb!
Next time we'll think about that scenery class once more. Try to remove anything redundant, consolidate some code that needs consolidating (I'm taking about Slices and TextureSet here). Add some NameSpaces and try to be as cool and groovy as possible.
Then making sure that map objects more relative to the map! I'm looking at you "Player" and NPC". After this there are many ways to go and I might develop both in parrallel because I'm mad.
1 comment:
Again, excellent tuto, but this time lesser bog, that's fine.
Do you intend to write another tutorial? It would be nice something about the "dialog message" like the ones in lots of RPG (with the stats at the bottom of the screen). Maybe LUA can help me about that.
Thanks,
++
Post a Comment