Part IV.   The Mouse

 

 

Before we do anything else, we must initialize the mouse so we can see it.  Here's the mouse map I've made for us to use:

 

img21.gif

FIGURE 4.1

 

I named it yellowarrow.bmp and saved it in the template_6\images directory.  Isn't it just beautiful?  Hopefully the yellow will contrast the green ground.  We better declare it:

 

bmap yellowarrow = <yellowarrow.bmp>;

 

That was stupidly easy.  Now let's make the new Mouse_Init() function!

 

function Mouse_Init()

{

        mouse_map = yellowarrow;

        mouse_mode = 1;

        while (mouse_mode > 0)

        {

                mouse_pos.x = pointer.x;

                mouse_pos.y = pointer.y;

                wait(1);

        }

}

 

We can call this function in the main script, right after we called Camera_Init().

 

In an isometric game, such as the one we plan to create, the mouse is used extensively.  It is used for building paths on the ground, clicking on entities, and pressing buttons on the panels.  Therefore, in our isometric game, we have to be able to detect which tile the mouse is hovering over.  If our game were two-dimensional, this task would be trivial because we could just use the mouse's screen coordinates, but in a three-dimensional world things get a bit more challenging.  Take a look at a screenshot:

 

img22.gif

FIGURE 4.2

 

See the problem?  The camera is facing the ground at an angle, so merely using the mouse's screen coordinates isn't going to do the trick.  What we want to do is use the mouse's screen coordinates to find where on the ground the mouse is pointing.  Fortunately, there is a solution, and it's called the vec_for_screen function.

 

The method I am going to describe is certainly not original.  It's used all over the forum, I've seen it in the AUM magazine, and in some other tutorials.  This tells me it's a pretty darn good method, or perhaps the only method.  To understand it, let's first talk about what exactly the vec_for_screen function does.  Actually, it's very simple to understand what it does.  You send it a vector, with the x and y values set to the mouse's screen coordinates, and define a z value yourself (the mouse position only has two dimensions).  The function magically turns the vector into the corresponding three-dimensional coordinates of the world.  But remember our problem?  The camera is facing the ground at an angle, so how do we know what the z value should be?

 

What we end up having to do is use the vec_for_screen function twice.  We use it once to get a 3D vector that is very close to the camera, then again to get one that is farther away.  We then use the almighy trace function to find where the ground is in between.

 

What is the trace function?  It basically sends a ray between two vectors and finds out whether or not anything is in between.  If it hits something (hopefully it will hit the ground in our game), it stores the 3D vector of its collision in a variable called target.  Pretty cool, huh?  Of course it is.  In essence, here's what we're doing:

 

img25.gif

FIGURE 4.3

 

Finally we can create our function!  Let's call it Mouse3D().  Here it is:

 

function Mouse3D()

{

        var vector1;

        var vector2;

        

        vector1.x = mouse_pos.x;

        vector1.y = mouse_pos.y;

        vector1.z = 5;

        

        vector2.x = mouse_pos.x;

        vector2.y = mouse_pos.y;

        vector2.z = 5000;

        

        vec_for_screen(vector1, camera);

        vec_for_screen(vector2, camera);

        

        trace(vector1, vector2);

}

 

Not too long at all.  After this function is called, the variable target will hold the desired vector.

 

Let's create a little test to see whether or not this function works (of course, it will work).  I've created the following image for the test:

 

img23.gif

FIGURE 4.4

 

The bitmap is 64 by 64 pixels, called square.bmp, and I saved it in the template_6\images directory.  Let's place this as a sprite in our level, and then attach this action to it:

 

action Square

{

        my.oriented = on;

        my.facing = off;

        my.decal = off;

        

        my.pan = 0;

        my.roll = 0;

        my.tilt = 90;

        

        while(1)

        {

                Mouse3D();

                vec_set(my.x, target);

                my.z = 0.5;

                wait(1);

        }

}

 

The first three instructions make sure that the sprite will only face the direction we tell it to.  The next three instructions tell it what direction to orient itself (up, of course).  The instructions in the while loop call our ingenius Mouse3D() function, sets the sprite's position to coordinates stored in the target vector, and lastly brings the sprite a little higher so that it doesn't sometimes disappear under the surface of the ground.  Here are two screenshots of the action in action:

 

img26.gifimg27.gif

FIGURE 4.5

 

Wow!  This is so good, we could package it and sell it as is!  But let's not make 3D GameStudio look bad.  There is a problem with this little feature: in an isometric game there are tiles!  Therefore, our blue little square shouldn't always appear right underneath the mouse.  We need to implement some snapping.  In other words, the blue square should appear to be highlighting the nearest tile.  Isn't it convenient that the sprite is exactly the same size as the tile size we chose?

 

Snapping is actually very easy.  All we need to do is take the sprite's my.x and my.y values and use some math to calculate the 'snapped' position.  Since our tile size is 64, we must first divide the my.x and my.y by 64, and use the int() function to drop decimal values.  Then we multiply that value by the tile size, 64.  Lastly, to get the sprite into a tile position, we add or subtract 32 (the tile size divided by 2) depending on whether my.x and my.y are positive or negative.  Hope that didn't sound too confusing.  Here's a one-dimensional example:

 

img28.gif

FIGURE 4.6

 

Of course, making the calculation two-dimensional is as simple as repeating the calculation with my.x substituted by my.y.  Remember, if the my.x or my.y value is negative, we have to subtract 32 instead.  So here is the new snapping function:

 

function SnapIt()

{

        if (my.x < 0)

        {

                my.x = TileSize * (int(my.x / TileSize)) - (TileSize / 2);

        }

        if (my.x >= 0)

        {

                my.x = TileSize * (int(my.x / TileSize)) + (TileSize / 2);

        }

        if (my.y < 0)

        {

                my.y = TileSize * (int(my.y / TileSize)) - (TileSize / 2);

        }

        if (my.y >= 0)

        {

                my.y = TileSize * (int(my.y / TileSize)) + (TileSize / 2);

        }

}

 

We must call this function in the while loop of the Square action, right before the wait(1) instruction.  And we're done with snapping!

 

Before we move on, let's add one more feature.  Remember in the last part how we only wanted the tiles to be on a 20 by 20 tile grid?  Let's add a few more if statements to the SnapIt() function so that the square will not appear outside of the 20 by 20 grid.  If the grid is 20 by 20 tiles, then that means the square can only travel 10 tiles to left or 10 tiles to the right of the y = 0 line (or the x = 0 line).  Therefore, the largest and smallest values that my.x and my.y can be are 608 (64 * 10 - 32), and -608.  More if statements are easily added:

 

function SnapIt()

{

        // x-axis snapping

        if (my.x < 0)

        {

                my.x = TileSize * (int(my.x / TileSize)) - (TileSize / 2);

                if (my.x < -((GridLength / 2) * TileSize - (TileSize / 2)))

                {

                        my.x = -((GridLength / 2) * TileSize - (TileSize / 2));

                }

        }

        if (my.x >= 0)

        {

                my.x = TileSize * (int(my.x / TileSize)) + (TileSize / 2);

                if (my.x > ((GridLength / 2) * TileSize - (TileSize / 2)))

                {

                        my.x = ((GridLength / 2) * TileSize - (TileSize / 2));

                }

        }

        

        // y-axis snapping

        if (my.y < 0)

        {

                my.y = TileSize * (int(my.y / TileSize)) - (TileSize / 2);

                if (my.y < -((GridWidth / 2) * TileSize - (TileSize / 2)))

                {

                        my.y = -((GridWidth / 2) * TileSize - (TileSize / 2));

                }

        }

        if (my.y >= 0)

        {

                my.y = TileSize * (int(my.y / TileSize)) + (TileSize / 2);

                if (my.y > ((GridWidth / 2) * TileSize - (TileSize / 2)))

                {

                        my.y = ((GridWidth / 2) * TileSize - (TileSize / 2));

                }

        }

}

 

We could optomize some of that code, but in its extended format it's easier to see what it's doing, so let's keep going.  I placed the demo of our work so far in Demos / Part 4 - The mouse / isometric.exe.  Also, notice in the demo I set the camera box limits to correlate with the box created by the 20 by 20 grid:

 

var XMax = 640;

var XMin = -640;

var YMax = 640;

var YMin = -640;

 

Now what can we do with this awesome mouse power?  Let's build some paths!

 

Back

Part V - Paths