Stage 4 - 1 : Presence
So presence - what the hell am I talking about? Well I'm talking about not being able to walk through trees and the player having some relation to the tiles he's walking on. How the character is connected to the tiles is a branching point in the design on many RPGs I'm most likely going to choose a way now and then stick with it. It's unlikely I'll cover numerous different ways. You should follow the tutorial for now and then when you feel more confident but in the system you would most like to use.
Cleaning up 'Actor'
What do we want the Actor class to be?
Currently Actor class is reflecting more and more the one instance we have of it - that is, it's becoming a hold all for the player class. It's likely we'll have a seperate player class, or playable class.
Difference between a 'GameObject' and an 'Actor'
A GameObject is something in the game that requires a graphical representation. An 'Actor' is something that the player might interact with or might be a NPC.
Actor currently has a lot of movement information infact it's currently:
Actor = GameObject + MovementInformation
Also it's texture loading methods are specific to a player. Without defining some preset texture info though it's going to be tricky. For now we'll assume on the whole Actor is okay - the only thing that needs changing are the small functions saying Left, Right, Up, Down, Stand - get rid of all of them and replace it with this one:
public void SetMovementType(States movementInfo)
{
currentState = movementInfo;
}
Due to this we need to make a few minor changes in PlayingGameState
private void UpdateInput()
{
DXInput.KeyboardState state = inputDevice.GetCurrentKeyboardState();
if (state[DXInput.Key.Return])
{
/** Enter has been pressed **/
}
if(state[DXInput.Key.RightArrow])
{
Player.SetMovementType(Actor.States.right);
}
else if(state[DXInput.Key.LeftArrow])
{
Player.SetMovementType(Actor.States.left);
}
else if(state[DXInput.Key.UpArrow])
{
Player.posY += 0.1f;
}
else if(state[DXInput.Key.DownArrow])
{
Player.SetMovementType(Actor.States.down);
}
else
{
Player.SetMovementType(Actor.States.standing);
}
Tile highlighting
Let's make the tile texture change where our player is standing. First though a few notes: currently player has posX and posY now this is where he is rendered from - i.e. the top left corner. So our player is currently represented by a point and that points not even in a good poistion. Really he should be identified by a rectangle around his feet.
Pictured here is the location of Player.posX and Player.posY, it is the red pixel!
If we represent him by a rectangle around his feet (what he's going to be colliding into things with) then we have four points to check against scenary and other tiles. This rectangle could be a tighter fit than the quad we use, if we wanted to get really complicated we could do pixelxpixel collision detection - but we don't and a nice box should suit us fine!
The other point, of course, is that posX and posY both use DirectX co-ordinates - so they are a percentage of the screen and start at -1,-1. We can make things easier on ourselves by adding onem then the points are from 0-2 (and later dividing by 2 so we're working with one - it's easier for dealing with percentages). I hack out my functions to do this point detection stuff there may well be more efficent or more simple algorimths if you know something that would work better use that!
How do we get the points that make up the rectangle? We create them in relation to the top corner of the sprite. So the very bottom points will be -.50Y away. So to get the bottom half of the rectangle we can using the following values inreference to the top corner value.
Where are we? ... Press Enter.
Okay now let's do a little bit of programming to see if we can put this information to use. We'll take over the Enter key press for now in the PlayingGameState. Everytime we press Enter we'll turn the tile(s) the character is currently stepping on into Stone texture tiles.
To do this we'll probably create some throw away functions, stuff thats just there to prove a point and later will be incorpareted into an appropriate class (probably the map class for a lot of this stuff)
Are you ready for a needless complicated function? Are you crying "Why oh why did he use a straight array rather than a matrix?" - well you will be! Let's get started!
Okay in each direction we have 8 tiles, so each 12.5% of the length equals a different tile. Let's start by defining this percentage tile length.
float tileLengthPercentage = 1f/8f
Let's consider the X direction, to turn it to a percentage we need to add one and divide by 2.
float percentagePosition = (posX + 1)/2f;
-1 ------------ 0 ------------ 1
Add 1
0 ------------ 1 ------------ 2
Divide by 2
0 ------------0.5------------ 1
Now we need to find what number tile it is in on the X axis. We divide the percentagePosition of our posX
by the size one tile length as a percentage of all the tiles length. Then we round this number down and
this gives us our X tile number. That is how far along the X row it is.
xRowNumber = Math.Floor(percentagePosition/tileLengthPercentage);
Now let's tackle Y, it's pretty much the same apart from that we going to take 1 away to get the percentageY position and then take the absolute value - i.e. knock off the negative sign. This way Y gives us the row number when we divide by the tileLengthPercentage.
float percentagePositionY = Math.Abs((posY +1f)/2f);
int yRowNumber = (int)Math.Floor(percentagePositionY/tileLengthPercentage);
So really similar.
Each Y is a different row - so each 1 Y is worth 8 tiles. Each 1 X is worth 1 tile. So to get the final tile number in the array we use:
return (yRowNumber * 8) + xRowNumber;
Let's get a looksee at this function in full.
public int GetTileIndex(float posX, float posY)
{
float tileLengthPercentage = 1f/8f;
float percentagePosition = (posX + 1f)/2f;
int xRowNumber = (int)Math.Floor(percentagePosition/tileLengthPercentage);
float percentagePositionY = Math.Abs((posY -1f)/2f);
int yRowNumber = (int)Math.Floor(percentagePositionY/tileLengthPercentage);
return (yRowNumber * 8) + xRowNumber;
}
So it takes in a directX X,Y position and it coverts this into a single number that represents which tile sits under the X,Y position. A pretty useful function, not wonderfully clear unfortunately but that's the nature of the beast. This is the kinda function you would document well because you are unlikely to change it - unless you find a bug and thats when you need your documentation.
Oh this function should just be thrown into PlayingGameState
for now.
Okay let's get into playing game state and into the input function namely: UpdateInput
. We'll check all four points of our rectangle and then we'll take all the ints and change the texture for each one - of course this is going to redundant most of the time but now is not the time for clever!
TIP: When you're using floats
remember to put 'f' after the number you're typing in, or all types of trouble occurs, if it lets you do it at all.
private void UpdateInput()
{
DXInput.KeyboardState state = inputDevice.GetCurrentKeyboardState();
if (state[DXInput.Key.Return])
{
/** Enter has been pressed - let's highlight a tile**/
//Top Left Hand Corner
Tile t = (Tile)Map[GetTileIndex(Player.posX, Player.posY -0.25f)];
t.tileTexture = this.stoneTexture;
//Bottow Left Hand Corner
t = (Tile)Map[GetTileIndex(Player.posX, Player.posY -0.50f)];
t.tileTexture = this.stoneTexture;
//Top Right Hand Corner
t =(Tile)Map[GetTileIndex(Player.posX + 0.25f, Player.posY -0.25f)];
t.tileTexture = this.stoneTexture;
//Bottom Right Hand Corner
t =(Tile)Map[GetTileIndex(Player.posX + 0.25f, Player.posY -0.50f)];
t.tileTexture = this.stoneTexture;
}
Okay rub your hands together and fire up your application. Walk around and press [enter], you'll notice that any tiles that you players feet are hovering over will turn to stone! MAGIC SPELL STONEWALK++. Of course you can tighten the rectangle up so it's really pixel tight and you'll aways be able to see the feet of your character on the one thats turned to stone.
You'll note that is seems that you always produce 4 tiles in stone around you - this is because you are covering a very large area currently with the boxed in area and this overlaps lots of tiles unless you are dead center and even then. Try plugging in these numbers for a smoother experience, the little.
Tile t = (Tile)Map[GetTileIndex(Player.posX+0.04f, Player.posY -0.30f)];
t.tileTexture = this.stoneTexture;
//Bottow Left Hand Corner
t = (Tile)Map[GetTileIndex(Player.posX+0.04f, Player.posY -0.49f)];
t.tileTexture = this.stoneTexture;
//Top Right Hand Corner
t =(Tile)Map[GetTileIndex(Player.posX + 0.19f, Player.posY -0.30f)];
t.tileTexture = this.stoneTexture;
//Bottom Right Hand Corner
t =(Tile)Map[GetTileIndex(Player.posX + 0.19f, Player.posY -0.49f)];
t.tileTexture = this.stoneTexture;
That works out nicer.
You may be thinking - why have we done this what is the point? well when we want to add an obstacle we'll we jump to the move function - we'll work out where the potential move will take us - then we'll throw in all the points of the rectangle to a function like that above. Any tile we intersect that is blocking us, will prevent the move from taking place.
Before we do that though - a map structure may be in order or some minor extension to the tile class.
Simple Map class
To make the simple map class we'll want to start a nice new project, don't want to lose any of our excellent stone walk work. To do this we'll just copy over what we currently have and then modify it.
A nice simple class skeleton to start off with and then we'll start punching in some details.
using System;
namespace GraphicsAndTime
{
public class Map
{
}
}
Well we're going to need some nice tile for our map, so let's look into that.
public Tile[] tiles;
private int area;
private int width, height;
public Map(int Width, int Height)
{
width = Width;
height = Height;
area = width * height;
tiles = new Tile[area];
}
Notice the other variables in there as well. These are for book keeping purposes. Also if we wanted to alter our map class, maybe stick in a two-dimensional array (probably a good idea but something I'm irrationally loathe to do) then it's not such a big job. Also we allow some custom map creation using the constructor. Speaking of custom map creation, we want to generate some detail on the map, so we're going to create a rather hackish function called the GeneratePlain function. Here I also puts some comments about how this function may be generalized in the future.
//May want to replace this with a manager pattern and thus
//create a simpler generate function
public void GeneratePlain()
{
for(int i = 0; i < area; i++)
{
tiles[i] = new Tile(grassTexture);
}
}
Okay so it's easy to see a problem here: grassTexture
, this is the texture we'd like the tile to be but . . . that texture hasn't necessarily been loaded! We don't want to mess up our map class by having to load textures and access the disk directly. We don't really want to shove it into the tile class, which I believe is already burdened by a lot of unecessary material.
SO how do we solve this problem? With a hack of course! Let's hack up some nice code, and then later we'll come back to it and give a bit more polish. In fact that is what we're doing right now with th map class - we're taking all that Tile bother out of the PlayingGameState
class.
If you know a bit about Design Patterns or general buzzwords on game developers boards you may know that Clock
can be refered to as a Singleton that is because we only ever have need of one clock, and we make it globally accessible. There's a line of thought that says too many singletons are rather bad coding practice. Don't let it stop you using them though, they're very handy things. In fact I'd used them whenever you feel like it and only stop to worry if for some reason they're causing you bother - you cannot understand how all your code is working for instance :)
Singleton Texture Class
Another new class, this one will handle textures for us! We're only scratching out it's outline at first, which is a good thing to do if you have a good idea of what the finished piece is going to look like! So let's start with a skeleton class.
We're getting quite a few classes, infact it may be time to make some folders and group some of them up so it's easier to work with what we've got. We may deal with that after this section.
TextureManager.cs
is what we're going to call this find new class.
public class TextureManager
{
public TextureManager()
{
}
}
At some point we're going to want to use a Hash Table to store this info
So first we need a data structure to store our textures. We're going to use a simple linked list using C# array list. In the future though we might well want to use a hash table. Because its O(1) access where as a link list is going to be O(n). If you're not sure what that means it probably indicates you're not currently undertaking a computer science degree (which for your sanities sake is probably for the best). It is an abstract measure of effiecency, so if you where creating this data structures using paper cards and bits of string these measurements still apply. For a fuller explanation look it up on the web. Or maybe I'll go off on a tangent in the near future and rant about it for a bit
public class TextureManager
{
private ArrayList textureList = new ArrayList();
public TextureManager()
{
}
}
Okay wonderful now we need to load textures or sets of texture into our shiny new texture list. We'll create a BufferTextures
function to do just that. To load these textures though we need access to Device. Being the TextureManager
we're going to be needing access to Device a lot, so much that it should store it's own local version.
So first we'll have the all important SetDevice
function that must be called before anything else.
static private ArrayList textureList;
static private Device device;
public TextureManager()
{
}
static public void SetDevice(Device d)
{
device = d;
}
Now before we forget let's put a call to SetDevice whenever the device is created. Quickly to the
main function:
static void Main()
{
GraphicsAndTimeForm form = new GraphicsAndTimeForm();
form.InitializeGraphics();
form.InitializeInput();
TextureManager.SetDevice(form.device);
form.gameStateManager = new GameStateManager(null);
TitleScreenState titleScreenState = new TitleScreenState(form.device,
form.gameStateManager,
form.deviceInput);
With that safetly out of the way we can build our buffer texture function.
static public void BufferTextures(string textureSet)
{
if(textureSet.Equals("Plain"))
{
try
{
textureList.Add(TextureLoader.FromFile(device, @"C:\grass.bmp"));
textureList.Add(TextureLoader.FromFile(device, @"C:\stone.bmp"));
}
catch(Exception e)
{
if(device == null)
{
MessageBox.Show("TextureManager's Device variable needs
+ to be set before being used!"
+ e.ToString(), "oops");
}
else
{
MessageBox.Show("There has been an error loading the textures:"
+ e.ToString(), "oops");
}
}
}
}
This loads the "Plains" texture set - that is the textures one might find on the plains - rock and stones in this instance. We pass in a string because in the future this could be a file name describing the plain textures and we could load them in (of course being careful to check that they where not already present). So all we do at the moment is pass in the string "Plains", then the texture set is hard coded to load in. There are some relevant error messages to show us what could be going wrong if they are summoned.
So Stone and Grass textures are loaded in grass in 0, stone is 1. Of course if we have lots of textures they could be any number! So we must careful and will probably make use of a nice hash table so we can request textures by name and recieve a reference quickly. All this though is in the future, currently we are merely making something that is a bit hackish. So let us continue we with another hackish function requestTexture
static public Texture RequestTexture(string textureName)
{
if(textureName.Equals("Grass"))
{
return (Texture) textureList[0];
}
else if(textureName.Equals("Stone"))
{
return (Texture) textureList[1];
}
else
{
MessageBox.Show("Requested texture cannot be found!: ", "oops");
return null;
}
}
Okay there is our function carefully tailored to work for only this instance and break for all others :). So let's plug it all in and see how this bad boy is going to work.
Back to the map class
//May want to replace this with a manager pattern and thus
//create a simpler generate function
public void GeneratePlain()
{
TextureManager.BufferTextures("Plain");
for(int i = 0; i < area; i++)
{
tiles[i] = new Tile(TextureManager.RequestTexture("Grass"));
}
}
The above function creates a big grassy field out of our map. Every tile is given the grass texture.
Let's put it into the maps constructor for now:
public Map(int Width, int Height)
{
width = Width;
height = Height;
area = width * height;
tiles = new Tile[area];
GeneratePlain();
}
That is our map intialized now we need to allow PlayingGameState to render it.
In PlayingGameState
we need to add a new map variable. For now we'll leave the other stuff as it is, until we have confirmed that our map is working.
public class PlayingGameState : GameState
{
private Texture grassTexture;
private Texture stoneTexture;
private Map map;
private Actor Player;
private Device device;
In PlayingGameStates constructor we shall intialize the map:
public PlayingGameState(Device d, GameStateManager g, DXInput.Device dInput)
{
device = d;
Tile.IntializeVertexBuffer(device);
gameStateManager = g;
inputDevice = dInput;
map = new Map(8,8);
LoadTextures();
IntializeMap();
Here too we'll leave the old Map stuff until we're certain this class is ready to replace it all. Into the process function next for the moment of truth - alter the code so it looks like below:
float x = -1f; //Remember we're using Cartesian
float y = 1f;
for(int i = 0; i < 64; i++)
{
Tile t = (Tile) map.tiles[i];
QuadMatrix.Translate(x,y, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.SetTexture(0, t.tileTexture);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
if(((i+1) % 8) == 0)
And woo-hoo it works (of course putting this amount of code together, means that the coder will have over looked a few things - I did but I've repaired them now. The clever coder should really check at smaller intervals, but things work so everything is good)
Cleaning Up
Our new super cool Map class is working as we wanted now we have to remove all the crap that was previously covering for our yet as uncreated map class. Then a few other other minor changes need to be considered also.
Our cuts are going to be in the playinggamestate code.
public class PlayingGameState : GameState
{
private Texture grassTexture;
private Texture stoneTexture;
private Map map;
private Actor Player;
private Device device;
private ArrayList Map;
private GameStateManager gameStateManager;
private DXInput.Device inputDevice;
We'll cut out those variables shown in bold.
Next up the public void LoadTextures()
procedure - cut that. Then let's trim the constructor like so:
public PlayingGameState(Device d, GameStateManager g, DXInput.Device dInput)
{
device = d;
Tile.IntializeVertexBuffer(device);
gameStateManager = g;
inputDevice = dInput;
map = new Map(8,8);
LoadTextures();
IntializeMap();
Player = new Actor(device, @"C:\playerrun2.tga");
}
Seeing as we're no longer calling that IntializeMap
procedure we'll cut that as well.
If like me you copied everything wholesale from our previous project then, there is still all that input stuff to make the tiles turn to stone. This can easily be updated like so:
if (state[DXInput.Key.Return])
{
/** Enter has been pressed - let's highlight a tile**/
//Top Left Hand Corner
Tile t = (Tile)map.tiles[GetTileIndex(Player.posX+0.04f, Player.posY -0.30f)];
t.tileTexture = TextureManager.RequestTexture("Stone");
//Bottow Left Hand Corner
t = (Tile)map.tiles[GetTileIndex(Player.posX+0.04f, Player.posY -0.49f)];
t.tileTexture = TextureManager.RequestTexture("Stone");
//Top Right Hand Corner
t =(Tile)map.tiles[GetTileIndex(Player.posX + 0.19f, Player.posY -0.30f)];
t.tileTexture = TextureManager.RequestTexture("Stone");
//Bottom Right Hand Corner
t =(Tile)map.tiles[GetTileIndex(Player.posX + 0.19f, Player.posY -0.49f)];
t.tileTexture = TextureManager.RequestTexture("Stone");
}
So that's that the redundant code removed and it's compiling as before. PlayingGameState looks a bit leaner again and the map function seems perfectly suited totally absent of things that are not essential.
Now we'll get rid of some of those magic numbers, first let's a add an accessor to Map.cs, it should look as below:
public int tileTotal
{
get
{
return area;
}
}
We may want to move some of PlayingGameState's process loop into the map function but for now we won't bother with it, except to change those meddlesome magic numbers. So let's do that now:
...
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, Quad
...
We need one last accessor for the row size. This part is really something that Map should be taking care of internally but for now it's something we can turn a blind eye to. So in Map we add:
public int rowLength
{
get
{
return width;
}
}
If all this get/set stuff is new to you, all we're doing is making a couple of the private variables - read only to the outside world.
Now let's remove another magic number in the process loop:
if(((i+1) % map.rowLength) == 0)
{
x = -1f;
y -= 0.25f;
}
Boundaries
It's what we've been waiting for, now we have a nice map class we can implement a few boundaries. Let us set our first task as preventing the player walking off the window.
When taking input we'll get the number of where the player would move, then we query the map and ask if we can move there, the map replies and we move or we don't. As simple as that. I think we should have a function in the Map called CanMove
.
Let's have a looksee at a skeleton function:
//Return true if the x,y specified is not blocked
//X,Y specified in DirectX Cartesian Co-ordinates
public bool CanMove(float x, float y)
{
return true;
}
We need to see if the X,Y is out of the box so to speak so if
y > 1 or
y < -1 or
x < -1 or
x > 1
I always have trouble choosing the correct 'or' so here from the MSDN library is the one we should use:
The operation x || y corresponds to the operation x | y, except that y is evaluated only if x is false.
Weeee so that's the one we'll use shouldn't take a second.
if(x > 1 || y > 1 || x < -1 || y < -1)
return false; //because you'd be walking out of the window
Now all we need is a bit of map consultation, for this we need to get inside the Actor class, and also make sure the Actor class is able to get access to the Map to call this function. We may as well stick this in the constructor and also add a function called SetMap
in case we start to deal with more than one map. Though in such a case a meta map will probably given to Actor - put we are getting adhead of ourselves.
The thing that will control all nine maps at once is what will be put into Actor in the final version.
public Actor(Device device,
Map mapIn,
string texturePath) : base(device, texturePath)
{
...
map = mapIn;
...
Okay we also need to add a map variable in their as well.
public class Actor : GameObject
{
Map map;
public enum States
Okay, good. Now we just make a set map function like so:
public void SetMap(Map newMap)
{
map = newMap;
}
That wasn't too hard, it's tempting to put these things into game object but they'll work where they are for now. Actor is a bit confused anyway and will hopefully be easier to sort out once we add another NPC actor.
Now let's make sure that constructors getting a Map to construct itself with. It all happens in the PlayingGameState constructor - let's have a closer look:
public PlayingGameState(Device d, GameStateManager g, DXInput.Device dInput)
{
device = d;
Tile.IntializeVertexBuffer(device);
gameStateManager = g;
inputDevice = dInput;
map = new Map(8,8);
Player = new Actor(device, map, @"C:\playerrun2.tga");
}
So all that is pretty groovy, no we actually need to put the bounds checking somewhere where it counts, namely the move
function in ... that's right Actor
.
private void Move()
{
switch(currentState)
{
case States.down:
{
if(map.CanMove(posX- 0.045f, posY ))
posY -= 0.045f;
}break;
case States.right:
{
if(map.CanMove(posX + 0.04f, posY))
posX += 0.04f;
}break;
case States.left:
{
if(map.CanMove(posX, posY-0.04f))
posX -= 0.04f;
}break;
}
}
Ah, it seems we don't have UP included here because I never did an animation for it. Well we may as well add it now, a little code alteration here and some over a PlayingGameState and everything will be fine.
public enum States
{
standing,
down,
left,
right,
up
}
Add this to the Move
procedure:
case States.up:
{
if(map.CanMove(posX, posY + 0.045f))
posY += 0.045f;
}break;
Now to alter the input code in PlayingGameState!
else if(state[DXInput.Key.UpArrow])
{
Player.posY += 0.1f;
}
becomes
else if(state[DXInput.Key.UpArrow])
{
Player.SetMovementType(Actor.States.up);
}
Run it and note how odd it is! Why? Because we forgot to take the hot spot(s) into account :o. Even after I spent the first section of this chapter talking about them - doh! We still referencing the players position here, from the very top corner of his sprite. In this particular case we probably want the bounding box to surround the visible sprite.
We need to throw in four more variables some where - bounded X left and right, bounded y up and down. Let's add the feet positions to the Actor class. We just take the offsets we were using before when creating stone tiles on the ground.
We don't need all four points this time because when we're move say left, we are only going to encounter boundaries to the left of us. This means we can cheat a bit on how many ints we need, a shortcut that we will no doubt have to rectify later. So to Actor we add these four accessors:
private float posFootLeft
{
get {
return posX+0.04f;
}
}
private float posFootRight
{
get {
return posX + 0.19f;
}
}
private float posFootUp
{
get {
return posY -0.30f;
}
}
private float posFootDown
{
get
{
return posY -0.49f;
}
}
Okay now back to the Move
procedure and lets change hot spot. If you're finding it hard to follow how we intend to bound the character on all four sides, using merely two pairs of X,Ys don't worry because at some point we'll probably switch back to the four points merely for claritys sake.
Okay so it's a bit hackish in places, put it works. Watch in amazement as your character can now not leave the playing area!
More Boundaries - this time Rocks! and boundaries in general!
I know what you're thinking - it doesn't get much more exciting than rocks, and that's where you'd be right! In this brief section we'll make the code we've just written rather cleaner, and make it so that the player cannot walk over stone tiles!
We don't want to be making stone tiles when we press return anymore - we'll just end up stuck everytime we press the enter key. So let's clean out that code.
if (state[DXInput.Key.Return])
{
//all gone!
}
Lets add a 'blocking' attribute to the tile definition. As well as some code. Tiles may be blocked sometimes because NPCs or enemies are there. So we really need an isBlocked function that can check all the conditions the tile is experiencing at that moment.
public class Tile
{
static public VertexBuffer vertexBuffer;
public Texture tileTexture;
private bool Blocking = false;
public bool block
{
set
{
Blocking = value;
}
}
//We may want to check if anything is occupying the tile.
public bool IsBlocked()
{
return Blocking;
}
Okay let's go to the Plain generation and add a stone tile that is blocking.
public void GeneratePlain()
{
TextureManager.BufferTextures("Plain");
for(int i = 0; i < area; i++)
{
tiles[i] = new Tile(TextureManager.RequestTexture("Grass"));
}
((Tile)tiles[22]).tileTexture = TextureManager.RequestTexture("Stone");
((Tile)tiles[22]).block = true;
}
Currently we can walk all over it - we can take our function from before (it was called GetTileIndex) and calculate whether our new movement will put us into a blocked area and if so we don't can't make the move. So we'll copy that over to the map function.
Now some quick editing of the CanMove function:
public bool CanMove(float x, float y)
{
if(x > 1 || y > 1 || x < -1 || y < -1)
return false; //because you'd be walking out of the window
int newTile = GetTileIndex(x,y);
if(newTile >= area)
return false;
if(tiles[newTile].IsBlocked())
return false;
return true;
}
If you run the code you'll note that the character is kind of blocked when attempting to walk on the tile but not totally blocked. This is because of our earlier shoddy coding. We'll recify that now.
Let's go to the Move
procedure and make a few changes! We need to do four CanMove checks to ensure all four points (well we could do less if we took into account the direction we where walking in that is, but we're not going :P) So we need to shape up this Move function a bit so we can do all these functions at the end. Let's see the finished piece:
private void Move()
{
float newX = posX;
float newY = posY;
switch(currentState)
{
case States.down:
{
//if(map.CanMove(posFootLeft, posFootDown - 0.045f))
newY -= 0.045f;
}break;
case States.right:
{
//if(map.CanMove(posFootRight + 0.04f, posFootUp))
newX += 0.04f;
}break;
case States.left:
{
//if(map.CanMove(posFootLeft-0.04f , posFootUp))
newX -= 0.04f;
}break;
case States.up:
{
//if(map.CanMove(posFootLeft, posFootUp + 0.045f))
newY += 0.045f;
}break;
}
//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))
{
posX = newX;
posY = newY;
}
}
And there we go, it's now fully working. You cannot get on that stone tile it will block you in all directions! But you can walk past the tile below and your head will not be blocked by it. This creates an illusion of depth.
It is also best if you remove all that posFootLeft etc, it was rather messy and ugly we've just hard coded here for now and it is nicer currently.
Groovy, that's quite a nice important chunk polished off. Seeing as we have a map, it would be nice if we could load and save it. Of course as we make tiles / maps more complicated these functions are going to grow - but as a say proof of concept let's see if we can do it now.
Loading and Saving
Here we can make use of interfaces:
interface IStorable
{
void Read( );
void Write(object);
}
All we want to do is save the map - so basically the order of the tiles, whether the tiles are blocking or not and what texture they are using. If you know about Serialization you know this could be done pretty easily and we may play with this later but for now we'll use a mix of readable and binary data.
Saving and loading a single tile
We'll probably make use of that handy enter key again to handle this. Okay so if you haven't created the interface above add it now! Currently we don't care about file sizes or anything like that at all. The only thing we want to do is to be able to save the map and load it. There are no other requirements.
Let's head on over to the Tile class and implement this storable interface.
public class Tile : IStorable
{
and let's stick in skeletons of the two functions:
public void Read()
{
}
public void Write(object o)
{
}
Wunderbar.
So what is our Tile write out format going to look like? Well simple very simple, something like below:
Stone
True
So Texture name followed by if it's blockable or not. Two lines per tile.
First we need to get a texture name from our Texture data type. To do this we're going to need a new function in the TextureManager
this is going to be a hackish function that will be replaced later with something more general.
Remember to include using System.IO;
at the top of the program. This goes for any code file where we're playing with input output.
So the function we're going to add to TextureManager is this:
static public string LookUpTexture(Texture t)
{
if (t.Equals( textureList[0]))
{
return "Grass";
}
else
return "Stone";
}
It look up a texture and gives the name we use for it inside the game - provided of course it's Grass or Stone, otherwise it returns Stone. Otherwise it's super perfect, now let's get back to that writer function in Tile.
Let's do a little coding and we'll have a nice write function:
public void Write(object o)
{
StreamWriter writer = (StreamWriter) o;
writer.WriteLine(TextureManager.LookUpTexture(tileTexture));
if(Blocking)
writer.WriteLine("True");
else
writer.WriteLine("False");
}
Now we need but test it out - let's head over to PlayingGameState and get into the piece of code that runs when we press enter. Currently our it should look like so:
if (state[DXInput.Key.Return])
{
}
If it doesn't look like this, then make it look like this. We're going to write some code so that when we press enter we save a certain tile. Make sure to put using System.IO
at the top of the code!
Okay let's make it so that it creates a new text file in the root f C: and writes the first tile to that
file.
if (state[DXInput.Key.Return])
{
using (StreamWriter sw = new StreamWriter(@"c:\MapFile.txt"))
{
map.tiles[0].Write(sw);
}
}
And bingo it works fine:
We Should also check tile 22 as this gives the other case - the blocking stone tile. Let's check and yes it is true also - great.
Let's write a simple Read function and have it read in as well. It seems we need to alter the IStorable interface too so it reads
public interface IStorable
{
...
void Read(object o)
}
Then we must change the Read function in the tile to reflect this alteration of the interface. Then we write a nice read function. Note that all these functions are rather fragile, they don't even have try and catch statement if something breaks, then everything falls apart.
public void Read(object o)
{
StreamReader reader = (StreamReader) o;
string line1 = reader.ReadLine();
string line2 = reader.ReadLine();
if(line1.Equals("Grass"))
{
tileTexture = TextureManager.RequestTexture("Grass");
}
else if(line1.Equals("Stone"))
{
tileTexture = TextureManager.RequestTexture("Stone");
}
if(line2.Equals("True"))
{
Blocking = true;
}
else
Blocking = false;
}
Now let's look at PlayingGameState again:
private void UpdateInput()
{
DXInput.KeyboardState state = inputDevice.GetCurrentKeyboardState();
if (state[DXInput.Key.Return])
{
using (StreamWriter sw = new StreamWriter(@"c:\MapFile.txt"))
{
map.tiles[22].Write(sw);
}
using (StreamReader sr = new StreamReader(@"c:\MapFile.txt"))
{
map.tiles[22].Read(sr);
}
}
Okay we now write the tile data and then we read it. Let's alter the code so we read it into a different tile, say tile 1.
map.tiles[1].Read(sr);
We've copied tile 22 to position 1. And it now blocks as well - pretty cool, show that our blocking / presence code is roboust and can handle tiles that are not numbered 22 :).
So we can now load and save tiles. That's pretty spiffy but we want to load and save maps, shouldn't be too hard all we need do is add a little header information - the size of the map, maybe a name (we're not going to do this at the moment though), a list of which tile set we're using - remember in this instance we're using as TileSet called "Plains" :))
Let's play with the Enter key a bit more first though ...
First let's get the code to write out all the tiles. The code we use to do this says, look at all the tiles in map, if they are IStorable, then call their Write
method. Let's see how this looks in actual code:
if (state[DXInput.Key.Return])
{
using (StreamWriter sw = new StreamWriter(@"c:\MapFile.txt"))
{
foreach(IStorable t in map.tiles)
{
t.Write(sw);
}
}
/**
using (StreamReader sr = new StreamReader(@"c:\MapFile.txt"))
{
map.tiles[1].Read(sr);
}
**/
}
Perfect the text file looks something like this:
Now we just need to redo the reading code and we're away.
using (StreamReader sr = new StreamReader(@"c:\MapFile.txt"))
{
foreach(IStorable t in map.tiles)
{
t.Read(sr);
}
//map.tiles[1].Read(sr);
}
Okay it's time to step this all up a notch.
Storing the map!
public class Map : IStorable
{
Don't forget the skeletons of the implemenation - you may have noticed that VS offers to put these in for you, after you write the above, by pressing Tab when the window prompts you to - it's nice like that.
public void Read(object o)
{
}
public void Write(object o)
{
}
Okay we need to put some stuff in there, let's go for:
textureset
width
height
all the tiles
Nice, but it's worth baring in mind that we may want to use a number of texture sets on one map. Therefore if doing it all by text we'd put a number before texture sets to say how many we were doing. For now though, as always, I like simple.
Let's start with writing, this time we'll take care of most of the IO ourselves. So the object being passed into Write and we will assume it is a string.
public void Write(object o)
{
String outputFile = (string) o;
try
{
using (StreamWriter sw = new StreamWriter(outputFile))
{
sw.WriteLine("Plain"); // should be a variable somewhere :0
sw.WriteLine(width);
sw.WriteLine(height);
foreach(IStorable t in tiles)
{
t.Write(sw);
}
}
}
catch(Exception e)
{
MessageBox.Show("Error writing map file: " + e.ToString(),
"Write Error");
}
}
Now lets use the Enter key once more to taste this baby.
if (state[DXInput.Key.Return])
{
map.Write(@"c:\MapFile.txt");
}
Works fine! Now we need to load this nice new map file in!
Notice when creating this we have coded - bottom up started with small bits, tested them and then created the bigger bits - this I believe is the bext way to code IO. Otherwise you'll end up with some error somewhere and you won't have clear idea what's going on. This system, although currently very simple gives you a clear view of whats going on at every step. Now we need to read this map though, to do this will mean creating a new map and a new set of tiles. To do this in a useful way we need to create blank tiles that can have their data read into them - What does this mean? It means tile needs a new constructor:
public Tile()
{
}
And there it is, okay maps read function is going to be of reasonable length, let's see it!
public void Read(object o)
{
String inputFile = (string) o;
using (StreamReader sr = new StreamReader(inputFile))
{
string textureSet = sr.ReadLine();
int newHeight = int.Parse(sr.ReadLine());
int newWidth = int.Parse(sr.ReadLine());
height = newHeight;
width = newWidth;
area = width * height;
TextureManager.BufferTextures("Plain"); //this will mean we're buffering
// the texture twice! But this is something TextureManager should deal
// with.
tiles = new Tile[area];
for(int i = 0; i < area; i++)
tiles[i] = new Tile();
foreach(IStorable t in tiles)
{
t.Read(sr);
}
}
}
So we create a map to the size the file says, then we intialize all the tiles, then we read dat into all the tiles. Also we load the texture set, as noted by the comment though - this causes TextureManager to load the TextureSet again. This is a problem of texture manager though. So let's test this out, let's look to our Enter code one last time:
if (state[DXInput.Key.Return])
{
map.Write(@"c:\MapFile.txt");
map.Read(@"c:\MapFile.txt");
}
So we can maybe comment out the first line and then edit the map file by hand and see the changes loaded - try that a few times. If you alter the size bear in mind that the player is bounded by the Window and not the map, and that the tiles are stored in a one dimensional array.
So we now have saving and loading functions for the map. It was pretty easy and it's easy to see how it can be expanded upon. You could go off and start making level editors and the like or adding more information to the map - there's plenty of pathways that start here.
What we've done
- Created Obstructions and made more simple to add
- Made a rough sketch of how our TextureManager will look
- Create a nice map class
- Introduced loading and saving of maps
- Created a simple map format that can be edited by hand
- PlayingGameState is a little leaner once again
So we've covered quite a bit! Big pat on the back, a lot of grunt work this time - much of the stuff behind the scenes. Its easier to work when you can see the changes take place!
What's next?
What is next, once again we've finished a section but there are many different directions we could now take to further our wonderful RPG creation. Monsters would nice a some point, but this requires an amount of basic work that I'm not willing to cover in one go.
- NPC
- GameObjects such as a bush and a rock
This is what will aim to do in the next section. A nice NPC should clear up the Actor class nicely! And our plain will look a little less boring if we put some bushes and whatnot in their good - good, see you then!
Fixing things
The Enter key
'Enter' is an awfully useful key we're probably better swapping the load and save keys for some little less well known!
if (state[DXInput.Key.Return])
{
}
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");
}
This changes load to LeftAlt-L and save to LeftAlt-S and leaves the Enter key free for more experiments later.
The Texture Machine
If you keep press Alt-L or Enter if you haven't made that change then you keep loading new textures into memory that are already present. If you have a lot of patient this is going to cause the machine to run out of memory. It's also potential wasteful. Even though we know that TextureManager is something that will be refined - this is just too big a possible bug to leave until later. It can be dealt with very simply, we'll create a boolean called "Plain". To do this for many we may well use another hash table. Or n linklist of bools or strings. For now though we will continue to keep things simple.
public class TextureManager
{
static private ArrayList textureList = new ArrayList();
static private Device device;
static private bool Plain = false;
And when we come to buffer textures we add the follow little snippet:
static public void BufferTextures(string textureSet)
{
if(textureSet.Equals("Plain"))
{
if(Plain)
return;
else
Plain = true;
5 comments:
I really liked your tutorials, but it seems that after the initial loading tutorials, the information gets a bit scattered.
Many steps are left out or only vaugly referenced. If you could post the complete source at the end of each tutorial to reference that would be a huge help.
As it is, I followed your tutorials well enough up until the timers. While I tried to continue on, it was impoossible and now the code I have is all kinds of screwed up.
Just a suggestion.
I would but I have no webspace :(
If you can tell me the particular step or steps you had a problem with I can try to add more detail.
I think towards the end the tutorials become a hazy and a bit more like "how I did this" :D
I have created a share for you on my server (which is hosted by surpasshosting).
My server is on a fast connection and I have 10GB to play with. In the three years I've hosted there, I've not even filled 1GB.
So, if you would like to store your images, source whatever there in your own share I'd would be more than happy to donate 100 or so megabytes for your use.
Why would I do this?
Because I'm the anonymous poster up there that said I loved the tutorials. So if I can help, I'd be honored.
Please email me for more information.
dokks at ravensmyst dot com
I have created a share for you on my server (which is hosted by surpasshosting).
My server is on a fast connection and I have 10GB to play with. In the three years I've hosted there, I've not even filled 1GB.
So, if you would like to store your images, source whatever there in your own share I'd would be more than happy to donate 100 or so megabytes for your use.
Why would I do this?
Because I'm the anonymous poster up there that said I loved the tutorials. So if I can help, I'd be honored.
Please email me for more information.
dokks at ravensmyst dot com
hello,
i hope u can understand my poor english, because it's not my native language.
i like your tutorials, because it's easy to understand(unlike my english :)).
so, i followed your tutorials, and
everythings' going well until today, i opened the task manager to check about memory, as u mentioned in this part that it may run out of memory if i don't add a flag to prevent loading map repeatly.
When i closed the task manager, i found that the game window is blank!!
nothing on it. maps, sprite, everythings' disapeared.
i tried again, and it disapeared again.
it doesn't matter, if i set the game window to background and bring it to front.
also, it goes well with other applications. but not task manager.
i have no idea on this. would u tell me why it happens and, how to repair this problem?
i'm using c# express version instead of VS.NET.
my system is windows XP professional version.
i forgot the version of DX SDK.it is a 2007/4 released version.
thank you
Post a Comment