Stage 5 - 1 : NPC AHOY!
So the next thing we're going to do is create a nice NPC for the user to interact with - once again it is time to call on my art skills (notably our main character still does not have a animated walking backward series of frames). So I don't know about you guys but my RPG I think is going down the old fantasy lines. This makes my NPC a bit random, but I drew him how I drew him and here is, odd looking world war one-ish guy.
One again I can't host .tga's anywhere so please convert the .png. Cheers
Only need one frame because I'm not going to have him move, well not presently.
My undeniably wonderful art skills aside for one moment, let us start coding - I think we'll begin by making Tile be derived from GameObject. This is a somewhat painful but if everything is derived from GameObject, there will be more happiness in the world.
Tidying Up
Let's make Tile a GameObject too
First let's look at GameObject and see what changes may need to be implemented:
using System;
using System.Windows.Forms;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace GraphicsAndTime
{
public abstract class GameObject
{
protected Texture texture;
protected VertexBuffer vertexBuffer;
protected Device device;
protected Matrix QuadMatrix = new Matrix();
protected int vbOffset = 0; //VertexBuffer offset to render from
public float posX, posY = 0; //DirectX Position i.e. middle of the screen
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 abstract void IntializeVertexBuffer();
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, vbOffset, 2);
}
}
}
Okay the X and Y position of a tile, we can store that using DX co-ordinates. Though in the future we may have maps bigger than the screen and it may not be a constant value. Therefore we should change this to a function that says getDXPosition. See I told you this would be painful :D
Okay so let's make this change - compile - and see what goes wrong!
public abstract class GameObject
{
protected Texture texture;
protected VertexBuffer vertexBuffer;
protected Device device;
protected Matrix QuadMatrix = new Matrix();
protected int vbOffset = 0; //VertexBuffer offset to render from
//public float posX, posY = 0; //DirectX Position i.e. middle of the screen
public abstract PointF GetDXPosition();
public abstract void SetDXPosition(float X, float Y);
We get the following errors - as expected really.
C:\ ... \SimpleMap\Actor.cs(8): 'GraphicsAndTime.Actor' does not implement inherited
abstract member 'GraphicsAndTime.GameObject.GetDXPosition()'
C:\ ... \SimpleMap\Actor.cs(8): 'GraphicsAndTime.Actor' does not implement inherited
abstract member 'GraphicsAndTime.GameObject.SetDXPosition()'
Okay let's pop on over to actor and implement these accessors!
First we need to add the posX and posY floats. As Actor makes use of such things quite a lot.
public class Actor : GameObject
{
Map map;
protected PointF position = new PointF(0,0);//X,Y position in DirectX co-ordinates
and the functions to get this:
public override PointF GetDXPosition()
{
return position;
}
public override void SetDXPosition(float X, float Y)
{
position.X = X;
position.Y = Y;
}
If you've been following this carefully you may note a problem. We have this SetDXPosition function but we can't set the DX position of tiles because it's all controlled by the map. This leaves us with two options reduce the abstract functions to just the get. Or change the way map is written. For now we'll leave it hanging - until we clean up again or the situation becomes clearer (I can imagine us wanting a tile slowly sliding across the screen for instance, but for this we could use some other game object. For now we leave things hanging to be dealt with when the situation becomes clearer.
Okay with that done compliling brings a whole host of new errors - mainly revolving around the fact that we must write position.X rather than posX and position.Y rather than posY, so let's make life easy on ourselves and do a mass replace (only in the current document mind you!)
Perform those replaces and two each are done. Then the complier alerts us to a problem with GameObject! In the render statement it can no longer find posX or posY, we need to subsitute in the abstract functions we created so it looks like this:
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(GetDXPosition().X,GetDXPosition().Y, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.DrawPrimitives(PrimitiveType.TriangleFan, vbOffset, 2);
}
Compile and behold - everything works again!
Let's continue to browse through GameObject, next up is the constructor. This code is a little old in light of our Texture manager so let's alter it a bit. Currently we have:
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");
}
}
Let's change this too:
public GameObject(Device d, Texture gameObjectTexture)
{
device = d;
texture = gameObjectTexture;
}
This simplification like the previous is going to cause errors, but before we start to muddy ourselves with them - let's go to the texture manager!
public class TextureManager
{
static private ArrayList textureList = new ArrayList();
static public Texture player;
static private Device device;
static private bool Plain = false;
Our player texture is going to be called so often - 99% of the game or more no doubt that we can give him his own special variable in the TextureManager. We'll load the texture in, in the SetDevice
procedure.
static public void SetDevice(Device d)
{
device = d;
player = TextureLoader.FromFile(device, @"C:\playerrun2.tga");
}
Notice how messy it is at the moment, we could do with a specific function that handles textures being loaded and generating useful errors if this doesn't happen.
If you try and compile now, there will be an error about GameObjects constructor being called by Actor. Lets rectify this.
public Actor(Device device,
Map mapIn,
string texturePath) : base(device, texturePath)
Needs altering so it goes:
public Actor(Device device, Map mapIn, Texture actorTexture)
: base(device, actorTexture)
Then one last change and everything will hopefully be right again!
We create an Actor object - for the player, in the constructor in the PlayingGameStateClass
. This obviously has the wrong arguments, we'll use our wonderful (if somewhat hackish) TextureManager singleton to solve this problem:
Before:
Player = new Actor(device, map, @"C:\playerrun2.tga");
After:
Player = new Actor(device, map, TextureManager.player);
There we go wonderful.
A Thought
Let's get rid of public abstract void SetDXPosition(float X, float Y);
, it can still exist in Actor, but for tile it's always going to incorrectly implemented and that smacks or poorly thought out coding :D
Simple snip public abstract void SetDXPosition(float X, float Y);
from GameObject
and in Actor change the code from public override void SetDXPosition(float X, float Y)
to public void SetDXPosition(float X, float Y)
Let's make Tile a GameObject!
public class Tile : GameObject, IStorable
{
static public VertexBuffer vertexBuffer;
We have a static VertexBuffer here because we have only tile shape that we use for all tiles - this is good we don't want a crap load of vertex buffer pointers in each tile, it's just taking up needless space. But GameObject has a VertexBuffer! Well not for long, let's go back to GameObject we're going to get rid of it's VertexBuffer!
public abstract class GameObject
{
protected Texture texture;
//protected VertexBuffer vertexBuffer;
...
public abstract VertexBuffer GetVertexBuffer();
GameObject makes use of this in the render function so we need to make this change:
public void Render(Device device)
{
QuadMatrix = Matrix.Identity;
device.SetStreamSource(0, GetVertexBuffer(),0);
This of course is going to upset various other parts of code, we need to deal with those now! First the actor class:
public class Actor : GameObject
{
Map map;
protected PointF position = new PointF(0,0);
protected VertexBuffer vertexBuffer;
...
public override VertexBuffer GetVertexBuffer()
{
return vertexBuffer;
}
Looking to Tile again we see that it's IntializeVertexBuffer(); function requires a device being passed to it. While game object store's a device reference. Which is best? Well our game isn't going to be cutting edge, we could easily accomodate all GameObjects having such a reference. Looking at it though it seems we only require access to it for Intialization of the Vertex Buffer and for a render function. The render function may not even stay! So we don't need it that much let's get rid of the reference and parameters like so
public abstract class GameObject
{
//Cheeky little change here too
public Texture texture;
//protected Device device; *poof*
protected Matrix QuadMatrix = new Matrix();
...
public GameObject(Texture gameObjectTexture)
{
//device = d; *poof*
texture = gameObjectTexture;
}
...
public void Render(Device device)
{
...
public abstract void IntializeVertexBuffer(Device device);
...
Once again this causes a few waves of disturbance through our code, that needs correction. Actor as usual will be the first casulity let's go fix it up:
public Actor(Device device, Map mapIn, Texture actorTexture)
: base(device, actorTexture)
becomes ...
public Actor(Device device Map mapIn, Texture actorTexture)
: base(actorTexture)
and for the body of the constructor:
public Actor(Map mapIn, Texture actorTexture)
: base(actorTexture)
{
IntializeVertexBuffer(device);
Now to right the actual function:
public override void IntializeVertexBuffer(Device device)
{
And that sorts out actor - nice. Apart from where it's called to be rendered in PlayingGameState
. Let's make that quick fix now:
This:
Player.Render();
to this:
Player.Render(device);
Back to tile, maybe this go we'll finally convert it into a game object.
public class Tile : GameObject, IStorable
{
static public VertexBuffer vertexBuffer;
public Texture tileTexture;
private bool Blocking = false;
The above is what we're dealing with we need to add three functions: GetDXPosition
, GetVertexBuffer
, IntializeVertexBuffer(Device device
and edit the constructors.
First up IntializVertexBuffer, simple to do
public override 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);
}
We call this very near start up, only once because it makes the VertexBuffer for all tiles. Notice now though that it can be no longer static, this makes calling it from Tile hard. So we need to alter this intialization code so it can still go ahead, let's get this out the way now!
public PlayingGameState(Device d, GameStateManager g, DXInput.Device dInput)
{
device = d;
//Tile.IntializeVertexBuffer(device); *poof*
Tile t = new Tile();
t.IntializeVertexBuffer(device);
We create a new Tile object and have it intialize the vertex buffer for all tiles. This is maybe a little unlcean but the VertexBuffer code needs to be altered and moved closer to the texture code anyway! So that is something we will come to as we inch further to robustness
Okay the rest of the override functions:
public override System.Drawing.PointF GetDXPosition()
{
return new System.Drawing.PointF (0,0);
}
public override VertexBuffer GetVertexBuffer()
{
return Tile.vertexBuffer;
}
Notice that the GetDXPosition is a place holder, it's currently beyond our means to say where a tile is, we need access to the map, or the map information to give that out, we could maybe have an object that has to be passed in and make sure that's the map for the tile, or possibly the map for all objects, as it's quite possible a lot of gameobjects will work their position out from map. For now though it is something we once again leave hanging.
VertexBuffer is pretty simple, the only remaining thing now is to sort out the constructor.
public Tile() : base(null)
{
}
public Tile(Texture texture) : base(texture)
{
//tileTexture = texture; *poof*
}
Let's get rid of the tileTexture variable too:
public class Tile : GameObject, IStorable
{
static public VertexBuffer vertexBuffer;
//public Texture tileTexture; *poof*
private bool Blocking = false;
Then we need to change all those tileTextures to texture (which is in GameObject)
public void GeneratePlain()
{
TextureManager.BufferTextures("Plain");
for(int i = 0; i < area; i++)
{
tiles[i] = new Tile(TextureManager.RequestTexture("Grass"));
}
((Tile)tiles[22]).texture = TextureManager.RequestTexture("Stone");
((Tile)tiles[22]).block = true;
}
Also in the Read function we need to make the swap:
public void Read(object o)
{
...
texture = TextureManager.RequestTexture("Grass");
...
texture = TextureManager.RequestTexture("Stone");
There's one in write too!
public void Write(object o)
{
StreamWriter writer = (StreamWriter) o;
writer.WriteLine(TextureManager.LookUpTexture(texture));
Okay now some places that use Tile texture information outside tile. Such as Maps generate plain procedure:
public void GeneratePlain()
{
TextureManager.BufferTextures("Plain");
for(int i = 0; i < area; i++)
{
tiles[i] = new Tile(TextureManager.RequestTexture("Grass"));
}
((Tile)tiles[22]).texture = TextureManager.RequestTexture("Stone");
((Tile)tiles[22]).block = true;
Then in PlayingGameState we need to do one last change:
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.texture);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
We could cut this down and put in the t.Render(); but I have enough of messy around for now, it compiles we should rejoyce.
Groovy it works and Tile is now a GameObject, GameObject is now more generalized but still not quite set in stone. We may no longer actually need to store texture information in our game objects we could just have a texture label or similar, for now though we'll let the changes we've made settle.
Tidying up is always tricky, we have to keep moving to a framework that will easily accomodate the more advanced things that we wish to try. Imagine we want to shrink everything, we could get all the gameobjects and now alter their vertexbuffers without specialized code to handle tiles. That's the kind of power we're after. There's still plenty to be done in this direction but weare going to move there in slow steps. As it can be rather complicated, as the above jumping around making minor changes shows - look how GameObject is become more and more like an inteface!
Now back to the fun stuff getting that NPC in there ... of course we may have to rejig actor a little :D
NPC Arrives!
Okay let's hope into PlayingGameStates constructor:
public PlayingGameState(Device d, GameStateManager g, DXInput.Device dInput)
{
device = d;
Tile t = new Tile();
t.IntializeVertexBuffer(device);
gameStateManager = g;
inputDevice = dInput;
map = new Map(8,8);
Player = new Actor(device, map, TextureManager.player);
}
After player we're going to add a new actor ... of course we need a new Texture, for now we'll just put in the player texture as a place holder.
public class PlayingGameState : GameState
{
private Map map;
private Actor Player;
private Actor NPC;
Then let's intialize him:
Player = new Actor(device, map, TextureManager.player);
NPC = new Actor(device, map, TextureManager.player);
Now we need to make it Render, so let's take a leisurely scroll down to the process procedure:
...
Player.Render(device);
NPC.Render(device);
...
Load it up, move around and see that there are two players. Currently one does nothing and your player seems to be permentantly behind him. We need to fix all this and we'll do it by associating it with the map, the map needs to keep a list of it's gameobjects.
Creating a list of map objects
At some point we may wish for specific map objects, that is objects associated directly with the current map (i.e. not clouds or weather conditions for instance, or flashign GUIs). For now we'll just use our bog standard GameObject type. We'll be using an arraylist - we can see our entire map, so we'll be loading everything on this array list. The player if present in the map should also be on the list! We want to draw things, that are furthest in the baskground first. If you know anything about depth cues you'll know that overlapping objects give an illusion of depth.
As shown above, things further to the top of the screen are seen to be further away. Things nearer the eye come before those further from the eyes - occulsion. We do not have this currently in our game, our NPC hangs there, while our player can walk through him in all sorts of unatural ways - we need to order the map's GameObjects by they're Y position. Using DirectX co-ordinates the lower the Y, the later we show render it. So we order the array by greatest value of Y and then render the list. And bingo - so let's get round to constructing that - to the map class!
using System.Collections;
We'll use an array list to store our game objects. Because objects will be added and removed to the map and a map will not have a constant or limited numberv of objects. So for now we'll make it private too just to make things neat:
public class Map : IStorable
{
public Tile[] tiles;
private ArrayList GameObjects = new ArrayList();
private int area;
Okay we're going to want to do a few things with this:
- Add Objects (ordered by Y)
- Remove Objects
- Render all objects in order
First things first lets make an add an object function. So we want to add a GameObject then we want to sort the list. If you remember the Clock we were doing something similar with TimeEvents, we overloaded the CompareTo function. So we'll make a similar change to TimeObject. So let's open our GameObject function and slot in this code:
public abstract class GameObject : IComparable
public int CompareTo(Object rhs)
{
GameObject g = (GameObject)rhs;
return g.GetDXPosition().Y.CompareTo(this.GetDXPosition().Y);
}
So we sort by Y position when we compare GameObjects, doesn't sound quite sensical but for now this is the only sorting of GameObjects we're going to be doing and we can let it go.
Let's make use of this sort by making our Add function in map.
public void AddGameObject(GameObject g)
{
GameObjects.Add(g);
GameObjects.Sort();
}
Okay, pretty simple now we need a remove function.
public void RemoveGameObject(GameObject g)
{
GameObjects.Remove(g);
}
Also simple, we may need more powerful remove functions but if we do it's something we'll find out about later. Okay so we do the rendering of the map outside of the map class. We don't want to upset the boat at this point, so let's do the same for the GameObjects - this means making the array list public:
public Tile[] tiles;
public ArrayList GameObjects = new ArrayList();
private int area;
So in the process procedure of PlayingGameState we throw in some GameObject rendering code underneath the tile code.
x += 0.25f;
}
}
map.GameObjects.Sort();
foreach(GameObject g in map.GameObjects)
{
g.Render();
}
Player.Render(device);
NPC.Render(device);
Okay we've written some code, but it doesn't do anything! We need to actually use it so that we may test the results, so let's throw in. We sort every loop because the players Y is going to be changing as is any moving object.
So let's test this baby out! In our PlayingGameState Process let's cut:
Player.Render(device); /*poof*/
NPC.Render(device); /*poof*/
...and in the constructor we'll add these bad boys into the GameObjects
list.
Player = new Actor(device, map, TextureManager.player);
NPC = new Actor(device, map, TextureManager.player);
map.AddGameObject(Player);
map.AddGameObject(NPC);
}
Yeah we're leaving in code here that we could now kind of remvove. Those big variables for the NPC and player - but you never know when we might need access to them. We'll probably have seperate NPC lists and the like later - for now let's keep access to our Test Npc nice and open.
Okay I think you'll agree it looks pretty damn groovy right now. Walk that character through his unmoving alter ego.
So all very cool, it may be worth noting here, that we are comparing from the top left hand corner of the sprite, not his feet or the middle so it's not the same as the boundary hecking stuff but it should suffice. Talking about boundary checking it may be worth having a look into that, so our NPCs aren't walking all over each other.
More Bounds Checking - MORE!
Okay we have some basic boundary checking in already, all we need do is extend that. At the bottom of the move function in Actor we have:
//Check the bounding box around the feet
if( map.CanMove(newX +0.04f, newY -0.30f)
& map.CanMove(newX +0.04f, newY -0.49f)
& map.CanMove(newX +0.19f, newY -0.30f)
& map.CanMove(newX +0.19f, newY -0.49f))
{
position.X = newX;
position.Y = newY;
}
}
The boundaries are hard coded, we don't want this type of hard coding anymore, as we may be dealing with a number of NPCS, and oddly shaped objects. So we want to put a boundaries data type into the Actor class. So we'll but in a list of points, this way we're not restricted by how detailed our bounds checking might wish to be.
public class Actor : GameObject
{
Map map;
protected PointF position = new PointF(0,0);
protected VertexBuffer vertexBuffer;
protected Pointf[] boundary;
To the constructor and lets set up our boundary.
public Actor(Device device, Map mapIn, Texture actorTexture)
: base(actorTexture)
{
IntializeVertexBuffer(device);
map = mapIn;
Clock.AddTimeEvent(
new TimeEvent(95, new TimeEvent.Call(Animate)));
Clock.AddTimeEvent(
new TimeEvent(45, new TimeEvent.Call(Move)));
boundary = new PointF[] {new PointF(+0.04f, -0.30f),
new PointF(+0.04f, -0.30f),
new PointF(+0.19f, -0.30f),
new PointF(+0.19f, -0.49f)};
}
Yes all very hard coded at the moment, in the future that will change.
So now we have out array of points that express our player boundary offset from the current player position. Let's go look at the Move
function.
//Check the bounding box around the feet
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))
{
position.X = newX;
position.Y = newY;
}
On compiling everything works well but we've reduced the amount of hard coding yay! Though we're still hard coding the number of expected boundary points here! I'm willing to leave it for now. Let's see what we need to monkey around with to get GameObjects to check if they're colliding. We're going to need to add a function to the GameObject class so that we can check for collisions - we'll call it Intersect
public abstract bool Intersect(PointF p);
As you well know when we've put something this high level in we'll have to update all things inheriting from it.
public override bool Intersect(System.Drawing.PointF p)
{
return false;
}
So we put this handy placeholder code in Actor and Tile. Notice how with the more we add the more Tile seems to be more and more broken. We could imagine an intersect function being very useful to Tile, say if a player steps on a tile but that code is all connected up with Map. We therefore at some point will give Tiles access to the map they are apart of. And fix all these broken functions. (We could just add all the tiles to the GameObject list ... but we're going to be having so many that it really is worth having a seperate routine that's faster.)
But that's for later for now let's go and play with Actor and make it comparable too. This proved to be tricky for various reasons, let's looked closely at the code I finally knocked up:
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;
}
It's simple and there are redundant variables but I needed to spell everything out because there was some trouble with how it was working. Simply it checks it a point is withing a rectangle made by the first four points of the boundary. Basically to intersect the point needs to be greater than one less than the other. Or for the y axis less than one, greater than the other. If both these conditions are filled then the point is inside the boundary of the gameobject we're inspecting.
So now we need to make use of this handy function by creating a new collision routine in the map. (we'll probably do some consolidating later of and make sure class functionality is correctly split). So let's call the new function movementObstructed
yeah catchy I know. Let's belt this baby out:
public bool movementObstructed(PointF[] boundary, GameObject focus, PointF pos)
{
bool intersect= false;
foreach(GameObject g in GameObjects)
{
if (g.Equals(focus))
continue;
PointF topLeft = new PointF(pos.X + boundary[0].X, pos.Y + boundary[0].Y);
PointF bottomLeft = new PointF(pos.X + boundary[1].X,pos.Y + boundary[1].Y);
PointF topRight = new PointF(pos.X + boundary[2].X, pos.Y + boundary[2].Y);
PointF bottomRight = new PointF(pos.X + boundary[3].X, pos.Y + boundary[3].Y);
if(g.Intersect(topLeft))
{
intersect = true;
}
if(g.Intersect(bottomLeft))
{
intersect = true;
}
if(g.Intersect(bottomRight))
{
intersect = true;
}
if(g.Intersect(topRight))
{
intersect = true;
}
}
return intersect;
}
Also very simple and basic because of certain bugs that didn't seem to want to go away. Basically we take all four points of our moving game object and then check if any of these four points are going to intersect with the current game object. Not overly tricky but needs to be constructed with care. Right now we need to call this function when we move an Actor around.
So we add a little more to the bottom of the move function
//Check the bounding box around the feet
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)))
{
position.X = newX;
position.Y = newY;
}
}
}
So we call movementObstructed to find out if the point we're going to move to is obtructed r now! If not we move. Okay if you run it now it will work but ... it could work better. There's something odd about the collision zones - it's because the bounding boxes around the characters is too big, so they never seem to touch! We need to adjust!
While we're in the movement function though, now we have all this collision stuff maybe we should add some code to save a few cycles. I know usually I avoid this, but collision code is a place where lots of efficency can be created by doing relatively little as is the case here. If our character is standing we don't need to check where he will be standing next because there is no movement - therefore we don't need to bother with all that collision code. So let's do the following:
case States.up:
{
newY += 0.045f;
}break;
case States.standing:
{
return;
}
}
Simple but worth doing. Now let's have a look at those adjustments. I think maybe the top boundary of the rectangle is a bit high so let's mess with that first - to the Actor constructor!
boundary = new PointF[4] {new PointF(+0.04f, -0.30f),
new PointF(+0.04f, -0.49f),
new PointF(+0.19f,-0.30f),
new PointF(+0.19f, -0.49f)};
Currently we have the above, so we need to experiment with a few different values until we find something that we like.
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)};
Okay that's starting to feel a lot better and a lot smoother. Also this should work with all GameObjects - if we whack in some AI and have NPCs running around they'll collide. Cool heh?
Cleaning
Cleaning sucks because you don't get visible feed back, but to make the game easy to develop it's something that needs to be done now and again. We're not doing any massive overhaul here - instead we will deal with one thing - GameObjects. GameObjects are getting a little messy, there's some stuff that we can't do with the Tile class so they're placeholders and they're getting general crowded too. So we're going to add another intermeditery abstract class called MapObject, it will be a GameObject and have an internal reference to the map. Then we'll see what can drop down from GameObject.
public abstract class MapObject : GameObject
{
public MapObject(Texture texture) : base(texture)
{
}
}
It's very easy to get a skeleton class up. Now the main thing about a map object is about having a reference to a map, so let's sort that.
public abstract class MapObject : GameObject
{
protected Map map;
public MapObject(Texture texture, Map mapIn) : base(texture)
{
map = mapIn;
}
public void SetMap(Map newMap)
{
map = newMap;
}
}
Okay basic though it is it's doing a important job, so let's try and patch this new class in! So first we'll do Actor then tile. For Actor we have a map reference in already so it's all ready for the transfer!
public class Actor : MapObject
{
//Map map; /**POOF**/
protected PointF position = new PointF(0,0);
Next remove the set map function:
//public void SetMap(Map newMap)
//{
// map = newMap;
//}
Finally a little fiddling with constructor.
public Actor(Device device, Map mapIn, Texture actorTexture)
: base(actorTexture, mapIn)
{
IntializeVertexBuffer(device);
//map = mapIn; /**POOF**/
That was not so hard. In time we may wish to make GameObject an interface and do the heavy stuff in MapObject. Currently though it's hard to tell. The division is things like GUI aren't map objects. But they are gameobjects, inventory and conversation icons and all crazy stuff like that doesn't need map access. That stuff is a little down the path to spending too much time thinking about now though so we'll try and keep our design a little flexible.
So tile, we're going to need to makie changes the tile constructor and therefore everytime it's intialized :(
public class Tile : MapObject, IStorable
{
Constructor changes:
public Tile(Map mapIn) : base(null, mapIn)
{
}
public Tile(Texture texture, Map mapIn) : base(texture, mapIn)
{
}
Changes to map class for Tile construction:
public void GeneratePlain()
{
TextureManager.BufferTextures("Plain");
for(int i = 0; i < area; i++)
{
tiles[i] = new Tile(TextureManager.RequestTexture("Grass"), this);
}
((Tile)tiles[22]).texture = TextureManager.RequestTexture("Stone");
((Tile)tiles[22]).block = true;
}
Also this slice of code from the Read function:
tiles = new Tile[area];
for(int i = 0; i < area; i++)
tiles[i] = new Tile(this);
We could do all this with a find and replace but for me it's useful to wander back through the code. So one final change in playing game state:
public PlayingGameState(Device d, GameStateManager g, DXInput.Device dInput)
{
device = d;
gameStateManager = g;
inputDevice = dInput;
map = new Map(8,8);
Tile t = new Tile(map);
t.IntializeVertexBuffer(device);
Okay now everything works as it one did with a slight architectural change. Now we can start to fill in the Tile object so it works as a fully fledged GameObject. To do this though we need some extra functions in map to help us out!
First let's make GetTileIndex public. Then we can do tiles intersect function:
public override bool Intersect(System.Drawing.PointF p)
{
return (this.Equals(map.tiles[map.GetTileIndex(p.X,p.Y)]));
}
Next to the DX position function. First we need to find out which number it is in the array, we'll put a function in map to deal with this called GetTileIndex
(that's right we're overloading whoo!) like all the best things in life it will be as simple as humanly possible.
public int GetTileIndex(Tile t)
{
for(int i = 0; i < area; i++)
{
if(t.Equals(tiles[i]))
return i;
}
Console.WriteLine("Couldn't find a tile in the map");
return 0;
}
This function should always work - so for the fail case a rather lame error message is produced, as well as giving the wrong answer :D
Okay let's create the DX position, this should give the top corner of the tile in question although I have not tried it. It therefore probably doesn't work but I'm not the kind of guy who likes to test thouroughly unexpected bugs and like presents ... or something. So we get the tile index, then we check how many times 8 goes into it, this is the number of rows down it is. Then we take what's left after all the 8s have been taken out and this is the number of tiles X it is. Then we multiple these values by the length / height of a single tile. Then we make this number work in the DX Coordinate system by taking and adding one respectively to the values. And this should give our final answer ... though without testing this seems somewhat unlikely at least the theory is sound - so here it is:
//Not tested
public override System.Drawing.PointF GetDXPosition()
{
int index = map.GetTileIndex(this);
float tileLength = 1f/map.rowLength; // in directX co-ords
int ylevel = System.Math.Abs(index / map.rowLength);
int xlevel = index - (ylevel * map.rowLength);
PointF p = new PointF( xlevel * map.rowLength -1, //DXcoords
-ylevel * map.rowLength +1);
return p;
}
And that's the tile class fully fleshed out. We may want to have the Map classes list of objects to turn from GameObjects to MapObjects, this seems a sensible step.
public class Map : IStorable
{
public Tile[] tiles;
public ArrayList MapObjects = new ArrayList();
public void AddMapObject(MapObject m)
{
MapObjects.Add(m);
MapObjects.Sort(); //By y position
}
public void RemoveMapObject(MapObject m)
{
MapObjects.Remove(m);
}
public bool movementObstructed(PointF[] boundary, GameObject focus, PointF pos)
{
bool intersect= false;
foreach(GameObject g in MapObjects)
A crafty change above because I'm lazy. Now to PlayingGameState:
map.AddMapObject(Player);
map.AddMapObject(NPC);
}
Then the good old process loop:
map.MapObjects.Sort();
foreach(MapObject m in map.MapObjects)
{
m.Render(device);
}
And there we have it, the code is starting to feel a bit tighter now and the tile-centric rendering subroutine is well intergrated. Also we can bring the CompareTo interface down to the map object so it looks like:
public abstract class MapObject : GameObject, IComparable
...
public int CompareTo(Object rhs)
{
GameObject g = (GameObject)rhs;
return g.GetDXPosition().Y.CompareTo(this.GetDXPosition().Y);
}
And this info is removed from GameObject. That's the last bit of tidying that we'll do for now ... there is more to do though - maps rendering is a bit all over the show.
Back to the main thread of getting an NPC to interact with
We have the wonderful .tga file - TextureManager is still a bit flakely so for now we are going to put in an NPC variable, so we can get some results.
static public Texture player;
static public Texture NPC;
static private Device device;
static private bool Plain = false;
static public void SetDevice(Device d)
{
device = d;
player = TextureLoader.FromFile(device, @"C:\playerrun2.tga");
NPC = TextureLoader.FromFile(device, @"C:\NPC.tga");
This a bit hacky but we need to give the TextureManager and Saving and Loading a bit of a revamp before we can do something more elegant.
The Actor expects a cetain size of TGA so it can pick out the correct frames. So I've just expanded the texture so it will pick our new NPC as the default standing frame.
1 comment:
It's me again !!
Wonderful tuto!!
I've never wished to find something usefull as these ones. I hope you'll try to write something like a "How to start programming 2D game in C#" book.
I just can't wait to read all the next chapters ^^
Post a Comment