Monday, February 28, 2005

Stage 5 - 4 : Getting A Really Good Rock





Most rocks aren't actors they're more potential actors. Should we have another class. Or should we polish the actor class until it's really general? This is the question the plagues us!




We should at least start with allowing boundaries to be adjusted! That way we can have small rocks and big monsters surely the best of both worlds.



New Project




We'll call this project really good rock. As usuakl copy over all the data from the previous project!



Basic Namespace structure




Now my projects code name is EINFALL, so that's the master namespace I'm going to house all my files in. So choose your name, if you really must you can copy mine - but if your going into Game Design the least you could think of is a name for your game.




Okay, notably Clock, TimeEvent and some other's aren't in any namespace, they should make sure every CS file has it's root namespace as EINFALL or whatever your choosing to call your game. After you do this make sure it compiles!



Sub Namespace GameStates




GameName.GameStates is the order of the day, so in here we'll put playing game state and title screen state and game state itself.




using EINFALL.GameStates;



The above must be added to the main .cs file that has the main function and to the GameStateManager.cs file too. Once again check if it compiles.



Sub NameSpace Motions




In here we'll throw NoMotion, PlayerMotion and IMotion.




using EINFALL.Motions;



Add to PlayingGameState and Actor to achieve compiliability.




In PlayingGameState I have the following includes:




using System;
using EINFALL.Motions;
using Microsoft.DirectX.Direct3D;
using DXInput = Microsoft.DirectX.DirectInput;



Generally there should be no includes that aren't being used relics from early code should be killed off.




That cleans up a bit of our code but really there needs to be a great number of well defined namespaces. I'm delaying putting some more in now because I'm unsure of the best place to put so I'll defer until later.




The real greatness of namespaces is using them as boxes to put "done" code that you can than forget about and just keep in mind what really should be worked on at the moment - currently our project desk is a bit messy



Do we need a scenery class?




Rocks currently in their and working fine, the only thing wrong with it is it's boundary. Well boundaries are going to be a potential problem from many different shaped actors, so we should make the actor constructor take in boundary info.




Note: boundaries are still a fuzzy concept they're hard programmed to have only four values yet we set it up so they can potentially have more. For now we'll keep the fuzziness but this is something we may wish to lock down as our game stares to emerge from the ether of bits.



Loading in boundaries




So in Actor we have the following variables:




public class Actor : MapObject
{
protected ISprite sprite;
protected IMotion motion;

protected PointF[] boundary;
protected PointF position = new PointF(0,0);
protected int idle = 0;



We can remove idle that information is now covered by motion. Here we're concerned with the boundary.




The boundary is intialized in the constructor as below:




public Actor(Map mapIn, string TexSet, IMotion m)
: base(mapIn)
{

sprite = TextureManager.BufferTextureSet(TexSet);

motion = m;

blocking = true;

boundary = new PointF[4] {new PointF(+0.04f, -0.41f),
new PointF(+0.04f, -0.49f),
new PointF(+0.19f, -0.41f),
new PointF(+0.19f, -0.49f)};

}



Okay so let's add a new static function that will create just such a boundary. Why not a static variable? Well we may want to dynamically change an Actors boundary so this seems wisest!




static public PointF[] CreateHumanBoundary()
{
return 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)};
}



A little wave of my magic wand and the constructor looks like this:




public Actor(Map mapIn, string TexSet, IMotion m)
: base(mapIn)
{

sprite = TextureManager.BufferTextureSet(TexSet);

motion = m;

blocking = true;

boundary = Actor.CreateHumanBoundary();
}



Okay so let's alter the constructor so we have to pass this value in:




public Actor(Map mapIn, string TexSet, IMotion m, PointF[] bounds)
: base(mapIn)
{

sprite = TextureManager.BufferTextureSet(TexSet);

motion = m;

blocking = true;

boundary = bounds;
}



Of course this sets off various kinds of problems in the PlayingGameState class. So let's fix them.




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.CreateHumanBoundary());



Okay so we're still giving the Rock the human bounds but at least now we could change it if we wish.



On needing a scenery class revisited (again) :D




So now Actor could potentially do a perfect rock, member of tile or not member of tile.




The only thing a tile based piece of scenery wouldn't need is the elaborate bounds checking. Which isn't done anyway because it occurs in Render(device) and all tile based objects are going to use Render(x,y,device).




So for the time being no particular scenery object is needed. We can just use Actor. This may change if we increase the amount of information in Actor. Notably we might add a "brain" at some point as well as "action/reaction" code.



Testing out Actor as scenry use




We'll fix up rocks boundary add it to the main map objects and see how it fares this will allow us to pin point bugs and correct on short sightedness that we've had.




First we're going to need an appropiate boundary. So in Actor we'll create a new static function like so:




static public PointF[] CreateTileBoundary()
{
return new PointF[4] { new PointF(0f, 0f),
new PointF(0f, -0.25f),
new PointF(+0.25f, -0f),
new PointF(+0.25f, -0.25f)};
}



So we shouldn't really be pulling these numbers out of the air, but for now we are, maybe when we can query the map, or Tile for the numbers we'll do that, but currently that functionality is missing.




Okay in the PlayingGameState classes constructor we do the following:




Actor rock = new Actor(map, "plains",
new NoMotion(s.GetOffset("Rock")),
Actor.CreateTileBoundary());

Player.SetMapPosition(4,4);
NPC.SetMapPosition(4,6);
rock.SetMapPosition(5,5);

map.AddMapObject(Player);
map.AddMapObject(NPC);
map.AddMapObject(rock);



So we're adding it as an MapObject so we can test if the boundaries work. And work they do - but there is a rather noticable problem:



Oh noes.


So the rock should be behind the player - why is this not happening. Well the player is being reference from the top let corner - that's his "hotspot". So he's actually behind the rock if he's references from the head, but he should be being reference from the feet.



Where is all this voodoo happening?




Well cast your mind back and you will see that we put a CompareTo into the map object class. Let's have a look at it:




public int CompareTo(Object rhs)
{
GameObject g = (GameObject)rhs;
return g.GetDXPosition().Y.CompareTo(this.GetDXPosition().Y);
}



Notice that it just get the DirectX position of the Y. We of course want to modify this so we'll stick a Getter into the MapObject class.




public abstract float hotspotY
{
get;
}



For completenesses sake we'll whack the X version in there to:




public abstract float hotspotX
{
get;

}



Now we must implement them in both Tile and Actor. The tile one is very simple its just the top corner so we make a call to getDxPosition.




public override float hotspotX
{
get
{
return GetDXPosition().X;
}
}

public override float hotspotY
{
get
{
return GetDXPosition().Y;
}
}



Okay now to Actor, this where we change things a little.





public override float hotspotX
{
get
{
return GetDXPosition().X + boundary[0].X;
}
}

public override float hotspotY
{
get
{
return GetDXPosition().Y + boundary[0].Y;
}
}



Finally to MapObject where we change the comparison function. But ... the compare function takes in GameObject. Okay no problem we'll just move the get/setters up to GameObject and everything should be be okay.



Remove the abstract hotspot functions from map object and place them into the game object.




Then alter the comparision function like so:





public int CompareTo(Object rhs)
{
GameObject g = (GameObject)rhs;
return g.hotspotY.CompareTo(this.hotspotY);
}



With that done the rock now is hidden as we desired!



Yay

Moving map objects with the map




Before I demonstrated how we could move the map to be rendered from a different X,Y position. The only draw back was that the mapObjects in the map did not move, they remained fixed. This shouldn't be too hard to solve.




Map has a render function, this is where we'll want to strike - so let's have a look at it.




public void Render(float x, float y, Device device)
{
float xLimit = x;

for(int i = 0; i < tileTotal; i++)
{
Tile t = (Tile) tiles[i];
t.Render(x,y, device);

if(((i+1) % rowLength) == 0)
{
x = xLimit;
y -= 0.25f;
}
else
{
x += 0.25f;
}
}
}



Okay so currently it's not rendering it's GameObjects, this is being done by playingGameState, so we wish to rectify this!




So we simply cut the code out of playing game state and drop it into our map render function. Omiting all the map object references. Like so:



public void Render(float x, float y, Device device)
{
float xLimit = x;

for(int i = 0; i < tileTotal; i++)
{
Tile t = (Tile) tiles[i];
t.Render(x,y, device);

if(((i+1) % rowLength) == 0)
{
x = xLimit;
y -= 0.25f;
}
else
{
x += 0.25f;
}
}

MapObjects.Sort();
foreach(MapObject m in MapObjects)
{
m.Render(device);
}

}



Okay now we want to store the base values of X and Y that are passed into this render function. This way we know where in the screen we'll be drawing the map from. Easy enough:




public void Render(float x, float y, Device device)
{
float xBase = x;
float yBase = y;


for(int i = 0; i < tileTotal; i++)
{
Tile t = (Tile) tiles[i];
t.Render(x,y, device);

if(((i+1) % rowLength) == 0)
{
x = xBase;
y -= 0




Now the place we should render is the Actor position plus the original X,Y of the map. We could do this by calling the render(x,y,device) function. If we choose to do this though we have to move the boundary code.




Also the Actors X,Y position will be incorrect, which will work no problem for collisions as everything is relevant but it will cause issues with say ... mouse intersection!




We should have a MapPosition that we set in the render loop then MapObjects can include this when they get their DX position. So we take the map positions from (-1,1) the top corner of the map and then if there's any difference we add this on when we give out the directX coordinates of our Actors and Tiles.




Ok so let's have a map postion and an accessor that gives the difference - they'll look like so:




private PointF position = new PointF(-1f,1f);

//The difference of position from top left -1,1
public PointF positionDifference
{
get
{
PointF difference = new PointF(-1 - position.X,
1 - position.Y);

return difference;




Now let's slightly edit Maps render function to update it, to this change.




public void Render(float x, float y, Device device)
{
float xBase = x;
position.X = x;
position.Y = y;


for(int i = 0; i < tileTotal; i++)
{
Tile t = (Tile) tiles[i];
t.Render(x,y, device);

if(((i+1) % rowLength) == 0)
{
x = xBase;
y -= 0.25f;
}
else
{
x += 0.25f;
}
}

MapObjects.Sort();
foreach(MapObject m in MapObjects)
{
m.Render(device);
}
}



There, beautiful.




Next change is in the GetDXPosition in Actor.





public override PointF GetDXPosition()
{

PointF final = new PointF(
position.X - map.positionDifference.X,
position.Y - map.positionDifference.Y);

return final;
}



That works perfectly try changing the PlayingGameState map rendering co-ords to the following:




map.Render(-0.5f,1.3f, device);


Woot!


Nice, notice how all the Actors are now relative to the map - makes me happy.




Also the bounds on the map prevent you walking in black black space - pretty groovy no?



Let's not forget Tile while we're here




Tile should also give correct DX position, Currently it's assuming -1,1 values so this needs to be updated!




public override System.Drawing.PointF GetDXPosition()
{

int index = map.GetTileIndex(this);
float tileLength = 2f/map.rowLength; // in directX co-ords

/*
* It's 2f because X goes from -1 to 1
*/
int ylevel = System.Math.Abs(index / map.rowLength);
int xlevel = index - (ylevel * map.rowLength);

PointF p = new PointF( -1 + (xlevel * tileLength) , //DXcoords
-ylevel * tileLength +1);

p.X = p.X - map.positionDifference.X;
p.Y = p.Y - map.positionDifference.Y;

return p;
}



That's fine but setting tile position isn't working! I don't know why I think because of the muddles of the offset coordinate system. So I'll have to think about that ...




... okay the problem is we store potential coordinates in Motion. That's a pretty easy fix the remaining problem is in the SetMapPosition, there's a magic number that centers the NPc on a tile. Also the rock isn't position correctly because NoMotion doesn't actually store any numbers we'll have to add these. So the more correct name will NoMotion-apart-from-sudden-teleportation.




Let's start with the IMotion implementation - NoMotion. I'ts now easy to find as it's under the Motions namespaces. It needs a bit of altering so let's go:




public class NoMotion : IMotion
{

int idle;
float internalX;
float internalY;




Then we need to alter the Getters / Setters.




public float X
{
get
{
return internalX;
}
set
{
internalX = value;
}
}

public float Y
{
get
{
return internalY;
}
set
{
internalY = value;
}
}



Okay now to Tile. In Tile we need to give the coords of the tile in reference to the map position so let's do the following:




public override System.Drawing.PointF GetDXPosition()
{

int index = map.GetTileIndex(this);
float tileLength = 2f/map.rowLength; // in directX co-ords

/*
* It's 2f because X goes from -1 to 1
*/
int ylevel = System.Math.Abs(index / map.rowLength);
int xlevel = index - (ylevel * map.rowLength);

PointF p = new PointF(-1 + (xlevel * tileLength) , //DXcoords
-ylevel * tileLength + 1);

p.X = p.X - map.positionDifference.X;
p.Y = p.Y - map.positionDifference.Y;

return p;
}



Now we have to alter how we change the positions in Actor. So first SetDXPosition:




public void SetDXPosition(float X, float Y)
{
motion.X = X;
motion.Y = Y;
}



Next on to SetMapPosition




public void SetMapPosition(int tileX, int tileY)
{
try
{
int tileIndex = tileX + (map.rowLength * tileY);
PointF p = map.tiles[tileIndex].GetDXPosition();
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());
}
}



That's all fixed - finally so we can now move the map around the screen and all the mapobject will tag along with it.




Rock is now fully working, it's helped prove the flexiblity of the Actor class. You could say we've managed to get a really good rock



Okay very nice but what about some bush related promises?



Shoddily crafted bush


It's a shoddily crafted bush - and jebus it's two tiles tall!.




I've stuck it in my plains .tga file. Though it's not what I'd conventionally think of when thinking of a bush on the plains but it will do!




So just to make my texture file look nice I've put the tree right over on the far end of the file. Over to the right. So in the Sprite Set file this will have to be taken into account.




Also two tiles tall - this means we don't have a preset vertex buffer to use, we either create a custom one and the code to handle this or create a new present one. Before doing all that though let's just settle for the single tile size and to check if we're actually managing to pick out the right texture.




\plains2.tga
16
Stone



We need to add another four to the vertex buffer.




Bush
tileshape
1.0
0
1.0
0.250
0.875
0.250
0.875
0



This should work for the measurements of my textue file. So let's check if it works.




Go to playing game state and let's change the offset name for the rock so that the bush texture is loaded. It creates quite a cool small bush:




Actor rock = new Actor(map, "plains",
new NoMotion(s.GetOffset("Bush")),
Actor.CreateTileBoundary());


Small shoddily crafted bush


If we want to keep the bush at this size we could just keep the TextureSet info and add a new set of info for a our full size bush, so we could have bush and dwarf bush or something.



Getting a full size bush




The bush is the same size as an NPC I only just noticed so we can use a preset to load it we just modify the TextureSet info like so:




Bush
base character shape
1.0
0
1.0
0.250
0.875
0.250
0.875
0



Run it again and you get the following:



Normal size shoddily crafted bush


Blocking is present too ... but it's blocking a tilesworth from the top of the sprite. It should be blocking a tile's worth from the bottom :( We can hack this together by using the Players blocking code, the feet are around the same area as the root's so it will work perfectly!




Let's write the code to put all the elements together. We'll do this in the PlayingGameState constructor:




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(6,5);
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);
}




We get this and all the bounds work nicely.



Where's the hero?


Oddly the tree doesn't look like it did on the .tga file. I believe this maybe because they way we've set up transparency only takes into account binary transparency rather than the graded system - I'm not entirely sure how to resolve this - I guess reading of some kind :D

No comments: