Part V.   Building Paths

 

 

In just about all isometric games, building paths or roads is an important element.  It is time for us to program a system that allows the building of paths!  Believe it or not, it's rather easy.  All we need to do is create a function that is called when the mouse is clicked, and creates the path texture on the ground.  There are different types of entities we can use for the ground, but I've chosen to use sprites.  They are small and render fast.  So let's make a sprite!  I've extracted the following texture from the standard.wad file and saved it in the directory template_6\images

 

img29.gif

FIGURE 5.1

 

I named it earthtile.bmp (from the texture's name, of course).  It looks like it will make a nice texture for our new dirt paths!  But I must resize it first, it's currently 128 by 128 pixels, and we need it to be 64 by 64 (the tile size).  Here's the resized bitmap:

 

img33.gif

FIGURE 5.2

 

Ah, that should work.  The first thing to do it is . . . you guessed it . . .

 

string earthtile = <earthtile.bmp>;

 

That will make the file easy to access in our functions we are about to write.  Well, here's is the function that needs to be called everytime the mouse is clicked:

 

function BuildPath()

{

        Mouse3D();

        ent_create(earthtile, target, null);

}

 

Remember that Mouse3D() function?  I knew it would come in handy.  That function stores the vector we need to create the entity in target.  It's pretty obvious what the ent_create instruction does: it creates earthtile.bmp at the vector target with no action (null).  And then at the bottom of our script file, we need to add this to call the function:

 

on_click = BuildPath();

 

Let's test this little function!

 

img34.gif

FIGURE 5.3

 

Aah!  That is certainly not what we wanted!  Their orientation is wrong and the sprites aren't snapped.  Should we give up?  Should we go buy DarkBASIC?  No!  The solution is simple: we assign the newly created sprites a small action that snaps them to a tile and orients them.  Here is the action:

 

action PathTile

{

        my.oriented = on;

        my.facing = off;

        my.decal = off;

        

        my.pan = 0;

        my.roll = 0;

        my.tilt = 90;

        

        my.z = 0.3;

        

        SnapIt();

}

 

It's easy to see what the action does.  Notice the my.z = 0.3 instruction.  We set the square.bmp's z at 0.5 in the Square action, so we need the path sprites to be below it, but still not too close to the ground, or else the engine may render it strangely when the camera is farther away.  Also notice how we are able to make use of the SnapIt() function once again.  Of course, this action needs to appear in the script before the BuildPath() function.  Now, in the BuildPath() function, we attach this action to the sprite when it is created:

 

ent_create(earthtile, target, PathTile);

 

Now let's see what happens:

 

img35.gif

FIGURE 5.4

 

Wow!  What a difference that action made!  These paths are just awesome!  But we've only just begun.  We need to be able to delete paths too.  But first let's address this:  one of the problems with the BuildPath() function is that it allows users to keep creating a sprite on a tile even if another sprite may already be there!  That's not good at all.  But how can we tell whether or not a tile has a path sprite on it?  This is where nodes come in.  This is also where a lot of confusing math comes in.

 

The first step is to make our blue square an entity pointer.  This can be done simply by putting the following code near the beginning of isometric.wdl:

 

entity* BlueSquare;

 

Now, in the blue square's action, we add:

 

BlueSquare = me;

my.passable = on;

 

This will allow us to have access to the blue square's x and y position coordinates, which will help with our confusing math.  Also, setting the blue square's passable flag on will help us when we start deleting paths, so go ahead and it in now.

 

Alright, it's time for the confusing math part.  We have to be able to determine whether or not a node is occupied.  Remember, we already have a node array, so all we have to do is a bit of math so that we can use the blue square's x and y coordinates to determine the index of the Nodes array.  This would be the obvious way to set an array element to a certain value:

 

temp = ((BlueSquare.x) / TileSize) * GridWidth + ((BlueSquare.y) / TileSize);

Nodes[temp] = num;

 

I must say, it looks rather elegant, but unfortunately it doesn't work.  That's because the values of BlueSquare.x and BlueSquare.y could be negative, so we will have to add a value to both of them so that they cannot be negative.  For example, if BlueSquare.x has a value of -602, we want the value to end up being 0.  So what can we add to -602 to make it 0?  Obviously we would add 602, but what code can we use to figure that out?  This code:

 

tempx = (GridLength / 2) * TileSize - (TileSize / 2);

 

Now all we have do is add this tempx value to BlueSquare.x and the job is done!  Of course, we would also want to add a tempy to BlueSquare.y.  So, the new function that allows us to set a node to any value looks like this:

 

function NodeFill(num)

{

        var tempx;

        var tempY;

        

        tempx = (GridLength / 2) * TileSize - (TileSize / 2);

        tempy = (GridWidth / 2) * TileSize - (TileSize / 2);

        

        temp = ((BlueSquare.x + tempx) / TileSize) * GridWidth + ((BlueSquare.y + tempy) / TileSize);

        

        Nodes[temp] = num;

}

 

And we'll end up calling this function every time we create a new path.  However, sometimes all we need to do is find out is whether or not a tile is currently occupied.  In that case, we can use a very similar function that returns the value of node instead of setting it to something.  Here it is:

 

function NodeCheck()

{

        var tempx;

        var tempY;

        

        tempx = (GridLength / 2) * TileSize - (TileSize / 2);

        tempy = (GridWidth / 2) * TileSize - (TileSize / 2);

        

        temp = ((BlueSquare.x + tempx) / TileSize) * GridWidth + ((BlueSquare.y + tempy) / TileSize);

        

        return(Nodes[temp]);

}

 

As you can see, it's almost the exact same code.  The math is a bit confusing, but we're almost done.  Lastly, we must edit our BuildPath() function so that it uses these new functions to check for the occupancy of a tile and to set the tile's corresponding node to a certain value if we are building a path on it.  So here we go:

 

function BuildPath()

{

        var vector1;

        var vector2;

        

        if (!NodeCheck())

        {

                Mouse3D();

                ent_create(earthtile, target, PathTile);

                NodeFill(1);

        {

        else

        {

                beep();

        }

}

 

This new version first checks to make sure that the tile's node is set to zero.  If it is, it builds the path (with the ent_create function) and then sets the tile's node to 1.  Notice I have ingeniously added a call to the beep() function so that we can make sure our code works by listening for the beep when we are trying to build a path where a path already exists.  Go ahead and try it!  You'll find that it works beautifully.  If it didn't, I wouldn't have put in the tutorial . . .

 

Anyway, the next task is to allow the user to be able to delete paths.  First, I edited the blue square to create a new square which will show up when we are deleting paths, and I named it squaredel.bmp:

 

img1.gif

FIGURE 5.5

 

This means that at the beginning of our code, we'll have to create strings that will hold the names of both squares:

 

string square1 = <square.bmp>;

string square2 = <squaredel.bmp>;

 

Also, near the beginning, we'll have to add a new variable that tells us whether or not we are in 'delete' mode (as opposed to 'build' mode).  What mode we are in will determine what square is shown (square1 if in 'build' mode, square2 if in 'delete' mode).  I call the new variable . . . BuildMode.  Declare it near the top of the script:

 

var BuildMode = 0;

 

Now we need to create a function that will allow us to toggle our build mode.  This is quite a simple step:

 

function DeleteMode()

{

        if (BuildMode == 0)

        {

                BuildMode = 1;

                ent_morph(BlueSquare, square2);

        }

        else

        {

                BuildMode = 0;

                ent_morph(BlueSquare, square1);

        }

}

 

The function calls the ent_morph function to change what square is being shown.  Lastly, near the end of the script, we need to add this:

 

on_del = DeleteMode();

 

Now whenever we press the delete key, we will be toggling the BuildMode variable.  The final thing we have to do is add some code to our BuildPath() function, so that if the user is in 'delete' mode (when BuildMode is equal to 1) then the path will be deleted (if one exists).  To delete a path, we can use the ent_remove function, but that means we'll need to have a pointer to the entity we want to remove.  In other words, we need to somehow get the path entity we want to remove into a pointer.  This isn't hard to do.  Remember how we used the trace function so find the ground earlier?  Now we can use it to find the path entity we want to remove!  That's because the trace function will return any entity it hits as the you pointer.  Also, the blue square will not be in the way, because we set its passable flag on, remember?  Ready for the beautiful code?  Here is the new BuildPath() function in its entirety:

 

function BuildPath()

{

        var vector1;

        var vector2;

        

        if (BuildMode == 0)

        {

                if (!NodeCheck())

                {

                        Mouse3D();

                        ent_create(earthtile, target, PathTile);

                        NodeFill(1);

                }

                else

                {

                        beep();

                }

        }

        else

        {

                if (NodeCheck())

                {

                        vector1.x = BlueSquare.x;

                        vector1.y = BlueSquare.y;

                        vector1.z = 100;

                        

                        vector2.x = vector1.x;

                        vector2.y = vector1.y;

                        vector2.z = 0;

                        

                        trace_mode = ignore_passable;

                        

                        trace(vector1, vector2);

                        ent_remove(you);

                        

                        NodeFill(0);

                }

                else

                {

                        beep();

                }

        }

}

 

As you can see, if the BuildMode variable is not 0, that means we want to delete paths instead.  We then use the vectors vector1 and vector2 as points for our trace, tell the trace mode to ignore passable objects (like the blue square), and we remove the target entity (the entity pointed to in you).  Lastly, we fill the tile's corresponding node back to 0.

 

In the end, we can build and delete paths with awesome power.  What fun!  By the way, if you build paths with your eyes closed, you may find your true unconscious desires . . . .

 

img2.gifimg3.gif

FIGURE 5.6

 

I placed the demo of this work in Demos / Part 5 - Paths / isometric.exe

 

Back

Part VI - Pathfinding