Spreadsheet viewer

This article offers you a chance to impress your boss, amazes your relatives, shows everybody that you are a serious dude after all, makes the world a better place and so on.

The code reads the data inside a text file and displays it as 3d graphs. Before I begin, let's see what's inside the data.txt file that came with this article:

data1, 100
data2, 50
data3, 35
data4, 75
data5, 58
end

Let's start with the function that reads the data:

function read_data()
{
data_handle = file_open_read("data.txt"); // open the file
while (str_cmp (temp_string, "end") == 0) // read until you have reached "end"

We open for reading the file named data.txt and we continue to read its content until we reach the end of the data ("end").

{
file_str_read(data_handle, temp_string);
str_cpy(data_name.string[i], temp_string);
if (str_cmp (temp_string, "end") == 0) // don't try to read inexistent data numbers after "end" -> this can create trouble
{
data_value[i] = file_var_read(data_handle);
if ((data_value[i] < 0) || (data_value[i] > 100)) // data_value must be in the 0..100 interval
{
error_text.visible = on; // make the error text visible
sleep (5); // for 5 seconds
exit; // and then shut down the engine
}
}
else
{
break; // get out of the loop -> don't increase "i" because it would be wrong and we need its correct value in main
}
i += 1;
}
ready = 1; // all the data was read
}

You can see that we have two columns inside data.txt: one for strings and the other one for their associated values. The strings are read in the temporary string temp_string and then their content is copied to data_name, an array of strings. The values are stored directly in the array named data_value. If the values read from data.txt are below zero or over 100 we show an error for 5 seconds and then we shut down the engine. The var named i (from index) will be 1 for the first record (data + value), 2 for the second record and so on. When we get out of the while loop, i = 5 if data.txt has 5 records, i = 2 if data.txt has 2 records and so on.

As soon as we get to the end of the file ("end") we use break to get out of the while loop and then we set ready to 1; this is a var that will be used in main. At this point we have all the strings stored in data_name and all the values stored in data_value.

function main()
{
camera.arc = 75; // set the field of view
read_data(); // read the data stored in data.txt
level_load (dummy_wmb);
while (ready == 0) {wait (1);} // wait until all the data is read
temp.x = 270;
temp.y = 100 - (5 - i) * 25;

We set the field of view to 75 degrees, we call the function that reads data.txt, we load a dummy level and then we wait until all the data is read (until ready = 1).

Let me tell you a few things about the dummy level - it is a special level. If you open it in Wed you will see that it contains a single block used as the origin for the 3d bars, but it hasn't got a hollowed box to surround it. If you "forget" to put your level inside a hollowed block or a sphere, the engine will render infinite distances (in fact, the limit is clip_range but let's not argue about that, ok?). Why would I want to do that? If something appears in the level for a few frames, it will remain there forever, even if you remove that entity, panel, text, etc. I have applied this method because I wanted to use a single panel for all the columns. If my data.txt file would have contained 50 records I would have needed to define 50 panels, right? With my trick I use a single panel to display all the values that appear over the 3d bars. I get the first value and I display it over the first bar for a few frames, then I read the next value and I move the panel above the 2nd bar for a few frames and so on.

The only drawback is that if I move the camera everything will be messed up, but why would I want to do that? Let's get back to main.

The 3d bars are in fact models; their scale_z is changed depending on their associated values, making the bars look shorter or taller. The models will be placed 270 quants in front of the player; their position on y will change depending on the number of records (data + value) like in the picture below:

You can see that no matter what number of records I use (from 1 to 5) in data.txt, the bars are aligned properly. The line that takes care of that is:

temp.y = 100 - (5 - i) * 25;

Do you remember i from function read_data? If we use 5 records in data.txt, i = 5 so temp.y = 100; for i = 4, temp.y = 75 and so on. The 3d bars will be created at the position given by temp; we've managed to align them on y but how can we align them on z? Our bar model has the origin in its center so if we would scale several bars without taking extra precautions we would get something like this:
Well... it is clear enough that if we scale an object its origin will remain at the same height; we must move the bars up or down depending on the value read from data.txt.

j = 0;
while (j < i)
{
temp.z = -100 + data_value[j];

We will use another counter named j because i holds the number of data records and we don't want to loose that value. The bar model has a height of 2 quants. If the value associated to a certain bar is 100, that bar will fill all the space on z (200 quants); for smaller values, the bars will be moved downwards with a quantity equal with data_value; this way all the bars will be aligned to the "floor".

ent_create (bar_mdl, temp, size_bar);
temp.y -= 50;
j += 1;
wait (3);
}
}

All the bars will be created at the position given by temp; the 2nd bar will have its y decreased by 50 quants and so on. We have to wait at least 3 frames because we want to display 5 different values in 5 different places using a single panel with a digit. If you have an older 3d card that uses double buffering you could wait (2) but it is safer to assume that the card uses triple buffering so wait (3) is the way to go.

Every 3d bar that is created runs this function:

function size_bar()
{
my.albedo = 100;
my.light = on;
my.lightrange = 0;
my.lightred = bar_color[k+0];
my.lightgreen = bar_color[k+1];
my.lightblue = bar_color[k+2];
k += 3;
my.scale_z = data_value[j];
vec_set(temp_value, my.pos);
vec_to_screen(temp_value, camera);
value_pan.pos_x = temp_value.x;
value_pan.pos_y = 80 + (100 - data_value[j]) * 4;
temp_value = data_value[j];
value_pan.visible = on;
}

I have set albedo to 100 because the bars must be sensitive to light; their colors are taken from an array - its definition is given below:

var bar_color[15] = 250, 0, 0, 0, 250, 0, 0, 0, 250, 250, 250, 0, 250, 250, 250; // red, green, blue, yellow, white

The first 3 values will give the color of the first bar, the next 3 colors will be used by the second bar and so on. I have decided to use red, green, blue, yellow and white; all the colors are applied to the white bar model and make it shine because I have set my.light = on.

The bars are scaled on z depending on the value stored in data_value; we want to display the figures on top of the bars so we use vec_to_screen to get the screen coordinates for every bar, then we place value_pan at its correct position, depending on data_value. Let's see how this works:

value_pan.pos_y = 80 + (100 - data_value[j]) * 4;

If the bar has a value of 100, value_pan.pos_y is set to 80; if the value is below 100, value_pan.pos_y will be bigger because the y coordinate is zero at the top of the screen. For every point below 100 we have to add 4 to value_pan.pos_y because the bar has a height of 2 quants and because every bar below 100 is moved downwards, in order to keep the alignment at the "floor" level.

Temp_value is used to display the value over the bar, using a single panel with a digit for all the values:

panel value_pan // displays the values over the bars
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 0, 0, 3, standard_font, 1, temp_value;
flags = d3d;
}

You might have noticed that I have used an array of strings - this is something that you can't read about in the manual because it isn't implemented at this point. However, we can use a text with multiple strings to create an array of strings:

text data_name // create an array of strings using a text
{
font = standard_font;
pos_x = 0;
pos_y = 0;
strings = 6;
string = " "; // 20 characters each
string = " ";
string = " ";
string = " ";
string = " ";
string = " ";
}

This text definition allows us to create an array of strings with 6 elements but we can use any value. This project can work with 1...5 data records so an array with 6 elements was more that what I needed.

I'm a lazy programmer so I try to work as little as possible; therefore, I have created a small c++ program to help myself during the development. All you have to do is to run filegen.exe with a few parameters and it will create the data.txt file for you. Here are some examples:

filegen 56 23

will create the following data.txt file:

data1, 56
data2, 23
end

filegen 99 88 77 66 55

will create the following data.txt file:

data1, 99
data2, 88
data2, 77
data2, 66
data2, 55
end

This way you don't have to edit data.txt to see how your graphs look with different record values. I have included the source code.

The project can be easily modified to accept csv (comma separated values, imported by many spreadsheet programs, including Excel). You can display the data names under (or over) the bars using a single text; if you want to display many more bars at the same time, change their scale_y depending on the number of records or make them thinner from the beginning.

Security cameras

This tiny article will show you how easy it is to implement a security camera. We will use a camera that scans for the player but before I proceed let's see a picture of scan_entity in action. This precious instruction will scan all the entities within a given sector. We can set the scanning angle horizontally and vertically; we can set the scanning distance as well:

I have created a security camera using a microscope model; don't mess with a programmer when he is looking for models :)
The action attached to the camera is security_camera:

action security_camera
{
my.skill1 = my.pan;
init_alarm();
while (1)
{
while (my.pan < my.skill1 + 60)
{
my.pan += 1 * time;
if (random(1) > 0.9)
{
start_scanning();
}
wait (1);
}
while (my.pan > my.skill1 - 60)
{
my.pan -= 1 * time;
if (random(1) > 0.9)
{
start_scanning();
}
wait (1);
}
wait (1);
}
}

We store the initial pan angle for the camera in skill1, we call the function init_alarm (more on that a little later) and then we rotate the camera 60 degrees around its starting pan angle, to the left and to the right - that's a 120 degrees scanning angle. Scan_entity is an instruction that eats resources so we shouldn't call it every frame. One of the solutions would be to replace wait (1) with waitt (3) or something like that but the movement would become choppy. There are several methods to fix this problem; the easiest method is to generate a random number and when it is bigger than a certain value (0.9 in my example) we call the function that does the scanning. You can adjust the rotation speed by changing 1 * time.

Ok, now we've got a random number every frame; when its value is over 0.9 (btw, this happens a few times a second) function start_scanning will run:

function start_scanning()
{
temp.pan = 45;
temp.tilt = 90;
temp.z = 700;
scan_entity(my.x, temp);
if ((result > 0) && (result < 500))
{
my.light = on;
my.lightred = 250;
my.lightgreen = 0;
my.lightblue = 0;
my.lightrange = 0;
wait (2);
my.light = off;
alarm = 1;
}
}

We set the horizontal scanning angle to 45 degrees, the vertical scanning angle to 90 and the scanning distance to 700 quants. Scan_entity will set result to the distance between the camera and the closest entity, if the entity is closer than 500 quants. If the camera has detected the player, it will change its color to red for 2 frames and - more important - the var named alarm will be set to 1.

I have promised to talk about function init_alarm a little later so here it is:

function init_alarm()
{
while (alarm == 0) {wait (1);}
snd_loop(alarm_wav, 100, 0);
}

This function was called at the beginning of the game and it has waited for alarm to become 1. As soon as this happens, an alarm sound is played in a loop. This is the place where you should alert the enemies, close some doors, shut down the engine... whatever you want to do.