![]() |
![]() |
![]()
![]()
![]()
|
Tutorials Tutorial III - Realizing a Particle System with AGS Introduction With those things cleared up, we can finally move on to the topic: the particle system. In a realistic environment we could use those systems to represent flames, fountains or sparks, but we can also be a little more creative - it's up to you. Before we even get started with the actual creation I would like to drop some lines on how exactly this tutorial is going to work! You will (hopefully) learn two things here: how to create a particle system on the one hand of course, on the other hand I will try to explain some crucial elements of programming as well. I will show you single functions (or parts of those functions) and then I will go through every single line and try to explain as good as possible what they do and how they work. I will not be able to go into great detail and teach you the basics of programming, but I am sure that even somebody little experienced in programming generally will be able to understand the basics sooner or later - in the end it's all logic (that's what programming is all about anyways)! You will not need to copy and paste every single line from those examples (and you shouldn't, because I am not necessarily presenting them in the right order), I will give you the whole source code in one big chunk at the end. Also note that we don't plan on implementing this technique into Indiana Jones and the Fountain of Youth at this point in time. It doesn't fit in with the traditional gamestyle that we're trying to accomplish in this game. But that doesn't mean that adventure games shouldn't use special effects such as this one - I've seen a game in which you play as a magician and whenever you used your magic wand it would create sparkles. Those were managed with such a particle system. Getting started
When programming, it's essential that you have a very good understanding of what you want to achieve. If we put it the other way: it's impossible to code something if you don't know what you want. Knowing that, I'll give you a very short overview of how this "New Year's Simulator" is going to work practically: there will be a crosshair that you can move with your mouse. You will be able to left-click to emit a bunch of stars (which will look like a rocket exploding). To make things a little more interesting, you will also be able to right-click to change to the "repeating mode". In this mode, you will no longer be able to emit stars by left-clicking - the stars will be automatically emitted in smaller amounts (this will look more like sparkles coming from the cursor). Right-clicking again will of course switch to the first, "standard" mode again so that you can pretty much switch the "repeating mode" on and off by right-clicking. Also there will be a message on the screen to inform you about your controls (it will say something like: "Left-click to emit! Right-click to change mode!"). Now that we have an idea of what will be going on, it's time to start up AGS: select "Start a new game" and give it a title (I chose "Stars", but you can feel free to use whatever you want to). I'd recommend to use the "Empty Game Template". Create a room (we will only need one) and use whatever background image you think fits - you could for example draw a quick skyline with a nice evening sky or something completely different, once again it's entirely up to you. I'll go with a simple black background for the sake of simplicity. Also make sure to save the room in your game folder (not the compiled folder) with a name like "room1.crm" please. Good, now we need two more graphical components, before we can start with the actual programming part. The first component is an image of a star. Basically our "particles" will look like this. But if we put things in relation, it will eventually strike our minds that it must be really bad having up to one-thousand stars on the screen at the same time and letting them all look the same! Thus we should decide to draw two or three pictures with little stars, but drawing them all a little different (choose a different color, change their sizes or give them a different shape). The second image we need is a simple crosshair - after all we need something that we can aim with just like in every good game.
Programmers call images like those above "programmer's art". There's this idea all around that programmers can't draw and artists can't program. I don't think very much of that (although it definately is true in my case)! As you can see, I used pink to fill in those spaces, that are later going to be transparent (don't worry, AGS will do this automatically for us). I made all three star images 9x9 pixels big (but you can make them as big as you like to - you can even use different sizes for them) and I chose a size of 13x13 pixels for the crosshair. Now let's go back into AGS and import our images. To make this as easy as possible, I recommend to first select and copy your first star image with your paint program and then right-click in the sprite manager and choose "Paste new from clipboard (320-res)". This will import your selection and make it a new sprite. Repeat this for all the star images and for the crosshair. If this way of importing sprites somehow fails to work for you, then just do it the old way: save all images on your harddrive somewhere, then right-click and choose "Import new sprite..." and select your images. So now that we have all four images in AGS, we can finally begin with working on the really interesting stuff - the programming! Some definitions first function draw_healthbar ( int x, int y, int amount ) // draws the healthbar somewhere with a given amount{ if ( amount > 0 ) { RawDrawRectangle ( x, y, x + y, x - y ); } } function DrawHealthBar(int X,int Y,int Amount){ // Draws The Health Bar Somewhere With A Given Amount { if (Amount>0){ RawDrawRectangle(X,Y,X+Y,X-Y); } } Those are not the only two possibilities of course. As I said before, there are as many styles out there as there are programmers at least. I don't want to force you into a decision, however I will of course use my own style for this tutorial - if you feel like it, you can always replace all my function names, variable names etc. with your style! So what I like to do, is to "convert" all that mess in our global script with the same mess - but in my style. Don't get me wrong, I will not change a thing - it will all work the same as before, I will only change how things look. You might wanna copy the following code and overwrite everything that is currently in your global script with this:
#sectionstart game_start function game_start ( ) // is called on game start and before the first room loads { } #sectionend game_start
#sectionstart repeatedly_execute function repeatedly_execute ( ) // is called every frame { } #sectionend repeatedly_execute
#sectionstart on_key_press function on_key_press ( int keycode ) { if ( keycode == 27 ) // if "ESC" was pressed // quit the game QuitGame ( 0 ); else if ( keycode == 434 ) // if "F12" was pressed // save a screenshot SaveScreenShot ( "Screenshot.bmp" ); } #sectionend on_key_press
#sectionstart on_mouse_click function on_mouse_click ( MouseButton button ) { } #sectionend on_mouse_click So let's get to the promised definitions. You need to put those
at the beginning of your global script - above everything else that we
pasted in before. #define MATH_PI 3.14159265358979323846 // little mathematical helper #define PARTICLES_MAX 1000 // maximum number of particles #define PARTICLES_NUM_CLICK 50 // how many particles will be shown when mouse is clicked Next we define PARTICLES_MAX to be 1000. The comment after that line explains it all rather well. This is going to be the absolute maximum number of particles that we are going to have on the screen at one time. If we have too many (over 1000), then we will just not add any new ones. PARTICLES_NUM_CLICK is simply the definition of "a bunch". Remember when I told you that left-clicking will make "a bunch" of stars appear? This is exactly how many we will create when clicking. The reason why we use those definitions is simple: Imagine you wouldn't use them for a second. Later in the script, you'd use all those numbers directly. It would work the same, but once you want to tweak some values here and there, it's gonna be a pain in the butt to change everything around. It's much easier like this. Say you want to spawn more stars when the user performs a left-click: simply define PARTICLES_NUM_CLICK as 200 instead of 50 - it's that easy. Imagine you would need to go through all the lines of your script later on and replace this value everywhere, depending on the size of your code this could take years! One more thing we need is a global variable. We want to keep track whether the user is currently in "repeated mode" or not. bool rep_mode; // keep track of whether we are in "repeated mode" or not Coming up with a structure and the concept of an array Let's work with an example again. Imagine that you would want to code an enemy into one of your other games. This enemy will have different variables, that all have to do something (or you could say: they all hold some kind of information about the enemy). The list of variables could look like this: // definitions string enemy_name; // name of the enemy int enemy_health; // health of the enemy bool enemy_alive; // is the enemy alive or not? // let's change his health enemy_health = 32; struct enemy { string name; // name of the enemy int health; // health of the enemy bool alive; // is the enemy alive or not? };
enemy our_enemy; // now let's change his health our_enemy.health = 32; // definitions string enemy1_name; // name of the enemy 1 int enemy1_health; // health of the enemy 1 bool enemy1_alive; // is the enemy 1 alive or not?
string enemy2_name; // name of the enemy 2 int enemy2_health; // health of the enemy 2 bool enemy2_alive; // is the enemy 2 alive or not?
// skipping...
string enemy1000_name; // name of the enemy 1000 int enemy1000_health; // health of the enemy 1000 bool enemy1000_alive; // is the enemy 1000 alive or not? struct enemy { string name; // name of the enemy int health; // health of the enemy bool alive; // is the enemy alive or not? };
enemy our_enemy; struct enemy { string name; // name of the enemy int health; // health of the enemy bool alive; // is the enemy alive or not? };
enemy our_enemy1; enemy our_enemy2;
// skipping again...
enemy our_enemy1000; struct enemy { string name; // name of the enemy int health; // health of the enemy bool alive; // is the enemy alive or not? };
// define an array of enemies enemy our_enemy[1000]; // change health value of enemy number 742 to 32 our_enemy[742].health = 32; Allright, so far, so good. Now I will show you the way I designed the structure for each one of our stars. I didn't think of this all rightaway, usually when designing a structure you start with the obvious and the rest comes by itself. For example here, the first thing that came to my mind was: what does every single star need? A position probably. So I added two integer-variables for the position on the x- and the y-axis respectively. By asking yourself these questions, you can also determine just how complex you want your system to turn out later on. If you want all the stars to have a unique percentage of transparency (so that some stars would look almost transparent, while others might look almost non-transparent) to make your particles look even smoother and better, you should add a transparency-property to the star-structure, just as I did, for example. struct particle { float x; // the position on the x-axis ( in pixel ) float y; // the position on the y-axis ( in pixel ) float velocity_x; // the velocity on the x-axis ( in pixel ) float velocity_y; // the velocity on the y-axis ( in pixel ) float speed; // the speed of the particle ( in pixel ) float angle; // the angle of the particle ( in radians ) int time; // the number of frames it has been living ( in frames ) int sprite_slot; // the sprite slot of the particle sprite float gravity; // the amount of gravity that affects the particle int transparency; // the amount of transparency for the particle ( in percent ) };
// define our stars through an array particle star[PARTICLES_MAX]; The first two properties are the position on the screen for both axis. They are not defined as integers, but as floating values, which means our particles can have a position of 45.327 on the x-axis and 93.033 on the y-axis. Of course, when drawing our particles we will need to convert those numbers to integer values, because there is no pixel 45.327 on the screen. This would then get rounded down to 45 and be drawn there. But let's worry about that later (when we're talking about the drawing function), right now it's just important that we pretend there would be a pixel 45.327 and do our calculations later on using floating values. The next two entries called "velocity_x" and "velocity_y" stand for the velocity of our particle. Since our particles will be emitted or launched into a set direction with a set speed, we will need to keep track of how fast our pixel is currently moving and in what direction. Similarly, the following two properties mean pretty much the same thing, however this time we save the direction and speed of the particle in a different way (which is again useful for our later calculations) using one variable to keep track of the current speed and one of the current angle - usually this way of saving those two pieces of information is regarded as more intuitive. One thing to notice is that the angle is not given in degrees as one might expect, but in radians. Yet again this only matters for the calculations later on. Finally it starts to get a little less heavy on the maths - the next line defines a simple variable called "time", which counts the number of frames that the particle has already been living. When spawning and launching or shooting off our particle, this variable will be set to the logical, inertial value of zero. Then this value will be increased every frame. More information on all this later on, of course, when we are actually including all this functionality. And it continues as easy and simple to understand as the last line was - this time the variable is called "sprite_slot" and keeps track of the so-called sprite-slot that AGS gave our three star-sprites when we imported them. We need this because we drew three different looking sprites. We will set this to the according value (or sprite-slot number) after spawning the article. Why exactly we added this property may not be clear to you right now, but trust me, it is going to be useful later on, you don't need to understand the purpose of the variable at this point in time. Now it starts to get interesting again - with "gravity". I am sure you all know what it is. You don't need to have this in a particle system, but it's definately a nice addition and has an interesting effect, plus it gives you the possibility to simulate particle systems not only in conditions similarly to the earth, but also on other planets, underwater etc. Now who doesn't need a scene with real-time particles on the moon in his adventure game? This variable is going to hold the amount of gravity that affects and pushes the particle. It could be 0.7, which would mean that the particle is being pushed down every frame by almost one pixel, it could also be -3.7, which would push the particle up quite quickly as if it were underwater and filled with oxygen or something. Of course, this value is only realistic if we increase it over time - take this as an example: if you pick up an object in your room and drop it on the floor, it will start falling down slowly for a few milliseconds and then accelerate pretty fast. I will talk about this again and explain how exactly we are handling this physical property once it's time for us to actually do stuff with the variable. Last, but not least, there is the variable "transparency" left, which I talked about earlier when giving you an example of a property. I already gave you information on how this will work, basically every particle is going get a random transparency-value and will thus be drawn with a specific amount of transparency. Of course, we will not change this value while the particle lives (this would cause the particle to flicker, because every new frame - 40 times a second, the amount of transparency for every particle would be changed), we will set this value once when we spawn the particle and then leave it at that, so that this particle sticks to its amount of transparency for its whole life. Because we will draw lots and lots of particles, some will appear to be further behind and others closer to the camera, just because the transparency makes our eyes believe that we see something in three dimensions when there are actually only two. One more thought on dimensions: we could also add some kind of scaling variable, that makes particles bigger or smaller. This would give it a real three-dimensional appearance, but that is too much for this article. If you understood the concepts here, it would be the perfect task to do and try on your own afterwards! That closes the description of our particle-structure. One last thing left to mention is the last line in the code I gave you. Right after the definition of our structure, I defined our stars as an array. The array is called "star", it is of the type "particle" (obviously, because that's how we named our structure) and it contains not 1000 (as in our example before), but PARTICLES_MAX stars, which we defined as 1000 (I can literally hear you screaming out: hey, that's the same, isn't it? Well, as I said before, by using PARTICLES_MAX instead of a real number like 1000, we can change this number easily later on). Additionally, you can see another reason for why we named our structure not star but particle earlier - otherwise that name would've already been taken and we would need to call our objects sparks or something else. So, basically we now have the possibility to access our stars just like in those examples I gave you earlier when talking about arrays: // change transparency value of star number 325 to 83 star[325].health = 83; What do we have so far? Some definitions of values (MAX_PARTICLES for example), a definition for our particle-structure and a definition for our star-array. What is missing? You could say the counterpart to definitions: implementations. Before, we only added elements together and made big plans - in our head. For example when I told you we'd need to increase the gravity variable over time so it looks more real. This doesn't happen by itself, we still need to implement it. In order to do this, we need three functions that all have their unique purpose. The first function is called "create_star ( ... )". As the name implies, it's there to create (spawn, launch, give birth or whatever you want to call it) a new star. Without further complications, here it is: function create_star ( float x, float y ) // creates a particle at a given location { // define a variable for the id-number of the star, that we want to create int star_id; // use a backward while loop to determine which star should be created // we should then come up with the star with the smallest 'star_id' value // but which still has its framecounting time variable set to 0 int i = PARTICLES_MAX - 1; while ( i > 0 ) { if ( star[i].time == 0 ) // if the framecounting time variable equals 0 { // then we will mark this star star_id = i; } // continue the loop i--; } // now that we have the ID value for the star we want to create, we now have to // set some initial values / settings for it // set its x position to the x value this function gives us by parameter star[star_id].x = x; // set its y position to the y value this function gives us by parameter star[star_id].y = y; // set its sprite slot to random ( between 1000 and 1002 ) star[star_id].sprite_slot = Random ( 2 ) + 1000; // set its gravity value to 0 star[star_id].gravity = 0.0; // set its transparency value to something between 50 and 100 star[star_id].transparency = Random ( 50 ) + 50; // and set its framecounting time variable to 1 star[star_id].time = 1; // now choose a speed from 0 to 99 star[star_id].speed = IntToFloat ( Random ( 100 ) + 50 ); // and choose an angle from -1 to 1 ( in radians ) star[star_id].angle = IntToFloat ( Random ( 100 ) * ( FloatToInt ( 2.0 * MATH_PI ) / 2 ) ); // then set its velocity on the x-axis to its correct value star[star_id].velocity_x = Maths.Cos ( star[star_id].angle ) * star[star_id].speed; // and then set its velocity on the y-axis to its correct value star[star_id].velocity_y = Maths.Sin ( star[star_id].angle ) * star[star_id].speed; } // spawn one star where the mouse is create_star ( mouse.x, mouse.y ); Another thing that we should think about before rushing into the function is the problem of randomness. For something as logical as a computer, nothing is random. Your computer can't give you a real random number. Nobody can tell your computer to give you a random number, while everybody can tell your computer to give you one specific number or all numbers from zero to ten or all prime numbers in a certain range - but there is no way how a computer could just print out a random number. That's because computers can't decide, which is what our brain is for. If you ask a human to say either "one" or "zero", he can decide for one of the two choices in your head and say aloud his result. The number is not even really random in this case, it's just a decision by that person, but since other people won't know what he thinks, you can assume the answer as random. Or we can roll a dice, the result is not really random - actually it's decided by the laws of physics (air pressure, forces etc.), but since nobody can control those laws enough, the result of a thrown dice can be considered random. Back to the computer: it can't do that. In order to decide something, you would always need to assign some kind of condition. For example, if you imagine a computer is asked for a "random number" every five or six months, the computer could possibly connect to the internet and search for the word "there" on some news title-page. It's likely that somewhere in one of the headlines, the word "there" exists, if it does the computer could return "one", if the word can't be found, the computer could return "zero". Let's imagine another situation, this time a computer with access to heat sensors could be asked for a random number every few weeks. This computer could scan the temperature and return "one" if it is above a certain level or "zero" if it is not. However, most computers don't have that functionality, some don't even have access to the internet (and of course those two computers would always return the same result atleast for one day, and we need different random numbers simultaneosly). What we need is some information that every computer can offer, but that nobody can know ahead of time. And there is: the date and time. You might say: hey, I know what date it is and I know what time it is, how can we turn that information into random numbers? You need to think in small units. In milliseconds for example. Imagine you take the time of the day with lots of numbers even after the dot (for example, you start our application or game exactly when it is 12:36.385632 - twelve hours and 36.385632 minutes after midnight) - nobody knows this exact number, it's very likely to be different every time the user starts our game and you could then assign a condition like this to our case: multiply the number of minutes by 1000000, which would give us 36385632 and then see whether that number is dividable by two without leaving a rest - in our case it is, so return "zero" and not "one". There we have a pseudo-random number, not really random, but to the user or player later, it seems random. Whether you have your time settings set correctly in your system or wrong (for example showing a wrong date or time of the day) doesn't matter in this case. This is how most, if not all random number generators work - and this is how we can get AGS to supply us with much-needed random numbers. Luckily, we don't need to do all this by ourselves, that functionality is already included in AGS. The function is called "Random ( int max_value )" and returns a random number between 0 and "max_value". We will use this function heavily, because when spawning a star, we will need lots of random influence (such as a random value for the amount of transparency for example). Back to the function: in order to understand better what the function does, we will part it up. The following is part one out of two: // define a variable for the id-number of the star, that we want to create int star_id; // use a backward while loop to determine which star should be created // we should then come up with the star with the smallest 'star_id' value // but which still has its framecounting time variable set to 0 int i = PARTICLES_MAX - 1; while ( i > 0 ) { if ( star[i].time == 0 ) // if the framecounting time variable equals 0 { // then we will mark this star star_id = i; } // continue the loop i--; } particle star[PARTICLES_MAX]; // change transparency value of star number 325 to 83 star[325].health = 83; // define a variable for the id-number of the star, that we want to create int star_id; // use a backward while loop to determine which star should be created // we should then come up with the star with the smallest 'star_id' value // but which still has its framecounting time variable set to 0 int i = PARTICLES_MAX - 1; while ( i > 0 ) { if ( star[i].time == 0 ) // if the framecounting time variable equals 0 { // then we will mark this star star_id = i; } // continue the loop i--; } Okay, I hope I made everything clear to this point. Again, we can now assume "star_id" to be the id-number of the star that we want to create. This makes us ready for part two of the function: // now that we have the ID value for the star we want to create, we now have to // set some initial values / settings for it // set its x position to the x value this function gives us by parameter star[star_id].x = x; // set its y position to the y value this function gives us by parameter star[star_id].y = y; // set its sprite slot to random ( between 1000 and 1002 ) star[star_id].sprite_slot = Random ( 2 ) + 1000; // set its gravity value to 0 star[star_id].gravity = 0.0; // set its transparency value to something between 50 and 100 star[star_id].transparency = Random ( 50 ) + 50; // and set its framecounting time variable to 1 star[star_id].time = 1; // now choose a speed from 0 to 99 star[star_id].speed = IntToFloat ( Random ( 100 ) + 50 ); // and choose an angle from -1 to 1 ( in radians ) star[star_id].angle = IntToFloat ( Random ( 100 ) * ( FloatToInt ( 2.0 * MATH_PI ) / 2 ) ); // then set its velocity on the x-axis to its correct value star[star_id].velocity_x = Maths.Cos ( star[star_id].angle ) * star[star_id].speed; // and then set its velocity on the y-axis to its correct value star[star_id].velocity_y = Maths.Sin ( star[star_id].angle ) * star[star_id].speed; The first two properties we set for our star is its position on both axis. Remember where "x" and "y" came from? They where the parameters from our function. We don't have to worry about the location, the person that calls our function later on will have to supply us with a location. In the first two lines, all we do is to place the star exactly where it should be (if the function is called like this: "create_star ( 49, 84 )", we would place the star at 49, 84 respectively). Then we set the sprite-slot for our new star. By doing this, we decide how exactly it is going to look like. We drew three different looking variations and we need to choose one now. You could simply set this to the sprite-slot number of one of your graphics, but then all stars would look the same. So this is the first time that we will need random influence. We set the sprite slot to "Random ( 2 ) + 1000". "Random ( 2 )" can return 0, 1 or 2. We add 1000, so the result is either 1000, 1001 or 1002. Those numbers have to be the three sprite-slot numbers of our sprites. If your numbers are different or if you drew and imported more sprites, you will need to alter this piece of code. To find out what the sprite-slot of one of your graphics is, you need to click on Sprite manager in the AGS editor, then locate the sprite that you want to know the slot-number of and look what the number right underneath the image says. That's the sprite-slot. You can also change this number by right-clicking on the image and selecting Change sprite number, but that is not really a good idea, because it might mess up other stuff. Of course, you need to have all numbers lie in a row (like 1000, 1001 and 1002), but that should be automatically the case if you imported them one after the other. After that, we simply set the gravity that currently affects our star to "0". This is not realistic of course, but for simplicity's sake we will just handle gravity a little bit different than we should and make the gravity start this way without force. Then we decide how transparent the star will be drawn. We want to set the amount of transparency to some value inbetween 50 and 100 (in percent, where 100 actually means completly visible and 50 half-visible), because otherwise a lot of stars would get very transparent and hard to see, which would mean wasting resources. In the following line, we set the framecounting variable "time" to 1. This means that our particle is now considered active and alive instead of inactive. The next four lines set both speed and angle as well as the velocity on both axis to random values. The calculations made are pretty basic, it chooses a speed between 50 and 150 for the particle (you could change that to a speed between 0 and 10 if you wanted very slow particles etc.), a completly random angle (recalculated in degrees it would choose an angle between 0 and 360 degrees, so our particle can literally go anywhere - again, you could restrict that and only allow particles to be spawned approximately upwards in a 50 degree cone for example by using your knowledge about the "Random"-function as well as about the maths behind radians) and finally in the last two lines, we calculate the correct values for the velocity on both axis by taking the cosine and sine of the angle and multiplying this value by speed. If you don't understand the maths behind this, I'd urge you to get a basic grasp on trigonometry before trying to change those lines. This pretty much sums up the discussion on our first function. We can now create stars as we like. However, we need more control - the next function is going to be called "handle_stars ( )" and is going to do exactly that - handling and taking a look over all our stars. Instead of being called whenever necessary (as it was with the first function - we only called it whenever we needed it meaning whenever we wanted to spawn a star), we will call this function every single frame. More on when to call the functions later, first comes the complete second function: function handle_stars ( ) // this function needs to be called every single frame and handles the stars { int i = 0; // loop through all stars while ( i < PARTICLES_MAX ) { if ( star[i].time > 0 ) // if the star is activated on the screen somewhere { // move it star[i].x += star[i].velocity_x * IntToFloat ( GetGameSpeed ( ) ) / 1000.0; star[i].y += star[i].velocity_y * IntToFloat ( GetGameSpeed ( ) ) / 1000.0 + star[i].gravity; // now actually draw the star at its position, using its own sprite slot and its own transparency value RawDrawImageTransparent ( FloatToInt ( star[i].x ), FloatToInt ( star[i].y ), star[i].sprite_slot, star[i].transparency ); // then increase the gravity that affects the particle star[i].gravity += IntToFloat ( star[i].time ) / 100.0; // and then increase the framecounting time variable star[i].time++; if ( FloatToInt ( star[i].x ) < 0 || FloatToInt ( star[i].x ) > 320 || FloatToInt ( star[i].y ) > 200 ) // if the star has left the screen { // then remove it remove_star ( i ); } } // continue the loop i++; } } As you may have noticed, this function is not quite as large as the other and we already have lots of stuff cleared and out of the way, so this time I don't think it's necessary or logical to part this function up again, let's rather just get started with a line-to-line analysis! int i = 0; // loop through all stars while ( i < PARTICLES_MAX ) { if ( star[i].time > 0 ) // if the star is activated on the screen somewhere { // move it star[i].x += star[i].velocity_x * IntToFloat ( GetGameSpeed ( ) ) / 1000.0; star[i].y += star[i].velocity_y * IntToFloat ( GetGameSpeed ( ) ) / 1000.0 + star[i].gravity; // now actually draw the star at his position, using his own sprite slot and his own transparency value RawDrawImageTransparent ( FloatToInt ( star[i].x ), FloatToInt ( star[i].y ), star[i].sprite_slot, star[i].transparency ); // then increase the gravity that affects the particle star[i].gravity += IntToFloat ( star[i].time ) / 100.0; // and then increase the framecounting time variable star[i].time++; The second line is even simpler and just increases the time variable. Since we have handled this star, we can now make him one frame older. if ( FloatToInt ( star[i].x ) < 0 || FloatToInt ( star[i].x ) > 320 || FloatToInt ( star[i].y ) > 200 ) // if the star has left the screen { // then remove it remove_star ( i ); } } // continue the loop i++; } } function remove_star ( int star_id ) // removes a star { // set its framecounting time variable to 0 star[star_id].time = 0; } // set its framecounting time variable to 0 star[star_id].time = 0; The rest function repeatedly_execute ( ) // is called every frame { // reset the screen every frame RawRestoreScreen ( ); // choose white as color RawSetColorRGB ( 255, 255, 255 ); // draw a message to the screen if ( !rep_mode ) // if we aren't in "repeating mode" { // display first line of message RawPrint ( GetViewportX ( ) + 10, GetViewportY ( ) + 10, "LEFT-CLICK TO EMIT!" ); } // display second line of message RawPrint ( GetViewportX ( ) + 10, GetViewportY ( ) + 20, "RIGHT-CLICK TO CHANGE MODE!" ); if ( rep_mode ) // if we are in "repeating mode" { // then spawn one star every frame create_star ( IntToFloat ( GetViewportX ( ) + mouse.x ), IntToFloat ( GetViewportY ( ) + mouse.y ) ); } // handle the stars handle_stars ( ); } function on_key_press ( int keycode ) { if ( keycode == 27 ) // if "ESC" was pressed // quit the game QuitGame ( 0 ); else if ( keycode == 434 ) // if "F12" was pressed // save a screenshot SaveScreenShot ( "Screenshot.bmp" ); } function on_mouse_click ( MouseButton button ) { if ( button == eMouseLeft ) // if it was a left-click { if ( !rep_mode ) // if we are not in "repeating mode" { int i = 0; while ( i < PARTICLES_NUM_CLICK ) // loop through all stars that we want to create { // create the star at mouse coordinates create_star ( IntToFloat ( GetViewportX ( ) + mouse.x ), IntToFloat ( GetViewportY ( ) + mouse.y ) ); // continue the loop i++; } } } else // if it was a right-click { // then change rep_mode around rep_mode = !rep_mode; } } The first function is called "repeatedly_execute ( )" and is called every single frame by AGS as the name implies. function repeatedly_execute ( ) // is called every frame { // reset the screen every frame RawRestoreScreen ( ); // choose white as color RawSetColorRGB ( 255, 255, 255 ); // draw a message to the screen if ( !rep_mode ) // if we aren't in "repeating mode" { // display first line of message RawPrint ( GetViewportX ( ) + 10, GetViewportY ( ) + 10, "LEFT-CLICK TO EMIT!" ); } // display second line of message RawPrint ( GetViewportX ( ) + 10, GetViewportY ( ) + 20, "RIGHT-CLICK TO CHANGE MODE!" ); if ( rep_mode ) // if we are in "repeating mode" { // then spawn one star every frame create_star ( IntToFloat ( GetViewportX ( ) + mouse.x ), IntToFloat ( GetViewportY ( ) + mouse.y ) ); } // handle the stars handle_stars ( ); } // reset the screen every frame RawRestoreScreen ( ); // choose white as color RawSetColorRGB ( 255, 255, 255 ); // draw a message to the screen if ( !rep_mode ) // if we aren't in "repeating mode" { // display first line of message RawPrint ( GetViewportX ( ) + 10, GetViewportY ( ) + 10, "LEFT-CLICK TO EMIT!" ); } // display second line of message RawPrint ( GetViewportX ( ) + 10, GetViewportY ( ) + 20, "RIGHT-CLICK TO CHANGE MODE!" ); We then draw the messages (actually we first run through a check whether we are in "repeated mode" or not to decide whether we should print the first message or not) by using the AGS functions "RawPrint". The first two parameters those functions take is the position where the text is going to be drawn. "GetViewportX ( )" and "GetViewportY ( )" simply return the left and upper screen corner coordinates. We then add a few pixels to this corner, because we don't want the text to be exactly in the left-upper-corner but a little more to the right and lower. The third parameter is obviously the message itself. if ( rep_mode ) // if we are in "repeating mode" { // then spawn one star every frame create_star ( IntToFloat ( GetViewportX ( ) + mouse.x ), IntToFloat ( GetViewportY ( ) + mouse.y ) ); } // handle the stars handle_stars ( ); function on_key_press ( int keycode ) { if ( keycode == 27 ) // if "ESC" was pressed // quit the game QuitGame ( 0 ); else if ( keycode == 434 ) // if "F12" was pressed // save a screenshot SaveScreenShot ( "Screenshot.bmp" ); } This brings us to the last function of our little project - "on_mouse_click ( ... )". This one is similar to the small function above, it just doesn't concern keys on the keyboard, but the mouse-buttons. function on_mouse_click ( MouseButton button ) { if ( button == eMouseLeft ) // if it was a left-click { if ( !rep_mode ) // if we are not in "repeating mode" { int i = 0; while ( i < PARTICLES_NUM_CLICK ) // loop through all stars that we want to create { // create the star at mouse coordinates create_star ( IntToFloat ( GetViewportX ( ) + mouse.x ), IntToFloat ( GetViewportY ( ) + mouse.y ) ); // continue the loop i++; } } } else // if it was a right-click { // then change rep_mode around rep_mode = !rep_mode; } } if ( button == eMouseLeft ) // if it was a left-click { if ( !rep_mode ) // if we are not in "repeating mode" { int i = 0; while ( i < PARTICLES_NUM_CLICK ) // loop through all stars that we want to create { // create the star at mouse coordinates create_star ( IntToFloat ( GetViewportX ( ) + mouse.x ), IntToFloat ( GetViewportY ( ) + mouse.y ) ); // continue the loop i++; } } else // if it was a right-click { // then change rep_mode around rep_mode = !rep_mode; } Now, we are really almost done. There are only two more little things left to do. The first thing is to click on Room editor in AGS, then double click on the room in the list on the right that we created in the beginning, now choose Room from the menu on the top of AGS and click on Room interactions or hit ctrl + i - double-click on Player enters room (after fadein) and select Run script from the upcomming drop-down menu. Next, hit the Edit script-button and add the following line to the editor window: // script for Room: Player enters screen (after fadein) RawSaveScreen ( ); The second thing I mentioned was that we need to select the cursor. When we imported sprites, we drew not only stars, but also a crosshair that we wanted to use as mouse cursor. In order to do that, click on Cursors on the right side in AGS, select Cursor 0: Walk to from the list and click on Change image - then just locate and double-click on your imported crosshair image. That should do the trick! Conclusion #define MATH_PI 3.14159265358979323846 // little mathematical helper #define PARTICLES_MAX 1000 // maximum number of particles #define PARTICLES_NUM_CLICK 50 // how many particles will be shown when mouse is clicked bool rep_mode; // keep track of whether we are in rep mode or not // STRUCTURES struct particle { float x; // the position on the x-axis ( in pixel ) float y; // the position on the y-axis ( in pixel ) float velocity_x; // the velocity on the x-axis ( in pixel ) float velocity_y; // the velocity on the y-axis ( in pixel ) float speed; // the speed of the particle ( in pixel ) float angle; // the angle of the particle ( in radians ) int time; // the number of frames it has been living ( in frames ) int sprite_slot; // the sprite slot of the particle sprite float gravity; // the amount of gravity that affects the particle int transparency; // the amount of transparency for the particle ( in percent ) };
// define our stars through an array particle star[PARTICLES_MAX]; // FUNCTIONS function create_star ( float x, float y ) // creates a particle at a given location { // define a variable for the id-number of the star, that we want to create int star_id; // use a backward while loop to determine which star should be created // we should then come up with the star with the smallest 'star_id' value // but which still has its framecounting time variable set to 0 int i = PARTICLES_MAX - 1; while ( i > 0 ) { if ( star[i].time == 0 ) // if the framecounting time variable equals 0 { // then we will mark this star star_id = i; } // continue the loop i--; } // now that we have the ID value for the star we want to create, we now have to // set some initial values / settings for it // set its x position to the x value this function gives us by parameter star[star_id].x = x; // set its y position to the y value this function gives us by parameter star[star_id].y = y; // set its sprite slot to random ( between 1000 and 1002 ) star[star_id].sprite_slot = Random ( 2 ) + 1000; // set its gravity value to 0 star[star_id].gravity = 0.0; // set its transparency value to something between 50 and 100 star[star_id].transparency = Random ( 50 ) + 50; // and set its framecounting time variable to 1 star[star_id].time = 1; // now choose a speed from 0 to 99 star[star_id].speed = IntToFloat ( Random ( 100 ) + 50 ); // and choose an angle from -1 to 1 ( in radians ) star[star_id].angle = IntToFloat ( Random ( 100 ) * ( FloatToInt ( 2.0 * MATH_PI ) / 2 ) ); // then set its velocity on the x-axis to its correct value star[star_id].velocity_x = Maths.Cos ( star[star_id].angle ) * star[star_id].speed; // and then set its velocity on the y-axis to its correct value star[star_id].velocity_y = Maths.Sin ( star[star_id].angle ) * star[star_id].speed; } function remove_star ( int star_id ) // removes a star { // set its framecounting time variable to 0 star[star_id].time = 0; } function handle_stars ( ) // this function needs to be called every single frame and handles the stars { int i = 0; // loop through all stars while ( i < PARTICLES_MAX ) { if ( star[i].time > 0 ) // if the star is activated on the screen somewhere { // move it star[i].x += star[i].velocity_x * IntToFloat ( GetGameSpeed ( ) ) / 1000.0; star[i].y += star[i].velocity_y * IntToFloat ( GetGameSpeed ( ) ) / 1000.0 + star[i].gravity; // now actually draw the star at its position, using its own sprite slot and its own transparency value RawDrawImageTransparent ( FloatToInt ( star[i].x ), FloatToInt ( star[i].y ), star[i].sprite_slot, star[i].transparency ); // then increase the gravity that affects the particle star[i].gravity += IntToFloat ( star[i].time ) / 100.0; // and then increase the framecounting time variable star[i].time++; if ( FloatToInt ( star[i].x ) < 0 || FloatToInt ( star[i].x ) > 320 || FloatToInt ( star[i].y ) > 200 ) // if the star has left the screen { // then remove it remove_star ( i ); } } // continue the loop i++; } } #sectionstart game_start function game_start ( ) // is called on game start and before the first room loads { } #sectionend game_start #sectionstart repeatedly_execute function repeatedly_execute ( ) // is called every frame { // reset the screen every frame RawRestoreScreen ( ); // choose white as color RawSetColorRGB ( 255, 255, 255 ); // draw a message to the screen if ( !rep_mode ) // if we aren't in "repeating mode" { // display first line of message RawPrint ( GetViewportX ( ) + 10, GetViewportY ( ) + 10, "LEFT-CLICK TO EMIT!" ); } // display second line of message RawPrint ( GetViewportX ( ) + 10, GetViewportY ( ) + 20, "RIGHT-CLICK TO CHANGE MODE!" ); if ( rep_mode ) // if we are in "repeating mode" { // then spawn one star every frame create_star ( IntToFloat ( GetViewportX ( ) + mouse.x ), IntToFloat ( GetViewportY ( ) + mouse.y ) ); } // handle the stars handle_stars ( ); } #sectionend repeatedly_execute #sectionstart on_key_press function on_key_press ( int keycode ) { if ( keycode == 27 ) // if "ESC" was pressed // quit the game QuitGame ( 0 ); else if ( keycode == 434 ) // if "F12" was pressed // save a screenshot SaveScreenShot ( "Screenshot.bmp" ); } #sectionend on_key_press #sectionstart on_mouse_click function on_mouse_click ( MouseButton button ) { if ( button == eMouseLeft ) // if it was a left-click { if ( !rep_mode ) // if we are not in "repeating mode" { int i = 0; while ( i < PARTICLES_NUM_CLICK ) // loop through all stars that we want to create { // create the star at mouse coordinates create_star ( IntToFloat ( GetViewportX ( ) + mouse.x ), IntToFloat ( GetViewportY ( ) + mouse.y ) ); // continue the loop i++; } } } else // if it was a right-click { // then change rep_mode around rep_mode = !rep_mode; } } #sectionend on_mouse_click That's it. We're done, you can save your project and then run it. If there is any problem, feel free to contact me through our forums. I hope that I didn't make any mistakes, otherwise I might revise the text and change it. I know that this really is a lot of text to work through, and there is vital information here and there, but it really isn't quite as easy to write something on programming. I definately learned a lot by writing this myself, because there are just so many things that you get used to and not even think about, and when you get used to things, you start to forget why you did them in the first place. Although this might not be the uber-helpful learning resource, I hope it inspired you atleast to start becoming more involved in the technical aspects of programming. Thanks to Misja from the team for the text-formatting! Jan Simon - Juli 2006 |
![]() |
Jan is the lead programmer for the project, and has already rebuilt the superb playable demo. | ![]() |