Part II.   Creating a Camera System

 

Alright, so we have a nice big green block and we want to make an isometric game.  Time to add the camera.  Since this tutorial is targeted toward the creation of a 'Tycoon' type game, we don't want the camera to be constantly focusing on any one entity.  With our camera, the player will be able to do the following:

A pretty simple design for a camera.  Let's get to it!

 

To make the math for the camera movement a lot easier, it would be optimal if we did attach the camera to some entity, and then simply make the entity passable and invisible, so the player never knows it exists.  So let's add an entity into our level, right at the center.  It can really be any entity at all, since we plan to make it invisible.  Right-click and choose Add / Add Model and a nifty little model selector window will pop up.  I chose to use the fisch1.mdl from the work folder.  I guess that's how they spell 'fish' in German.  You know, my great great grandfather was an immigrant from Germany (but many of my ancestors are from Ireland).

 

Anyway, it's time to power up SED, because we've got some programming to do.  We need to make a 'behavior' for this little fish.  In SED, create a new file, so we have a fresh new file to work with (we will then include this new file in the main script).  I shall call my new file iso_systems.wdl and will refer to it as such.  It is in this file that we shall be programming thoughout the rest of this tutorial.

 

First, before we start creating camera functions, we need to create a pointer for the fish, so that our camera can have access to the fish's position.  So, in iso_systems.wdl we simply declare an entity pointer, create a small action that attaches the pointer to the entity, and set the entity's invisibility and passability on:

 

entity* MainObject;

 

action CameraPoint

{

    MainObject = me;

    my.invisible = on;

    my.passable = on;

}

 

By the way, if you are programming this while you are reading the tutorial, I would strongly suggest separating your variable declarations and functions, and of course add comments.  By the end of the tutorial, we'll have many functions and variables and code can start looking ugly and unreadable if you don't keep it tidy.

 

That was easy enough!  Now, go to your main wdl script, and near the top add:

 

include <iso_systems.wdl>;

 

Back in WED (you may have to reload the level) we can now attach the CameraPoint action to the fish entity.  Do so, build the level, and let's give it a test run:

 

img7.gif

Figure 2.1

 

That is just beautiful!  Okay, now let's generate the camera.  We don't need to declare the camera as a view, since it is declared by the engine by default.  So all we have to do is create a function that initializes its properties.  I shall call this function Camera_Init.  Let's take a look at it:

 

var CamZoom = 300;

var CamAngle = 0;

 

function Camera_Init()

{

        wait(10);

        

        while(1)

        {

                camera.z = CamZoom;

                camera.x = MainObject.x + CamZoom * cos(CamAngle);

                camera.y = MainObject.y + CamZoom * sin(CamAngle);

                

                vec_set(temp,MainObject.x);

                vec_sub(temp,camera.x);

                vec_to_angle(camera.pan,temp);

                

                if (CamAngle > 359)

                {

                        CamAngle = 0;           

                }

                wait(1);

        }

}

 

You should notice that we had to declare two new variables, CamZoom and CamAngle.  Remember, we said that we wanted the player to be able to zoom in and out, and rotate the camera around.  These variables simply store data that allows that to be possible.  Let's go through the instructions of the Camera_Init() function.

 

You may wonder why the wait(10) instruction is needed.  Well, when we call this function from the main script, the entire level may not yet be loaded/initialized.  This means that the entity pointer MainObject may still be empty.  Having this instruction simply gives the engine a little time to initialize the level.  Otherwise, we might get this error message:

 

img8.gif

FIGURE 2.2

 

That would be bad, so be sure to keep the wait(10) instruction there.

 

The rest of the instructions are in a while loop, and that's because we obviously want the camera to continue changing its position based on the variables.  The first three instructions calculate the x, y, and z coordinates of the camera based on CamZoom, CamAngle, and the x and y coordinated of the MainObject entity.  The middle three instructions (vec_set, vec_sub, and vec_to_angle) are used to make the camera face the entity MainObject.  These instructions were pretty much copied directly out of the 3DGS manual.  They may look a little confusing, but just remember these are the instructions that make the camera's orientation face downward at the MainObject entity.  Finally, the last little set of instructions is obviously used to reset the CamAngle back to 0 if it is 360.

 

Put the variable and the function into iso_systems.wdl.  Now all you have to do is call it from the main() function in the main script.  Hopefully, if you go the main() function of your main script, you will find the last instructions are of a while loop.  The best place to call Camera_Init (and the other stuff we'll have to call later on) is right before this while loop.  Let's give it a run (by the way, if you get that error about the empty pointer, try increasing the wait() instruction):

 

img9.gif

FIGURE 2.3

 

Nice!  But it's a bit dark.  Add some sunlight and rebuild the file:

 

img10.gif

FIGURE 2.4

 

Not bad at all.  Unfortunately, the camera can't move.  Time to program some user input.  We'll create two new functions, one that moves the MainObject entity forward, backward, and zooms in and out.  The other function will rotate the camera and move it from left to right.  Here's a list of the keys the user will use to move the camera:

These controls provide basic movement with only the use of five keys!  It's amazing!  The two functions that do this are Camera_Control() and Camera_Control2() and both contain while(1) loops so they are always at work.  But before we look at the code to these functions, we must review how the engine recognizes these keys.

 

The engine has a variable for every key on the keyboard.  For example, the variable for the up arrow would be key_cuu (cuu is short for 'cursor up').  When this variable equals 1, then the key is being pressed.  If this variable does not equal 1, then the key is not being pressed.  Here are the variables we'll need to use for our functions:

That should be easy enough.  Since there are eight possible movements, we are going need eight separate while statements, four in each function.  Take a look at the functions:

 

var ZoomSpeed = 2;

var RotateSpeed = 0.3;

var MoveSpeed = 2.5;

 

function Camera_Control()

{

        while (1)

        {

                while ((key_cuu == 1) && (key_ctrl == 1) && (key_cud != 1))

                {

                        CamZoom -= ZoomSpeed;

                        wait(1);

                }

                while ((key_cud == 1) && (key_ctrl == 1) && (key_cuu != 1))

                {

                        CamZoom += ZoomSpeed;

                        wait(1);

                }

                while ((key_cuu == 1) && (key_ctrl != 1) && (key_cud != 1))

                {

                        MainObject.y += sin(camera.pan) * MoveSpeed;

                        MainObject.x += cos(camera.pan) * MoveSpeed;

                        wait(1);

                }

                while ((key_cud == 1) && (key_ctrl != 1) && (key_cuu != 1))

                {

                        MainObject.y -= sin(camera.pan) * MoveSpeed;

                        MainObject.x -= cos(camera.pan) * MoveSpeed;

                        wait(1);

                }

                wait(1);

        }

}

 

function Camera_Control2()

{

        while(1)

        {

                while((key_cul == 1) && (key_ctrl == 1) && (key_cur != 1))

                {

                        CamAngle -= RotateSpeed;

                        wait(1);

                }

                while((key_cur ==1) && (key_ctrl == 1) && (key_cul != 1))

                {

                        CamAngle += RotateSpeed;

                        wait(1);

                }

                while((key_cur == 1) && (key_ctrl != 1) && (key_cul != 1))

                {

                        MainObject.y -= cos(camera.pan) * MoveSpeed;

                        MainObject.x += sin(camera.pan) * MoveSpeed;

                        wait(1);

                }

                while((key_cul == 1) && (key_ctrl != 1) && (key_cur != 1))

                {

                        MainObject.y += cos(camera.pan) * MoveSpeed;

                        MainObject.x -= sin(camera.pan) * MoveSpeed;

                        wait(1);

                }

                wait(1);

        }

}

 

There are three new variables that allow you to customize the speed of camera movement.  Take a look at the while statements, and notice that each one checks three keys: the ctrl key, a direction key, and the opposite direction key.  This is nice, because it means if I press up and down at the same time, they will cancel eachother out, and the camera will not move up or down.

 

We are going to make a few small changes to these functions, but for now copy them into good old iso_systems.wdl.  Instead of calling them from the main wdl script, let's call them from our last function, Camera_Init(), right after the wait(10) instruction.  This will save us a lot of trouble.  Save the file and then run it to check it out:

 

img11.gif

FIGURE 2.5

 

What a beautiful world!  By zooming out, I can see everything!  But being allowed to zoom in and out without limits may not be a good thing.  For example, you don't want the player to be able to zoom in so close he goes through the ground.  Let's add some limit testing, so that when the camera zooms in and out, there are limits.  Create two new variables called ZoomDownLimit and ZoomUpLimit (I set them to the values of 200 and 1400 in my code).  Then we just go back the first two while loops that control the zooming in Camera_Control() and add some code, so now they look like this:

 

while ((key_cuu == 1) && (key_ctrl == 1) && (key_cud != 1))

{

        CamZoom -= ZoomSpeed;

        if (CamZoom < ZoomDownLimit)

        {

                CamZoom = ZoomDownLimit;

        }

        wait(1);

}

while ((key_cud == 1) && (key_ctrl == 1) && (key_cuu != 1))

{

        CamZoom += ZoomSpeed;

        if (CamZoom > ZoomUpLimit)

        {

                CamZoom = ZoomUpLimit;

        }

        wait(1);

}

 

And now the player has some zooming limits.  What is that you say?  You want some more limits?  It would be silly to limit the rotation, but we can certainly add limits to the 'non-ctrl' movements.  In other words, we can prevent the player from going beyond the block and having to look into that eternal black abyss.  Take a look at Figure 2.6:

 

img13.gif

FIGURE 2.6

 

Figure 2.6 is a map of our big green block.  The white box is the large green block we built, and the red box is the invisible, imaginary box we want to force the player to stay inside.  We'll need four more variables to store these limits.

 

var XMax = 3000;

var XMin = -3000;

var YMax = 3000;

var YMin = -3000;

 

Now we could go ahead and write if statements as we did for limiting the CamZoom value, but that would take too long for these values.  Instead, let's just write a new function and it can be called once in each of the four while loops that control 'non-ctrl' movements (moving forward, backward, right, and left).  The function is very simple, it's just a collection of if statements, but it prevents us from having to do a lot of copying and pasting:

 

function TestLimits()

{

        if (MainObject.x > XMax)

        {

                MainObject.x = XMax;

        }

        if (MainObject.x < XMin)

        {

                MainObject.x = XMin;

        }

        if (MainObject.y > YMax)

        {

                MainObject.y = YMax;

        }

        if (MainObject.y < YMin)

        {

                MainObject.y = YMin;

        }

}

 

This function will work fine, but we only really needed two lines of code!

 

function TestLimits()

{

        MainObject.x = max(min(MainObject.x, XMax), XMin);

        MainObject.y = max(min(MainObject.y, Ymax), YMin);

}

 

A little more confusing, but more effecient.  Of course this function must appear before the Camera_Control() and the Camera_Control2() functions.  And, after calling this function in the camera control functions, the Camera_Control() and Camera_Control2() functions now look like this:

 

function Camera_Control()

{

        while (1)

        {

                while ((key_cuu == 1) && (key_ctrl == 1) && (key_cud != 1))

                {

                        CamZoom -= ZoomSpeed;

                        if (CamZoom < ZoomDownLimit)

                        {

                                CamZoom = ZoomDownLimit;

                        }

                        wait(1);

                }

                while ((key_cud == 1) && (key_ctrl == 1) && (key_cuu != 1))

                {

                        CamZoom += ZoomSpeed;

                        if (CamZoom > ZoomUpLimit)

                        {

                                CamZoom = ZoomUpLimit;

                        }

                        wait(1);

                }

                while ((key_cuu == 1) && (key_ctrl != 1) && (key_cud != 1))

                {

                        MainObject.y += sin(camera.pan) * MoveSpeed;

                        MainObject.x += cos(camera.pan) * MoveSpeed;

                        TestLimits();

                        wait(1);

                }

                while ((key_cud == 1) && (key_ctrl != 1) && (key_cuu != 1))

                {

                        MainObject.y -= sin(camera.pan) * MoveSpeed;

                        MainObject.x -= cos(camera.pan) * MoveSpeed;

                        TestLimits();

                        wait(1);

                }

                wait(1);

        }

}

 

function Camera_Control2()

{

        while(1)

        {

                while((key_cul == 1) && (key_ctrl == 1) && (key_cur != 1))

                {

                        CamAngle -= RotateSpeed;

                        wait(1);

                }

                while((key_cur ==1) && (key_ctrl == 1) && (key_cul != 1))

                {

                        CamAngle += RotateSpeed;

                        wait(1);

                }

                while((key_cur == 1) && (key_ctrl != 1) && (key_cul != 1))

                {

                        MainObject.y -= cos(camera.pan) * MoveSpeed;

                        MainObject.x += sin(camera.pan) * MoveSpeed;

                        TestLimits();

                        wait(1);

                }

                while((key_cul == 1) && (key_ctrl != 1) && (key_cur != 1))

                {

                        MainObject.y += cos(camera.pan) * MoveSpeed;

                        MainObject.x -= sin(camera.pan) * MoveSpeed;

                        TestLimits();

                        wait(1);

                }

                wait(1);

        }

}

 

Not much difference, of course, only a few new lines of code.  Now the player has to stay within bounds.

 

Well, we've programmed our camera!  Pretty simple, huh?  There really isn't much to it.  Of course, a more complex camera system could be designed, but this simplistic one should do for many purposes.  Now we have our little level, and a camera.

 

For a simple demo of the camera system we have just created, go to (from this tutorial's main directory) Demos / Part 2 - The camera / isometric.exe

 

We are on own way to creating the most important factors of an isometric game: tiles and nodes!

 

Back

Part III - Tiles and nodes