3D Real Time Strategy Tutorial

sandpaperleadΛογισμικό & κατασκευή λογ/κού

31 Οκτ 2013 (πριν από 4 χρόνια και 8 μήνες)

91 εμφανίσεις

3D Real Time Strategy Tutorial
Hello, and welcome to the 3 dimensional Real Time Strategy (RTS) tutorial.
In this tutorial many things will be explained such as; dynamic terrain, drawing onto
the terrain basic shapes such as circles and squares, generating terrain, creating units,
using joints with a program called Move Now and finally drawing the camera. To
continue with this tutorial you must have a working version of Game Maker version 7
Professional (3d games require the professional version) and at least a year of
knowledge on how to use Game Maker (you must of course know gml).
Now, before I start with the coding aspect; the most complicated part, I will
tell you a few things about the structure of most good Real Time Strategy games. A
Real Time Strategy game is usually split into two main groups of objects. The first
group of objects is the terrain, including things like bushes and trees. The second main
group of objects are the moving things, like units (either side) or projectiles.
An RTS, when built in the 3 dimensional axes requires at least two objects to
operate, the terrain controller and the camera. In order to have a HUD (heads up
display, the things that are drawn on your screen to alert you about things like your
cash, health or status) you must have a third object. When you add units to your RTS
game you must also add an object for each different type of unit, which means, we
now have four objects.
On major aspect which often challenges RTS games is that there must be
many objects doing many things at once. In Age of Empires, a large selling RTS
game, there can be up to two-hundred units on one team. The game itself can support
up to eight teams without any lag. We, ourselves, are not aiming to create another Age
of Empires, rather, we are aiming to prove the concept that Game Maker can create
small RTS games with good Frame rate (our game may only have the ability to have
ten or twenty different objects on the screen at once).
Now that I have explained some basic concept of the layout of most RTS
games I will explain the layout of our game. This is an important part of the design
process. The game I will help you create in this tutorial will have single textured
terrain (although I will explain the theory of how you can add this feature in) and one
type of unit (which you will be able to select/de-select. The unit will automatically
animate itself when walking, attacking or even standing.
You so far, know the RTS basics of this project so I’m going to keep this short
and start explaining and building the code we are going to use. Let’s start with the
Open up Game Maker 7 and create a new project. In that project create a blank
room (call it rm_terrain) and inside the room go to the view tab. Please note that if
you cannot see the views tab you are in simple mode. To switch to advanced mode go
to the file menu and tick the menu that says Advanced Mode.
In the view tab select from the list menu view0. This will be our main view.
Tick the checkbox that says Enable the use of views and the one below it that says
Visible when room starts. If you have done this correctly view0 should have turned
Now there are two groups in the view tab. Port on screen (where it is on the
screen) and View in room (where it actually is in the room. Notice that they both have
a width value (W) and a height value (H). Change the width of both settings to 1024.
Now change the height of both settings to 768. This emulates the resolution display
without actually changing the resolution. You probably would have noticed the view
is larger than the actual room. Go to the setting tab and change the width of the room
to 1024 and the height of the room to 768.
Now you have started the basic display mode lets begin with the camera
object. Close the room (make sure you save the changes) and create a blank object.
Call the blank object obj_camera (it is important you call it this or the code we add
later will not work). Close the object and save the changes.
In this tutorial we will create a set of scripts to deal with most of our work
instead of using code directly inside the objects. This makes it neater (and can make
the file size smaller). Create a new script and call it rts3d_camera_init. Once that
script is open paste the following header into it;
<no arguments>
To make it easier to use our code we will be using headers like this one in all
our scripts. Anyway, below the header paste the following lines of code;
// initialize the 3d drawing stuff
// more to do with viewing
// pixels on the screen blend into each other (fake AA)
// set the depth of the camera to fit with the other objects
depth = 16384;
What this does is simple. The first line (after the comment) starts 3d mode by
using the d3d_start() command. Then we set the default values for our drawing (to get
things ready). For those who do not know what culling() and hidden() is I will explain
it. Culling is when you remove the unwanted triangles from the reverse side of a
shape. This sometimes can make the game faster however when activated at the
wrong times can lead to distorted shapes and unwanted polygons. Hidden is when you
remove hidden shapes which are behind something. This usually does more good than
it causes bad and can be useful when used properly.
On the next line we use the texture_set_interpolation() command. This
command decides whether to blend the pixels on textures together. Finally at the end
of the script we have a line that handles the depth of the camera. If the depth of the
camera is not set correctly ugly things can happen with the screen and wreck the
players fun with nasty error messages.
Now that we have added the first piece of the script, we must add the rest.
Here is the next snippet of code you must add to the script.
// camera direction was zero, now 270
direction = 270
wish_direction = direction;
mydirection = direction;
turn_direction = 0;
// zooming variables (min/max distance allowed to terrain)
xto = 0;
yto = 0;
zto = 0;
min_dist = 160;
max_dist = 250;
dist = max_dist;
// the z height, speed and pitch of the camera
z = dist;
zspeed = 0;
pitch = 55
friction = 0.5;
Because the camera is an object we can set local variables. Here we are
creating several variables. The first four are self explanatory. They affect the direction
of the camera (and add a smooth viewing experience for the player). In the second
block there are little miscellaneous variables. The variables xto, yto and zto are where
the camera is looking at. The variable min_dist and max_dist are to do with the
zooming where min_dist is the allowed distance from the current height of the terrain
and max_dist is the allowed maximum distance.
The variable z is the current projection Z. As you know three dimensional
worlds have x and y however unlike 2D games have a third z axes. The pitch is like
the isometric effect of the view. The friction is a variable that can be contained in any
object, whether the camera or not.
Finally, there is one more line of code to look at. This calls another script
which prepares the mouse selection for our RTS (no good RTS does not have mouse
selecting). In 3D mouse selecting gets much harder than just using events.
Paste the following chunk of code into the end of the script;
// prepare looking view
The arguments here are the same as the arguments for
d3d_set_projection_ext();. This script prepares the math for 3D mouse selection.
Make a new script (closing this one, and saving it) and call it prepare_look. In the
script prepare_look paste the following lines of code.
//Script by Yourself
//arguments 0-10 (first 11 arguments): Same as first 11 arguments of
var mm;
// Get a vector which represents the direction the camera is pointing
and normalise it
dX = argument3-argument0;
dY = argument4-argument1;
dZ = argument5-argument2;
mm = sqrt(dX*dX+dY*dY+dZ*dZ);
dX /= mm;
dY /= mm;
dZ /= mm;
// Get the up vector from the arguments and orthogonalize it with the
camera direction
// Orthogonalize is a fancy way of saying I'll make the vectors
uX = argument6;
uY = argument7;
uZ = argument8;
mm = uX*dX+uY*dY+uZ*dZ;
uX -= mm*dX;
uY -= mm*dY;
uZ -= mm*dZ;
mm = sqrt(uX*uX+uY*uY+uZ*uZ);
uX /= mm;
uY /= mm;
uZ /= mm;
// Scale the vector up by the tangent of half the view angle
tFOV = tan(argument9*pi/360);
uX *= tFOV;
uY *= tFOV;
uZ *= tFOV;
// We need one more vector which is perpendicular to both the
previous vectors
// So we use a cross product: v = u x d
vX = uY*dZ-dY*uZ;
vY = uZ*dX-dZ*uX;
vZ = uX*dY-dX*uY;
// This vector's magnitude is now the same as ||u||, so we scale it
up by the aspect ratio
// This vector represents the 2D x-axis on your screen in 3D space
vX *= argument10;
vY *= argument10;
vZ *= argument10;
This is a script borrowed off the GMC so you will have to give credit in your
final product. I am not going to explain this math because the math itself would take
half the tutorial to explain however there are added comments which you can read to
get a better understanding of it.
This script is called prepare_look() however we need to create another script.
Create a script and call it execute_look. This will be the actual execution script where
we get the variables of the mouse relative to the 3d world (up and down the terrain as
well). In this script, execute_look, paste the following lines of code. This is only the
first snippet though;
//Script by Yourself
//argument argument 0,1,2 xyz of target
var pX,pY,pZ,mm;
// get the desired point's coordinates relative to the camera_obj_var
pX = argument0-camera_obj_var.x;
pY = argument1-camera_obj_var.y;
pZ = argument2-camera_obj_var.z;
Here what happens is that we create some temporary variables (on the first
line) and on the next three lines (ignore the comments please) we get the points
relative to the camera. Notice how the camera here is referred to as camera_obj_var?
Ignore that, it will get explained later. Here is the next snippet of code that belongs in
the script;
// scale this vector so that it's head lies somewhere
// on an imaginary plane in front of the camera_obj_var (your screen)
mm = pX*camera_obj_var.dX+pY*camera_obj_var.dY+pZ*camera_obj_var.dZ;
if (mm > 0)
pX /= mm;
pY /= mm;
pZ /= mm;
else {global.__x=0;global.__y=-100;exit}
What happens here is we scale the vector on an imaginary plane in front of the
camera. The camera we are using virtually gets projected onto your screen. There is
still one more snippet to go. Let’s have a look at this snippet;
mm =
global.__x = (mm+1)/2*1024;
mm =
global.__y = (1-mm)/2*768;
What this long piece of code does is it returns the two variables as global.__x
and global.__y. It isn’t over yet. We need to use more math before we get the real
positions of the mouse. What we have at the moment is __x and __y which are
projected along the z axes. At the moment the z is 0 however when we add terrain the
z value will span up and down therefore this will not be correct anymore. Before we
continue making the scripts for the 3d mouse selecting we need to add the terrain.
Add the rts3d_camera_init script to execute in the create event of obj_camera. You
can use a Drop and Drag tile or you can execute it via code (it has no arguments, so
use closed brackets).
Create a new object. This object, is the terrain object, and should be called
obj_terrain accordingly. Make sure that the depth of obj_terrain is -102. You also
need to go back to your camera object and change the depth of the camera object to -
1000 (drawn above everything else).
This terrain object will start be initializing itself. Create a new script called
rts3d_init_ter. This will initialize most things in our game (not only the terrain).
As the header for the script use the following code. This script has four
argument0 - width
argument1 - height
argument2 - precision (no smaller than 4)
argument3 - camera object
Now you have added the header I’ll explain some of the arguments that this
script will take so you can look at them in the next few snippets. Argument0 is the
width of the terrain. Argument1 is the height of the terrain. After the height and width
have been initialized it will be very hard to change them (because it creates several
grids) so a good handy number to use (in my opinion) is about 512 for each, or for a
larger map, 1024.
Argument2 is an important variable. It is the precision of the terrain. It is
recommended that you only use numbers that are divisible by four. The numbers 4, 8,
16, 32, 64, 128 and 256 are the numbers you should use for this argument. The more
precision the faster the game will go. If you use a value of 4, for example, the game
will lag a lot because the terrain has an added vertex every four pixels.
The last argument is something you came across earlier. To make the engine
easier to use we set the variable camera_obj_var to the third argument. This means
that you can change the name of the camera object during game play (to get different
perspective etc) and it will not throw errors easily.
Now we have explained the easy snippets lets look at the rest of this code.
Because we are using GM7 most of the global variables are initialized using the
globalvar function. This means they can be called LIKE local variables however they
are actually global variables. Here is the snippet;
globalvar terrain_model, terrain_prec, terrain_height,
terrain_segment, light_color, ter_h, ter_w, terrain_imod, use_lights;
terrain_imod = 4;
terrain_prec = argument2; // change this via script arguments, the
terrain_model =
terrain_height = ds_grid_create((argument0/terrain_prec) +
1,(argument1/terrain_prec) + 1);
light_color = c_white; // change this to alter the light color
use_lights = true; // lights add nice shading to terrain
ter_w = argument0; // save terrain width (w)
ter_h = argument1; // save terrain height (h)
What we do here is simple. On the first line we declare some variables. On the
second and third lines we set them to values. When we create the grid; terrain_model;
we are making a massive data structure to store models. To save speed we use a
technique that was implemented in an editable engine called Strangeland 3D. They
split every piece of terrain into a separate model so that different parts of it could be
draw without the entire terrain being drawn. This saves speed because if you are using
a precision of 16 and a width and height of 512: (512/16 = 32) therefore 32 multiplied
by the number of vertex per square of terrain is ((32 x 4) == 128) multiplied by 16
means that the piece of terrain has 2048 triangles. If we only draw the parts that are on
the screen we save about 1500 triangles. The technique really counts when we use a
larger piece of terrain with a finer precision value.
Onto the next line. We create another grid full of values. This grid is called
terrain_height. It uses a technique similar to the DTM (Digital Terrain Model)
examples for Game Maker. Instead of calculating the positions of each point on the
terrain, or using a fancy time consuming script, we save them at creation in a grid then
we simply refer to them later (like an array).
The variables following that are self explanatory. We have variables to do
with the lights then the variables ter_w and ter_h (terrain width and terrain height).
Let me introduce you to the next snippet;
for(i=0; i <= ds_grid_width(terrain_model); i+=1)
for(n=0; n <= ds_grid_height(terrain_model); n+=1)
// sets the number of texture repeats for each ter-block
ds_grid_set(terrain_model, i, n, d3d_model_create());

// set 1 (vertex 1)
tex1[i,n]=(i*terrain_prec + 0)/128;
tex2[i,n]=(n*terrain_prec + terrain_prec)/128;

// set 2 (vertex 2)
tex3[i,n]=(i*terrain_prec + 0)/128;
tex4[i,n]=(n*terrain_prec + 0)/128;

// set 3 (vertex 3)
tex5[i,n]=(i*terrain_prec + terrain_prec)/128;
tex6[i,n]=(n*terrain_prec + terrain_prec)/128;

// set 4 (vertex 4)
tex7[i,n]=(i*terrain_prec + terrain_prec)/128;
tex8[i,n]=(n*terrain_prec + 0)/128;
What we do here is we loop through the grid adding the models to the
terrain_model grid. We also calculate the amount of times the textures should repeat
on each vertex of the 4x4 sqaure (which becomes relative to the variable terrain_imod
later). The values are stored in the variables tex1, tex2, tex3, tex4, tex5, tex6, tex7 and
The next snippet is easy. It’s just generally calling variables;
// now we have created all the models for the terrain
globalvar regen_pos, camera_obj_var;
// regen_pos is the regenration points in form [x,y]
// to prevent regenrating the entire terrain we do a little each time
regen_pos[0,0] = 0;
regen_pos[0,1] = 0;
regen_pos[1,0] = (ds_grid_width(terrain_model) + 1);
regen_pos[1,1] = (ds_grid_height(terrain_model) + 1);
// the main camera for the terrain (an object)
camera_obj_var = argument3;
// set correct depth
depth = -102;
// finally, generate the terrain
terrain_initialized = true;
globalvar tex;
tex = background_get_texture(GRASSTEXTURE);
What happens here is that we set some more global variables using the
globalvar function (these include regen_pos and camera_obj_var). Then we look at an
interesting way of regenerating the terrain using using an array.
Here we have regen_pos which we have turned into a 2 dimensional array
(contrast to a single dimensional array). There are two points stored in this entire
array. There is a x position in the first part regen_pos[0,0] and a y position in the
second part regen_pos[0,1]. The next part is similar (almost exactly the same). The
array regen_pos[1,0] is the x component and regen_pos[1,1] is the y component. An
easy way to remember each part is that the x component always comes before the y
component (even in math, tables and graphs).
In the next part we set camer_obj_var to argument3. As explained earlier
argument3 is actually provided by the user. In this case we use obj_camera as our
argument3 value.
We need to set the depth of this object to -102 so the next line handles that for
us. The next few lines do the final parts of the terrain (including setting the texture
and setting the variable terrain_initialized to true). Notice how on the last line there is
a variable GRASSTEXTURE. Replace that with your own background. You’ll have
to find a nice one on the internet. The one used in this tutorial was from Rise OF
Nations and fits with the shading of the terrain perfectly.
Notice there is another script there. It is called rts3d_regen_ter();. This is short
for regenerating the terrain. This script is very long so let’s finish our camera object
before we go back to it.
Exit the script (saving changes) and in the create event of obj_terrain make it
execute the script rts3d_init_ter. Switch back to the camera objects (obj_camera) and
then make sure everything is right (by now there should be a single event, the create
event. In the create event it should execute the script rts3d_camera_init().).
Now, create a new script called rts3d_camera_update. This script needs to be
called in the Begin Step event of obj_camera. Here is the first parts of the script
/* RTS3D_camera_obj_var_UPDATE
<no arguments>
var len_dist;
// the length we reach out into
len_dist = 7.5;
// pan left across the screen
if(mouse_x < 2 || keyboard_check(vk_left))
camera_obj_var.xto += lengthdir_x(len_dist,camera_obj_var.direction-
camera_obj_var.yto += lengthdir_y(len_dist,camera_obj_var.direction-
// pan right across the screen
if(mouse_x > (1024-2)|| keyboard_check(vk_right))
camera_obj_var.xto +=
camera_obj_var.yto +=
// pan up the screen
if(mouse_y < 2|| keyboard_check(vk_up))
camera_obj_var.xto +=
camera_obj_var.yto +=
// pan down the screen
if(mouse_y > (748-2)|| keyboard_check(vk_down))
camera_obj_var.xto += lengthdir_x(len_dist,camera_obj_var.direction);
camera_obj_var.yto += lengthdir_y(len_dist,camera_obj_var.direction);
What these lines do is move the view around (using the arrow keys or by using
the mouse against an edge of the screen like most popular RTS games). In case you
are wondering the two lines upwards actually mean OR however they are bitwise
operators. There is much more information on bitwise operators in the Game Maker
Manual (as there is anything else).
Lets look at the next larger snippet of the script;
// xto/yto calculations
xto = min(max(0,xto),ter_w);
yto = min(max(0,yto),ter_h);
// turn to meet the direction we need to view in
var notequal;
if!(mydirection == wish_direction)
// if we need to turn, turn, otherwise don't shake the view
notequal = true;
mydirection += turn_direction;

// larger
if (mydirection > wish_direction && sign(turn_direction) = 1)
mydirection = wish_direction;
turn_direction = 0;

// smaller
if (mydirection < wish_direction && sign(turn_direction) = -1)
mydirection = wish_direction;
turn_direction = 0;

// set the main direction, used later
direction = mydirection;
// zoom in (add on numpad)
dist -= 5; // subtract five from dist to terrain
dist = max(min(dist,max_dist),min_dist);
// zoom out (minus on numpad)
dist += 5; // add five to dist to terrain
dist = max(min(dist,max_dist),min_dist);
// look calculations
x = xto+lengthdir_x(dist*0.8,direction); // xto; the same as in
y = yto+lengthdir_y(dist*0.8,direction); // yto; the same as in
z = dist;
// prepare the looking (this also allows us to use mouse_selecting
All of this is heavily commented so it shouldn’t take too much to figure out
how to use. The first bit handles the xto and yto variables. It keeps them inside the
view of the map (so we don’t stray off to infinite darkness).
The next larger block of code checks the direction you WANT to go in and
then adapts you current direction to give a smooth transition. After all the code we
finally use the calculations and use the prepare_look() function we made earlier. Now,
on to the drawing of the camera.
Add this script to execute in the Begin Step event of obj_camera (if you
haven’t already) and make a new script. Don’t forget to save your game so far. The
new script should be called rts3d_camera_draw. The contents of the script are;
<no arguments>
The script uses the d3d_set_projection_ext() function to look at the world. The
script takes no arguments (like the previous camera scripts). Exit the script saving
changes. Add it to the Draw Event of obj_camera. Before you continue make sure
there are three events in obj_camera. A begin step event, the create event and the
draw event.
If this is correct you are ready to continue. Lets start with the script I
mentioned earlier. This script is called rts3d_regen_ter(). Create a new script and call
it rts3d_regen_ter. The script itself is very complicated. Before we start I’ll explain a
bit about normals and vertex. The vertex we are using be made of four points (which
amounts to two triangles at drawing time). We need to normalise the vertex so that the
light is cast upon the terrain correctly and so the drawing takes place correctly. Before
we add a snippet to the rts3d_regen_ter script create a NEW script and call it
rts3d_calculate_normals. The contents of the script are;
<no arguments>
// a
// b
// c
// component
// r
nx=cpx/r; ny=cpy/r; nz=cpz/r;
// get normals
Obviously there is lots of math involved so I won’t explain it all here. Certain
members of the GMS (such as Yourself, a member) has explained how this works.
Close the script saving changes and go back to your rts3d_regen_ter script. At the
moment that script should be blank. Add the following snippet to it.
<no arguments>
if(regen_pos[1,0] = 0 && regen_pos[1,1] = 0)
var v_w, v_h, xa, ya, za, xb, yb, zb, xc, yc, zc;
var terrain_tempmodel, nx, ny, nz;
// to prevent errors create the temporary model
terrain_tempmodel = d3d_model_create();
// w and h (width and heights)
v_w = ds_grid_width(terrain_model);
v_h = ds_grid_height(terrain_model);
v_w2 = ds_grid_width(terrain_height);
v_h2 = ds_grid_height(terrain_height);
What this actually does is it sets some variables we are going to use later. We
start by looking at the top snippet which uses the variables we made, regen_pos, to
decide whether the terrain needs regenerating or not (if we called it in the step event,
which we are not, it would not erate the terrain each step). The next few lines are just
calling variables which we will use later.
Then we create a temporary 3D model and store it in the NON GRID variable
terrain_tempmodel. We will generate each piece of the terrain into terrain_tempmodel
and then set it as the new piece. When that is completed we clear terrain_tempmodel
and everything is as good as new. The next small block of code before the end of this
snippet just sets some variables based on the width and height of the terrain. This ends
up neater and faster than calling them at the needed times.
Here is the next long snippet which you must paste after the last snippet;
// set some Z values for temporary handling
// these values get re-used later instead of wasting another
z1 = max(min(ds_grid_get(terrain_height,min(i,v_w2-
z2 = max(min(ds_grid_get(terrain_height,min(i+1,v_w2-
z3 = max(min(ds_grid_get(terrain_height,min(i+1,v_w2-
z4 = max(min(ds_grid_get(terrain_height,min(i,v_w2-
// calculating the normals for the triangles

// set the new values ready for calculation


// set our oldest model to the new one
terrain_tempmodel =
// if we have a valid point
if(i mod terrain_imod == 0 && n mod terrain_imod == 0)
// destroy old temp model
// replace it with a new one
terrain_tempmodel = d3d_model_create();
// the four Z positions for the points of the terrain
z1 = ds_grid_get(terrain_height,i,n);
z2 = ds_grid_get(terrain_height,i+1,n);
z3 = ds_grid_get(terrain_height,i+1,n+1);
z4 = ds_grid_get(terrain_height,i,n+1);
// start creating the sqaure in the terrain




// set the actual grid of terrain models to use our new shiny
temp one

What this does is it actually generates the terrain. Notice in the middle how we
actually use the rts3d_calculate_normals script we made earlier? Now we have almost
finished the bulk of the complicated stuff. All that is left to do is reset the generation
positions (so that like mentioned earlier we don’t just regenerate the entire terrain for
no reason). Here is a small snippet you must insert at the end to make sure this dosen’t
// reset so we don't have to do it all again
regen_pos[0,0] = 0;
Now that script is done, save it and close it. Before proceeding save your game
and make a backup. All that is left to do before we are finished with the camera and
terrain is create the drawing script. You should have a sigh of relief because none of
the scripts to come are as complicated as the one we just made. From here on, they are
However, you may recall that we were working on the 3d mouse selection.
Before we actually draw the terrain lets complete the selection (and add some extra
scripts to help us with other things later). We need to create a new script called
rts3d_mouse. Once you have created that script paste the following code into the
var newx, newy, newz, mX, mZ, placex, placey, a__a;
// calculations for area on screen
newz = 0;
// x,y,z calculations for the positions
mX = dX+uX*yscreen+vX*xscreen;
mY = dY+uY*yscreen+vY*xscreen;
mZ = dZ+uZ*yscreen+vZ*xscreen;
if mZ=0 then mZ=.0001 //error handling
// get final place
placex = x-z*mX/mZ
placey = y-z*mY/mZ
// then follow to calculate the position on the terrain
newx = placex;
newy = placey;
var to_test_z;
to_test_z = rts3d_ter_z(newx,newy);
if(to_test_z >= newz - 1 && to_test_z <= newz + 1)
// it works so we update
global.x_mouse = newx;
global.y_mouse = newy;
if(newz >= 100)
// it works so we update
global.x_mouse = newx;
global.y_mouse = newy;
a__a = sqrt(sqr(camera_obj_var.x - newx) + sqr(camera_obj_var.y -
newy) + sqr(camera_obj_var.z - newz));
// increase tests and hope that in our next calculation we get it
newx += 2 * ((camera_obj_var.x - newx) / a__a);
newy += 2 * ((camera_obj_var.y - newy) / a__a);
newz += 2 * ((camera_obj_var.z - newz) / a__a);
until newz>100
// once completed, we have the variables mouse_x and mouse_y
This doesn’t need too much detail because it is commented but I’ll explain the
first bit. What this does is it puts our old global.__x and global.__y variables and
pastes them onto the terrain (so the mouse variables flow up and down the terrain
correctly). The first bit is just repeating maths found in the execute_look() script
however the second large segment (with the do() statement) chacks the terrain to find
a suitable position for the scaling onto the terrain. Notice how there is a script called
rts3d_ter_z on the second line after the do()?
The rts3d_ter_z script is the second script we are going to create. It takes two
arguments and returns a number. What this script is going to do is read from the grid
(which has the positions of four points in it, each a point of a square). At the moment
unless the precision is set to one there will be slight gaps between the points so we
want there to be a smooth transition from [a,b] to [c,d]. A way we can do this is if we
read all four points then use a statement to decide whether the x component is larger
than the y component or whether it is the other way around. If the x component is
larger than the y component we use (z1 – z2) – (z2 - z3) then divide it by terrain_prec
however if the y component is larger we would use (z4 – z3) * (z1 – z4).
Please keep in mind that it is complicated and may take a while to get your
head around but if you draw it out on some paper in a small simple diagram its easier
to follow. Here is the complete code for the script rts3d_ter_z;
argument0 - X position on terrain
argument1 - Y position on terrain
var z1,z2,z3,z4,vx,vy,ox,oy;
// get the real position on the grid
// remember, the grid is divided to 16, so, this must be too
vx = floor(argument0/terrain_prec)
vy = floor(argument1/terrain_prec)
// then multiply this value by 16 with vx, vy
ox = argument0 - terrain_prec*vx
oy = argument1 - terrain_prec*vy
// the Z positions
z1 = ds_grid_get(terrain_height,vx,vy);
z2 = ds_grid_get(terrain_height,vx+1,vy);
z3 = ds_grid_get(terrain_height,vx+1,vy+1);
z4 = ds_grid_get(terrain_height,vx,vy+1);
// return a value (the z position of the chosen place on the terrain)
if ( ox > oy ) return (z1 - (ox*(z1-z2)/terrain_prec) - (oy*(z2-
else return (z1-ox*(z4-z3)/terrain_prec-oy*(z1-z4)/terrain_prec)
The two arguments are including the x and y positions of the point in which
we want to get the height of the terrain from. Please not that what I explained earlier
did not include the division by terrain_prec and that each side has to be multiplied by
either ox (x – terrain_prec*vx) or oy (y – terrain_prec*vy).
Now we are done close the script saving changes and make sure that you save
you game so far. We are almost done with the terrain. There are a few more scripts
left to create. We will go back to drawing the terrain now. Create a new script and call
it rts3d_terrain_draw. This will need to be called in the draw event of obj_terrain (go
and add it in the draw event of obj_terrain now, there are no arguments). Before we
start with the terrain drawing script we need to ask ourselves what the script needs to
Remember, our terrain is split into models. Each model is re-generated when
we call rts3d_regen_ter() and a model is split into smaller squares (multiplied by the
variable terrain_imod). The ideal way to draw the terrain is to first (at the start of the
script) gather positions of the first block of terrain in the view. Then, once we have
calculated that, we gather the positions of the last block of terrain the bottom right
hand side of the view. Once we know those positions we can simply draw all of the
co-ordinates between them.
In our draw event we also need to set up some lights. This will create the
smooth shading along the terrain (starting solid then slowly turning into a cross
hatched shadow). To do this we cannot only use a single light. If we used a single
light it would result in a sharp shadow and pitch black shading (not the ideal light for
a RTS game). We will instead, need two different lights. One light can go across the x
and y axes (to create the shading on the sides of the hills) and the other light can run
across the z axes to create the lights that run from above the hills.
Here is the complete code for the rts3d_terrain_draw script. As I said earlier it
takes no arguments;
<no arguments>
var v_pl1, v_pl1_1, v_pl2, v_pl2_1, v_w, v_h;
// this is a rough estimation of where the views are in the room
v_pl1 = ceil((camera_obj_var.xto - camera_obj_var.dist) /
(terrain_prec * terrain_imod)) - 2;
v_pl1_1 = ceil((camera_obj_var.yto - camera_obj_var.dist) /
(terrain_prec * terrain_imod)) - 2;
v_pl2 = ceil((camera_obj_var.xto + camera_obj_var.dist) /
(terrain_prec * terrain_imod)) + 2;
v_pl2_1 = ceil((camera_obj_var.yto + camera_obj_var.dist) /
(terrain_prec * terrain_imod)) + 2;
// w & h (width and heights)
v_w = ter_w / (terrain_prec * terrain_imod);
v_h = ter_h / (terrain_prec * terrain_imod);
if (use_lights)
// allow the use of lighting

// light 12, enable along x an y

// light 13, enable along z
// set the repaet of textures to true
for(i = max(v_pl1,0); i < min(v_pl2,v_w); i += 1)
for(n = max(v_pl1_1,0); n < min(v_pl2_1,v_h); n += 1)
// use a texture from the multitexturing grid
multitex_texture = tex; // to be changed later

// draw that certain part of the terrain if inside view
d3d_model_draw(ds_grid_get(terrain_model,i,n), 0, 0, 0,
// disable repeating textures
Notice how near the bottom of the script there is a line of code that sets the
variable multitex_texture to tex (tex was set in the rts3d_init_ter script). This allows
the expansion of multitexturing later (it will be explained however, multitexturing
makes the scripts very complicated and you may want to not have it so that they are
easier to understand).
Now. Before I continue and quickly tell you how to complete the terrain
sample (for running, we aren’t quite done yet) I’m just going to tell you some of the
optional things that can be added from here; units (with 3d animated models),
multitexturing, water and shape splatting.
Save your project and go back into the test room (the only room in the
project). Go onto the backgrounds tab and where it says Draw a background colour,
uncheck it. There is no need to draw the background color, as it will simply make our
complicated routines slower. Add both the objects into the room (the camera first then
the terrain object). Once you have done this save and run the project.
If it does not run make sure that you have placed the objects in the room in the
right order. If you still get errors you should go back through the scripts and make
sure that they are in the events of the proper objects and that you have actually added
them to execute in the correct events. If it still does not execute go back into the
scripts and check for broken lines (clipboard can do some weird things to text).
Well, that’s about it for the basics. To add more to your project look for these
headings. They explain what will be added next.
Water is something important that should be added in all RTS projects (unless
they are in some distorted location, like on the moon). The water itself is really easy.
We draw a semi-transparent sheet over the terrain at a certain height. When the terrain
is lowered below that height the water will smoothly appear onto the terrain (to create
smooth shorelines).
On the internet you can look for an animated cycle of water looping (little
splashes). This looks best. You can use the solid colour blue however it dosen’t look
as good.
The next part to getting the water right is to actually initialize the textures. In
the create event of obj_terrain add an execute code block and inside place the
following code.
// set water stuff
globalvar water_texture, water_height, water_tex;
water_tex = spr_water;
water_height = 0;
var wh, ww;
wh = ter_h / sprite_get_height(water_texture); // water height
ww = ter_w / sprite_get_width(water_texture); // water width
water_scaling = 3;
// create model
water_model = d3d_model_create();
ww/water_scaling, wh/water_scaling);
// loop through to save the fram animations (for animated water)
for(o = 0; o < sprite_get_number(water_tex); o += 1)
water_texture[o] = sprite_get_texture(water_tex,o);
upto = 0;
For this to work you must have replaced words spr_water with the name of
your water animation. If you are using the colour blue make sure that it is a blank
sprite that is completely filled with the correct shade of blue. The for() loop grabs the
texture of all the sub images. The upto variable simply tells us what frame we are up
to animating (we start at zero).
In the step event of obj_terrain place the following code. This will increase the
playing by one frame (and if we overlap the total amount it will replay the animation
from zero). This best works if your sprite looks good when you look it over and over
// increase animation playing frame
upto += 1;
// we've looped, start from start
if (upto > sprite_get_number(water_texture) - 1)
upto = 0;
Now, forward, onto the drawing of the water, the most confusing part. When
we draw the water the entire world is going to be reversed (because when we swap
into d3d_start() mode the [x,y] positions of the world get reversed). What we need to
do is draw the water backwards. Here is the code to draw the water. Add this AFTER
the drawing script for the terrain;
Now you should have created fully functional animated water. If the animated
water does not work (or looks ugly) try editing the true/false variables in the functions
d3d_set_culling() and d3d_set_hidden(). You can get some nice effects (and some bad
ones too).
Editing the terrain
Now that you have created the terrain your probably asking why you even
bothered if you can’t edit the terrain. Yes you can. In this part of the tutorial we add
some scripts that will let you edit the terrain (raise and lower certain parts). Let’s start
with the theory. We already have our pieces of our terrain height grid. By modifying
these we change the terrain but we must call rts3d_regen_ter() to update the models.
One of the most obvious easy scripts to make is rts3d_ter_raise_circle() which raises a
circular area on the terrain. Create the script and paste the following snippet into the
script (this is only the first part);
argument0 - x
argument1 - y
argument2 - radius
argument3 - amount
argument4 - auto_gen (whether to automatically update the terrain
var circlesze, changespeed;
// various variables to do different things
circlesze = argument2;
changespeed = argument3;
Now, already we have the common header slip. It tells us that there are 5
arguments (argument0 is actually argument1 however it is called argument0 inside
game maker). The first two arguments are the positions [x,y]. The third argument is
the radius of the circle (amount of pixels in each direction from the centre point of the
circle). The fourth argument is the amount. The amount is the number to modify the
terrain by. This can be a number like -5 or 0.5. The final argument is auto_gen. This is
whether we automatically update the terrain inside the script (by calling
rts3d_regen_ter()) or whether the user wants to call multiple commands (in which it
would be a waste of time updateing after each one when you could simply update
after every call has been done).
Here is the rest of the snippet;
// because the ds_grid_add_disk function only adds the outline we
need to make our own use of it
for(i = 0; i <= round(circlesze/terrain_prec); i += 0.5)
ds_grid_add_disk(terrain_height, round(argument0/terrain_prec),
round(argument1/terrain_prec), i,(changespeed/5) + i * 0.1);
// set the new areas that need to be re-generated (not regenrate them
at this point)
(round(circlesze/terrain_prec))) - 3;
(round(circlesze/terrain_prec))) - 3;
n_prec))) + 1;
n_prec))) + 1;

if (argument4 == true)
// if you tell us to regenerate, do it!
It starts with a for() loop which is used to raise parts of the terrain_height grid.
Because the ds_grid_add_disk() function only adds the outline we need to use the
loop to repeat the outline around the shape we want to fill. You can edit this script
easily to only do the outline by removing the for() loop.
Finally, the piece handling regen_pos is setting the array of positions to the
new update positions (based on argument0 and argument1). This is important because
when you have the option to update the terrain (in the next few lines) we don’t want
to update the entire terrain, only parts that were edited.
Drawing onto the terrain
Before we add units we need some scripts to draw those little selection circles
around their feet. In this part of the tutorial we will be making scripts to draw circles
and a flat square that finds it’s position between two terrain points.
The circle will probably be the most useful but is a fair bit more complicated
than the square. We’ll start by adding the header of the script. This will help us later
to know the arguments that the script takes. Add a script to your project and call it
rts3d_draw_circle. The header for this script is;
argument0 - X
argument1 - Y
argument2 - radius
argument3 - precision
As you can see the script will take four arguments. The first two are the x and
y positions of the circle. Because we are drawing a circle (and not an ellipsoid) the
third argument is the radius. The final argument is the precision. The precision is how
many lines to use to draw the circle. If you use a value of 8 you will get a fairly
smooth circle (however the edges may look slightly block when you draw big circles).
If you use a value of 40 or 50 (or even 100) you will get a circle which is completely
smooth but the game may start to lose frame rate (because it has to draw too many
Here is the second snippet for the rts3d_draw_circle script;
// get the amount of one fraction of the rotational of 360
var roundp, flt;
roundp = (360 / argument3);
flt = 0.3; // the amount we float above the ground
All that this snippet does is set some temporary variables. The first variables
roundp is the amount of one segment of the rotation when we draw (the length in
degrees of a side of the circle). The variable flt is the amount of pixels the circle is
floating above the ground. To edit these variables you simply cannot change the
arguments. This variable is hard coded into the script so to edit it you must change the
value in the script (this was made to make it easier to draw with fewer arguments)
Here is the remaining snippet. This handles the actual drawing of the
primitives that make up the circle. We use the rts3d_ter_z script to get the correct
height of each vertex.
// then enter a loop, no need to multiply multiples
for(i=0; i <= argument3; i+=1)
// get the positions of the vertex
var vx,vy,vz;
vx = argument0 + lengthdir_x(argument2, i * roundp);
vy = argument1 + lengthdir_y(argument2, i * roundp);

// get the height position on the terrain
vz = rts3d_ter_z(vx, vy) + flt;

// add the vertex
d3d_vertex(vx, vy, vz);
That is the final snippet. On the first line we start drawing a primitive by using
d3d_primitive_begin(). Then, once that is completed, we enter a loop that takes the
variables i. If the variable i is smaller than argument3 (the precision of the circle) we
do the calculations inside the curly brackets.
Inside the curly brackets we start by setting temporary variables (vx, vy and
vz). All of these variables refer to the position of the vertex. The first two variables
(vx and vy) are set to a position retrieved at an angle using the lengthdir_x() and
lengthdir_y() functions. The angle that makes up the argument is i (which is the side
we are up to out of the precision value) multiplied by length of one side of the circle.
We have to do this calculation otherwise if we estimated a value using a
higher precision would simply make the value overlap (creating an ugly star shaped
circle). We have completed the rts3d_draw_circle script however it would still be
useful to be able to draw squares into the terrain.
Create a new script and call it rts3d_draw_rect_flat. We are going to call it flat
because unlike the slower one (which can be drawn over multiple hills and bumps)
this one can be drawn on the face of a single terrain segment (which means as long as
you don’t do it too big it will execute faster and look as good as the slower routine).
Here are the contents for the script;
argument0 - X
argument1 - Y
argument2 - width of the rect
argument3 - height of the rect
// start the drawing
var flt;
flt = 0.2; // the amount of pixels it is hovering above the terrain
// vertex 1
d3d_vertex(argument0, argument1, rts3d_ter_z(argument0,argument1) +
// vertex 2
d3d_vertex(argument0 + argument2, argument1, rts3d_ter_z(argument0 +
argument2,argument1) + flt);
// vertex 3
d3d_vertex(argument0 +
argument2,argument1+argument3,rts3d_ter_z(argument0 + argument2,
argument1 + argument3) + flt);
// vertex 4
d3d_vertex(argument0, argument1 + argument3,
rts3d_ter_z(argument0,argument1 + argument3) + flt);
// return back to the first vertex (we are using linestrip, so we
d3d_vertex(argument0, argument1, rts3d_ter_z(argument0,argument1) +
// end the drawing
// back to false
Notice how when we do the square we don’t need a loop. A square is simple
as it only has four points (although in this case we need to draw 5 because we are
using the pr_linestrip as an argument for the primitive drawing).
This script is simple and only takes four arguments. The first two are the x and
y positions. The next two are the width and height of the rectangle. For the sake of
explaining how the second one can be done I’ll let you in on some theory. To draw an
infinite rectangle that covers up and down the terrain you would have to use a for()
loop for each side of the rectangle (this always amounts to four because a rectangle
only has four sides). You would have to start by going forwards on the first two edges
(from the top right hand corner). Then to avoid repetition you would have to draw
backwards from the remaining point.
Here is the source code for a script that draws across the terrain. You could
call it rts3d_draw_rect. Paste the following code into it;
argument0 - x
argument1 - y
argument2 - width
argument3 - height
// width of the vertex
//set width of rectangle
//set height of rectangle
for(i=0;i<=sidea;i+=1)//for the top side
var vx,vy,vz;
//add every point untill we reach the end of the rectangle
//for the right side
var vx,vy,vz;
//add every point until we reach the end of the rectangle
//for the bottom side
var vx,vy,vz;
//add every point going BACKWARDS, we go backwards to avoid extra
//for the left side
var vx,vy,vz;
//add every point going BACKWARDS, we go backwards to avoid extra
//add the start point, just to make sure we don't make any errors
The script is heavily commented so it shouldn’t take too long to understand.
Please note that I did not create THIS script. I simply added the comments. If you
want to use this script you should credit the GMC member freko.
Adding units to the RTS
This final part of the tutorial covers adding units. Before I start explaining any
of the code I will explain what movement system we are going to use. To animate the
units we will be using MoveNow. Search the GMC to find dmkito’s example on
animating characters using MoveNow. The tutorial/example can be found in the 3D
editable examples section.
Download the zip file that he has hosted (the original engine) and import the
scripts from the animation example into our 3dRTS game. Make sure that you also
copy all of the files used by the example into the same directory as you saved your 3D
rts (this includes all the .obj model files and textures).
Once you are sure that you have completed that create a new object and call it
obj_testunit. In the create event of obj_testunit execute the following snippet of code;
// setup some important variables
z = 0;
selected = false;
godir = 0; // the direction we are facing when we are walking
yoff = 0; // the y offset for this model
// goals for the movement
goalx = x;
goaly = y;
size = 7
// create a testunit model
mn_set(); // set the correct model for this unit
This starts by setting the z variable (height on the terrain). Then it saves some
variables used by the selection process. The variable godir is the direction that the
object is moving in. The variable yoff is the y offset of the model (if you load another
model with different body parts you will have to change this).
The variables goalx and goaly are the movement of the object. To move the
objects around in our RTS we will use mp_potential_step_object (where the object to
avoid is another instance of obj_testunit). The script mn_set() should have been
imported from the animation example. It sets the model that MoveNow exported to
this object (in this case the model was a small soldier wearing armour).
Make sure that you save the game before proceeding. In the Begin Step event
of obj_testunit paste the following code;
// check if we are selected
if mouse_check_button_released(mb_left)
if rts3d_mouse_region(x-size,y-size,x+size,y+size)
// the selection process
if selected = false
selected = true;
selected = false
Because this is only a tutorial we will only use click to select style selection.
This means that you cannot drag a selection box. Making a selection box is very easy
because we already have the global.x_mouse and global._ymouse variables setup. All
this snippet does here is check if we released the left mouse button (to avoid updating
the CPU intensive mouse variables every step).
Then we check if the mouse is in the correct region (a rectangle around the
object indicated by the variable size). If both are correct then we select the unit (or if
it is already selected we de-select the unit).
Here is the next piece of code for the Begin Step event of obj_testunit;
// we move towards the point if we recieve the right mouse buton
if (mouse_check_button_released(mb_right) && selected = true)
goalx = global.x_mouse
goaly = global.y_mouse
This checks if we pressed the right mouse button. If we did we update the
mouse positions and send the unit to that area (by using the goalx and goaly
variables). Finally here is the last snippet. It handles the moving and animation of the
if (point_distance(x,y,goalx,goaly) < 8)
// we are stopped, add any resting code here
if !(x = xprevious && y = yprevious)
godir = direction
What happens here is simple. If we are close to the destination (by 8 pixels)
then we play the waiting animation (which happens to be number 1). If we are not
close to our destination we play the walking animation and use
mp_potential_step_object to walk to our destination (avoiding obstacles). The final
line uses an if() statement again to determine if we are not still. If we are still using
godir = direction would result in the unit facing right all the time however we want it
to face the direction that we last moved. Therefore, if we only update the positions
when it is moving we will get that effect.
In the Draw event of obj_testunit paste the following code;
if !(rts3d_unit_check_inview()) // check if we are inside the view,
the draw/update
// we set our new z position by getting our spot on the terrain
z = rts3d_ter_z(x,y);
// set drawing prefs
// draw the circle around us if we are selected
if (selected = true)
// draw the circle around us
rts3d_draw_circle(x,y,16,16); // you can turn this into sqaur'i'sh
figure by changing the precision to 4
// draw the model
//d3d_model_draw(model,x,y,z,texture); // use this line if you want a
normal model
Here we start by checking if the unit is in the view (by using
rts3d_unit_check_inview). We will create that script later. Then we update the z
position of the unit onto the terrain (so we go up and down the hills). Finally we
actually do some drawing and set the lighting to false. If we are selected we draw the
circle around the unit using the rts3d_draw_circle command.
The final line is the mn_draw call. This uses the MoveNow animation system
to draw the unit. The first two arguments are the positions (x and y). The third
argument is the z position. The fourth argument is the direction that the model is
facing. The final argument is the scaling of the model (modify this to make it appear
bigger or smaller).
Remember the script rts3d_unit_check_inview? Create that script and paste
the following code into it;
<no arguments>
var v_pl1,v_pl1_1,v_pl2,v_pl2_1;
// get the final calculations (need camera_obj_var's position
// return true/false based on those
return false;
return true;
The code uses almost exactly the same calculations as the script that checks
the pieces of terrain inside the view. If you would like more information on the maths
used in some of these script contact Yourself (part of the GMC staff) and ask for an
explanation of some of the code snippets.
Close the code saving changes and make sure that you save everything you’ve
done in your game. You should be able to run the game without any errors.
Remember that if you want to see the unit on the terrain you must place the unit in the
room (or call a generation script in one of the controller objects).
You are now finished creating your RTS. You can share it with a friend or
continue developing. Some other cool things you could add would be enemy units and
drag selection boxes. It would also be cool to save and load maps (remember that the
positions of the terrain heightmap is saved in a data grid).
Final words
Something you must remember is that a 3D RTS in Game Maker is very CPU
intensive. On a good computer however you can get about 60 frames a second (a
speed of 30 makes a game playable, anything below being hard to play). Even without
the 3D models and terrain (and don’t forget the semi-transparent water) 2D RTS
games can be slow too.
If you want your 3D Rts to be successful you must use the most simple and
lag-free statements you can. There are a few optimizations that can be made to this
engine. Try splitting the water into segments (and only drawing what is onscreen).
That is all I have to say about this topic. I hope you have enjoyed this tutorial.
Happy 3D RTS making!