Stage 6 : A Bigger Better Map
Over the course of this chapter there are two areas we should cover:
- A map bigger than the screen.
- Advance loading and saving of the map
Some where down the line we want Actor interaction too.
The Camera
Transforming the view matrix can be thought of as twiddle with the various functions of the camera. We can do this in DirectX using the below kind of code.
Matrix QuadMatrix = new Matrix();
QuadMatrix.Translate(-0.2f,0,0f);
device.SetTransform(TransformType.View, QuadMatrix);
So we can shift the the viewpoint rather than shifting the map with all the objects on it. This seems a little bit more efficent.
We'll create a camera class ourselves - one that we can optionally attach to a GameObject. So we could have Camera.Follow(GameObject g); Sounds powerful - good! Generally we'll have the Camera follow the PC.
The Camera Class
It seems the camera must have a process stage - it must update what it is doing. There is no better place to stick this than the PlayingGameStates Process function, currently later we may push it into the metamap it all really depends.
The camera is going to be used a lot, it should really be a singleton, a nice static class that can be called from anywhere. We could even use it in other States if we really wanted to.
The PlayingGameState process function is going to be used as below:
public void Process()
{
if (device == null)
return;
device.Clear(ClearFlags.Target, System.Drawing.Color.Black, 1.0f, 0);
device.BeginScene();
Camera.Teleport(0,0.1f,0); //just too test!
Camera.Process(device);
map.Render(-1f,1f, device);
device.EndScene();
device.Present();
UpdateInput();
}
So this when we've created the Camera class should show us the scene with 0.1 taken off the bottom. But to see any of this we first need that camera class. The intial class with be quite bear bones but we'll slap on functionality as we go!
using System;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace EINFALL
{
///
/// Summary description for Camera.
/// Can we put a motion object in here?
///
public class Camera
{
static private Matrix position = Matrix.Identity;
static public void Teleport(float x, float y, float z)
{
ResetPosition();
position.Translate(x,y,z);
}
static public void ResetPosition()
{
position = Matrix.Identity;
}
static public void Process(Device device)
{
device.SetTransform(TransformType.View, position);
}
static public void Follow(GameObject g)
{
}
static public void Stop()
{
}
}
}
So the view matrix defaults to the identity matrix (which I believe we've dicussed briefly before).
The teleport function moves the camera to the position given in x,y,z co-ordinates, we could probably do we a 2D teleport as well or have that as the default as it's what we'll be using most.
Process just alters the view matrix on the device.
Let's see what this fantastic new class can do:
The follow method!
This is pretty groovy - we need to make some changes though. We're going to take the Process function of Camera out of the main loop and have the Clock take care of it.
We need to alter the process function and get rid of the Device argument. This is because it we update the camera as often as possible it produces a nasty effect with animating the character. And we don't have any Clock ability to pass variables (although it wouldn't be overly difficult to add with a few non-descript Object types). So let's get to it.
Let's start with new variables for the camera class:
public class Camera
{
static private Matrix position = Matrix.Identity;
static private bool follow = false;
static private GameObject target = null;
static private Device device;
All pretty self explanatory I feel. Follow boolean is are we following a game object - yes or no? Target is the game object that we're following. Device is the thing we need to render the viewport correctly.
Super fantastic++ now let's get rid of the device argument in the Process function, snippity snap. . . oh and why not throw in the follow code while we're there - it's pretty simple really.
static public void Process()
{
if(follow.Equals(true))
{
position.Translate(-target.hotspotX,
-target.hotspotY,0);
}
device.SetTransform(TransformType.View, position);
}
We use the hotspot because I think it works better to follow the middlish area of the player rather than the top corner of his head. If you want to define an Actor middle or do something different it shouldn't be too hard.
The code is so simple it's pretty beautiful. It's minus X,Y because ... I'm not too sure but it's the way I want it to function and it works.
We need to actually have device to refer too. So we need some kind of setup function - it could a little like this:
static public void SetDevice(Device d)
{
device = d;
Clock.AddTimeEvent(
new TimeEvent(45, new TimeEvent.Call( Camera.Process)));
}
Oh and we put a timer in there to update the cameras postion regualarly. The number is currently in sync with how often we animate the character because otherwise the animation gets a little flickery - not sure why!
Last part stip out the stuff we put in PlayingGameState.Process, then add some intialize stuff into the constructor.
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);
device.EndScene();
device.Present();
UpdateInput();
}
So the Process function is once again pretty clear.
Camera.SetDevice(device);
Camera.Follow(Player);
These two lines go at the end of the constructor.
Then notice how the player will stay in the center of the screen while the world seems to move around him!
If you still have [Enter] teleporting the player to the top corner try pressing it and watch how the camera follows him. It's pretty groovy and our gateway to larger maps.
That's the camera done for now - though there's lots more cool functionality we can add. Also the animation jitters bug me, I'd like to update the camera as often as I wished so I may look into circumventing that if possible.
Defining a map bigger than the screen
Is pretty simple - we merely increase the map size values in PlayingGameState
map = new Map(10,10);
Unfortunately we haven't been coding flexibly enough - there's a whole load of code assume the map is contained in a -1 to 1 box. This is no longer the case! So a load of functions get broken and I cry a bit.
The best ways to deal with this are to get a length and height functions that give the maps dimensions in directX measurement units.
Get rid of public PointF positionDifference
from map, we won't be needing that where we're going.
Instead we'll pop the following Getter into Map
public PointF topLeftPoint
{
get
{
return position;
}
}
It's time to pop a few new variables into the map class:
public class Map : IStorable
{
//Hard coding the tile size
static public float tileDimension = 0.25f;
public Tile[] tiles;
public ArrayList MapObjects = new ArrayList();
private int area;
private int width, height;
public float dxWidth, dxHeight;
private PointF position = new PointF(-1f,1f);
We make reference to a tile dimension so often we should really store it in a variable. I'm going to use a static, so this isn't going to be something we can dynamically change - but I'm okay with that. I don't want the tile size to change during the game ... currently. One could imagine having a battle engine where the tile size changed. For now I'm happy just to save it here.
The next bit is the width and the height of the map in DirectX coordinates from 0. So if we're rendering from -1 we need to make sure that's where we count from to get a good end of the map in Dx coords.
So let's see the intialization:
public Map(int Width, int Height)
{
width = Width;
height = Height;
area = width * height;
dxWidth = width * Map.tileDimension;
dxHeight = -(height * Map.tileDimension);
tiles = new Tile[area];
GeneratePlain();
}
Also at this point I trawled through some of the code and tried to make sure I was consistently using Width. So any varaibles using the word length I renamed. You may also wish to do this, at the very least bare it in mind in case you come across the varaibles I've changed as we proceed.
public void Render(float x, float y, Device device)
. The only change in this function is to use Map.tileDimension rather than the magic number 0.25
public int GetTileIndex(float posX, float posY)
{
float percentagePosition = (posX + position.X)/(position.X+dxWidth) - position.X;
int xRowNumber = (int)Math.Floor(percentagePosition/ Map.tileDimension);
float percentagePositionY =
Math.Abs((posY + position.Y)/(position.Y+dxHeight) - position.Y);
int yRowNumber = (int)Math.Floor(percentagePositionY/Map.tileDimension);
return (yRowNumber * width) + xRowNumber;
}
This function has been changed so that it should be able to work with any map at any position but it has not been thouroughly tested due to my love of surprise bugs!
public bool CanMove(float x, float y)
{
if(x > dxWidth ||
y + position.Y > position.Y ||
x + position.X < position.X ||
y < dxHeight)
return false; //because you'd be walking out of the window
int newTile = GetTileIndex(x,y); //this is where the magic ends!
if(newTile >= tileTotal)
return false;
if(tiles[newTile].IsBlocked())
return false;
return true;
}
The area checks here changed to make sure you can't walk out of the map area, rather than before where they just stopped you walking out of a 1x1 square with a topcorner at -1,1. So much suited to our needs. The dx coords coming in are the Actors position from 0.
That pretty much finishes the required updates to map
Now for the Actor class ...
Ok Actor.SetMapPosition, this was calling Tile to discover various things. I've killed that call off because when I was debugging it was making things rather complicated. So here's the new function:
public void SetMapPosition(int tileX, int tileY)
{
try
{
PointF p = new PointF(tileX * Map.tileDimension,
-(tileY * Map.tileDimension));
motion.Y = p.Y;
motion.X = p.X;
motion.Y -= boundary[0].Y + 0.10f;
Console.WriteLine(position);
//should take in Actor height.
}
catch(Exception e)
{
Console.WriteLine("SetMapPosition produced out of bounds: " +
e.ToString());
}
}
We get the X,Y dx coords referenced from 0 by taking the width and height and multiplying by the size of one tile. Then we set this as the new motion for the player. The +0.10f to the y is just to center the NPCs betters it's a bit of rough coding that needs to be sanded down at a later date.
The next thing to change in Actor is the GetDXPosition. It looks like so:
public override PointF GetDXPosition()
{
PointF final = new PointF(
position.X + map.topLeftPoint.X,
position.Y + map.topLeftPoint.Y);
return final;
}
There references to -1,1 as an important point in space are removed. The position of the player is reference from were ever the top corner of the map is.
One last place that's worth making changes is here:
//Tile shaped boundary
static public PointF[] CreateTileBoundary()
{
return new PointF[4] { new PointF(0f, 0f),
new PointF(0f, -Map.tileDimension),
new PointF(+Map.tileDimension, 0f),
new PointF(+Map.tileDimension, -Map.tileDimension)};
}
We use the nice new reference to the all important tile size value.
All those minor fixes and boom! We have a map larger than the screen that we can explore! The camera follows us as we look around! Yay! We can make a pretty big map with this too, so knock yourself out.
Loading and Saving
Currently we can load and save this map, all tile information will be saved. But we want to save more than that we want to save the Map objects and we want to save the tile occupants too!
Where the same object appears in different places on the map then efficent loading and saving can get a bit tricky - not too tricky we can't deal with though. For instance we could create our object Rock and then have it occupy many different tiles.
Before dealing with that though let us save the mapObjects. This should be much easier.
We create the bush by doing the following:
Actor bush = new Actor(map, "plains",
new NoMotion(s.GetOffset("Bush")),
Actor.CreateHumanBoundary());
So the map part is no problem we don't need to save that.
We do need to save the textureSet, type of motion, offset info and boundary info. So we want to save datablock that looks like the following:
[textureSet name]
[type of motion]
[motion offset - / idle state of textureSet]
[boundary type]
[X position]
[Y position]
We want the Actor to be able to write and read such a collection of data.
public class Actor : MapObject, IStorable
{
Here VS.net has put the skeleton functions in for us:
#region IStorable Members
public void Read(object o)
{
// TODO: Add Actor.Read implementation
}
public void Write(object o)
{
// TODO: Add Actor.Write implementation
}
#endregion
To know what texture set we're working with we're going to have to explicitly store the information. At least for now:
protected string ioTextureSetName;
public Actor(Map mapIn, string TexSet, IMotion m, PointF[] bounds)
: base(mapIn)
{
ioTextureSetName = TexSet;
Now we can write this information out! And read it in. Notable this writing and reading is still going to be fragile and rather weak code. Also The whole idiom probably needs a revamp. A static loader / reader is probably required to do this thing properly.
We must make change to Actor, and changes to map:
Actor needs a new simple constructor so the rest can be filled in by the reader. Yes it sucks yes we need something different at some point.
public Actor(Map mapIn) : base(mapIn)
{
}
The rest of the actor information will be filled in by the read method.
This is a good point to glance over the read record:
public void Read(object o)
{
StreamReader reader = (StreamReader) o;
ioTextureSetName = reader.ReadLine();
string motionType = reader.ReadLine();
int idle = int.Parse(reader.ReadLine());
string boundaryInfo = reader.ReadLine();
PointF[] bounds = null;
sprite = TextureManager.BufferTextureSet(ioTextureSetName);
switch(boundaryInfo)
{
case "Human Boundary":
{
bounds = Actor.CreateHumanBoundary();
}break;
case "Tile Boundary":
{
bounds = Actor.CreateTileBoundary();
}break;
default:
{
//load in all bounds information
}break;
}
boundary = bounds;
//assume it's been given the map
if(motionType.Equals("EINFALL.Motions.NoMotion"))
{
motion = new NoMotion(idle);
}
else if(motionType.Equals("EINFALL.Motions.PlayerMotion"))
{
motion = new PlayerMotion(sprite,idle);
}
else
{
Console.WriteLine("Error loading Actor");
}
motion.X = position.X = float.Parse(reader.ReadLine());
motion.Y = position.Y = float.Parse(reader.ReadLine());
}
It's pretty self explanatory and not very flexible. It's brittle code that needs working upon. But it will suit our purposes just fine.
The write code is a bit like the reverse of the read.
public void Write(object o)
{
StreamWriter writer = (StreamWriter) o;
writer.WriteLine(ioTextureSetName);
//Need to find out motion type!
writer.WriteLine(motion.ToString());
//Need to write idle - this is a hack!
motion.setState("standing");
int idle = motion.offset;
writer.WriteLine(idle);
//Need to write bounds
//This is really bad coding :D
if(boundary[0].Y.Equals(-0.41f))
{
writer.WriteLine("Human Boundary");
}
else
{
writer.WriteLine("Tile Boundary");
}
writer.WriteLine(position.X);
writer.WriteLine(position.Y);
}
The clever bit here is the motion.ToString()
bit. Even though we only have an IMotion reference and this could be any implementation if you tell it to ToString it writes out what particular implementation it is - this is good. Saves us work.
Saving the map with map objects
So Actors can now write themselves out and read themselves in. But this isn't a lot of use unless these functions get called. So to the IStorable members of the map!
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);
sw.WriteLine(MapObjects.Count);
foreach(Actor a in MapObjects)
{
a.Write(sw);
}
foreach(IStorable t in tiles)
{
t.Write(sw);
}
}
}
catch(Exception e)
{
MessageBox.Show("Error writing map file: " + e.ToString(),
"Write Error");
}
}
Writing is pretty easy but not that we're writing out Actors! We may have many different GameObjects for true functionality we should be writing out Load and Store as implemented by all GameObject types!
Reading is a little more complicated.
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;
int numMapObjs = int.Parse(sr.ReadLine());
MapObjects.Clear(); // We're loading not appending
for(int i = 0; i < numMapObjs; i++)
{
Actor a = new Actor(this);
a.Read(sr);
AddMapObject(a);
}
tiles = new Tile[area];
for(int i = 0; i < area; i++)
tiles[i] = new Tile("plains", this);
foreach(IStorable t in tiles)
{
t.Read(sr);
}
}
}
Nothing too hard. Now load the game up and press [ALT-S] and it will save the map and the map objects to c:\MapFile.map. Mine looks like below:
Plain
30
30
4
player
EINFALL.Motions.PlayerMotion
0
Human Boundary
0.25
0.31
plains
EINFALL.Motions.NoMotion
12
Human Boundary
0.5
-0.19
plains
EINFALL.Motions.NoMotion
8
Tile Boundary
1.25
-1.35
NPC
EINFALL.Motions.PlayerMotion
0
Human Boundary
1
-1.19
Grass
False
Grass
... Grass and False FOR AGES! ...
Grass
False
Reasonably sweet. Of course we've saved the player, which for general map loading saving we'd wish to avoid.
Don't alter the map file. We're going to mess with the code then we'll load our data from the map file.
First let us rather inelegantly open up a protected member of Actor. (Really the GameObject should give a uniquie id or at least id name to each object but on such luck at moment).
public string ioTextureSetName;
With that done let us change the constructor of PlayingGameState so that it appears as the below:
public PlayingGameState(Device d, GameStateManager g, DXInput.Device dInput)
{
device = d;
gameStateManager = g;
inputDevice = dInput;
map = new Map(30,30);
/**
ISprite s = TextureManager.BufferTextureSet("player");
Player = new Actor(map, "player", new PlayerMotion(s,0),
Actor.CreateHumanBoundary());
s = TextureManager.BufferTextureSet("NPC");
NPC = new Actor(map, "NPC", new PlayerMotion(s,0),
Actor.CreateHumanBoundary());
s = TextureManager.BufferTextureSet("plains");
Actor rock = new Actor(map, "plains",
new NoMotion(s.GetOffset("Rock")),
Actor.CreateTileBoundary());
Actor bush = new Actor(map, "plains",
new NoMotion(s.GetOffset("Bush")),
Actor.CreateHumanBoundary());
Player.SetMapPosition(1,0);
NPC.SetMapPosition(4,6);
rock.SetMapPosition(5,5);
bush.SetMapPosition(2,2);
map.AddMapObject(Player);
map.AddMapObject(NPC);
map.AddMapObject(rock);
map.AddMapObject(bush);
//map.tiles[22].AddOccupant(rock);
**/
map.Read(@"c:\MapFile.txt");
foreach(Actor a in MapObjects)
if(a.ioTextureSetName.Equals("player"))
Player = a;
Camera.SetDevice(device);
Camera.Follow(Player);
}
Notice the massive chunk we've been able to block out by loading from a map file! Yay for us.
Load it up - and oh my we're loading map files with Actor / GameObject data from a file! Woo we could now design cool maps and we have a format to load from. Albiet a crappy format but who cares it works!
The final count down - duh duh duh
I hate that song, so much.
Right all that's left is to save a game object associated with a tile. This is tricky because we may have the same gameobject associated with many tiles! It may also be a member of the MapObjects. All things we need to conisider to make a nice tight map format.
How do we deal with this?
First we make a list of all the unique occupiers on the map. We disregard any items on the list that are in the MapObjects list. Then we write out this list of unique occupiers. Then for each tile we reference which occupiers it has by reference to the order the occupiers where written down. A tad tricky but not out of our reach.
[normal tile stuff]
[int : number of occupiers]
[int : reference no. of 1st occupier]
...
[int : reference no. of nth occupier]
Then to load we build a list of the unique occupiers and add them to the relevant tiles using the offset number - easy now just the implemetation details :)
The map and the writing function
So to write out first we count up our uniques put them in a list. Write out how long it is followed by all the elements in it.
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);
sw.WriteLine(MapObjects.Count);
foreach(Actor a in MapObjects)
{
a.Write(sw);
}
ArrayList occupiers = new ArrayList();
foreach(Tile t in tiles)
{
if(!t.Occupants.Count.Equals(0))
{
//limiting ourselves to Actor
foreach(Actor a in t.Occupants)
{
if(!MapObjects.Contains(a))
{
if(!occupiers.Contains(a))
{
occupiers.Add(a);
}
}
}
}
}
sw.WriteLine(occupiers.Count);
foreach(Actor a in occupiers)
{
a.Write(sw);
}
It's worth noting that we're also making sure the unqiues aren't in the mapObjects - that means when we index the tile stuff we index by taking into account the concatination of both these lists.
So each tile will say - how many occupiers it has and then it will list the reference numbers.
[TileInfo]
[int : number of occupiers
[int : occupier 1]
...
[int : occupier n]
So our map files are getting pretty confusing with plenty of numbers! we could use XML or binary. XML would be nice because then the data is descriptive and easier for us to edit - but also bloated and easy for a user to edit which may be undesirable.
Grass true 1 3
So let's do the tile writing stuff - this would be nice if it was in Tile.Write but that obviously does not serve our purposes because we wouldn't have access to the list.
foreach(Tile t in tiles)
{
t.Write(sw);
sw.WriteLine(t.Occupants.Count);
foreach(Actor a in t.Occupants)
{
if(MapObjects.Contains(a))
{
sw.WriteLine(MapObjects.IndexOf(a));
}
else if(occupiers.Contains(a))
{
sw.WriteLine(occupiers.IndexOf(a)
+ MapObjects.Count);
}
else
{
Console.WriteLine("Something has gone horribly wrong in constructing"
+" our list of uniques");
}
}
}
Now we written it all out we have to read it in! I'm just going to lay this bad boy of function down. It's pretty obvious if somehat tricky to code without making errors.
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;
int numMapObjs = int.Parse(sr.ReadLine());
MapObjects.Clear(); // We're loading not appending
for(int i = 0; i < numMapObjs; i++)
{
Actor a = new Actor(this);
a.Read(sr);
AddMapObject(a);
}
int uniqueOccupiers = int.Parse(sr.ReadLine());
ArrayList uniqueOccList = new ArrayList();
for(int i = 0; i < uniqueOccupiers; i++)
{
Actor a = new Actor(this);
a.Read(sr);
uniqueOccList.Add(a);
}
tiles = new Tile[area];
for(int i = 0; i < area; i++)
tiles[i] = new Tile("plains", this);
int occCount;
foreach(Tile t in tiles)
{
t.Read(sr);
occCount = int.Parse(sr.ReadLine());
for(int i = 0; i < occCount; i++)
{
int occRef = int.Parse(sr.ReadLine());
if(occRef > MapObjects.Count)
{
occRef -= MapObjects.Count;
t.AddOccupant((GameObject)uniqueOccList[occRef]);
}
else
{
t.AddOccupant((GameObject)MapObjects[occRef]);
}
}
}
}
}
Yes it's a beast.
Thus ends this volume of the tutorial. You have your walking, bounded, animated man, with sceney and loadable / savable maps in an simple format.
What needs to be done
- Loading saving needs to be robust.
- Scripting, and extending Actors to allow it
- Inventory data type
- Items to pick up
- Combat system
- Some kind of plot
- The various inspired details that will make your RPG unique.
Out of all of these decent scripting is probably going to be the hardest. I know how to make a simple system through blood sweat and coding it myself tears. But I hear one can do it easily usin C# reflection abilities. This doesn't make sense to me as all I know about reflection is that you can use it to read attributes in the code. So there's a place to investigate.
The rest should be fairly trival to implement yourselves :D Just be stubborn and clever where possible but most of all stubborn and you'll see your game come alive. If you've read this far you should be able to take the ball and run with it - good luck, please send me anything you finished or that is cool. Please send questions and pleas for help to a patient message board. While I'd love to help you all I'm not going to! Other than release updated versions of this if necessary.
Will there be more?
Quite possibly. Though you must remember this is the code base or the start of the code base of my own fine game. That I would like to be commerically viable at some point, so I'm not going to give away all the source just yet.
2 comments:
Dude,
This was a great tutorial. I have written a game-engine for DirectDraw, and found that DX9c just messed up my LostDevice code - and NO help from ms.. rubbish!
I'm now (attempting) to rewrite my engine based on your code (sprites, windows, forms, controls, playfields - the works! (again! :( )). Thank you so much. By far the best tutorial i've seen.
One question - how does one draw straight lines? My current engine draws the borders and such rather than a bitmap.
Thanks again,
Kieron
FYI: stickleprojects@hotmail.com
Good posts. Check out LUA for scripting, it comes with c# bindings.
Post a Comment