TGB/ScriptTutorials/FadingTutorial
From TDN
[edit] PreambleWelcome to the TGB Fading Tutorial. Fading objects in and out is a commonly desired feature and this tutorial will teach you how to implement that feature for yourself! The fine-tuned code in this tutorial is fast, easy to understand, and a breeze to modify. Enjoy! [edit] Tutorial TakeawayIn this tutorial you will:
[edit] Prerequisites to the TutorialThis tutorial expects you to have a basic understanding of objects, scripting, and data flow within TGB. This program is a tutorial designed to familiarize you with TorqueScript and its application, not to get you started in the wonderful world of TGB. If you do not yet feel confident enough in your abilities to start your own project and begin coding, take a look at a few tutorials on Getting Started. This tutorial will show you how to write a general function for object fading and, more generally, manipulation, not where to write it. [edit] FadingThis section will first show you how to write basic fading functionality. It will then teach you how to expand upon that functionality with callbacks, interesting implementations, and event management. [edit] Basic FunctionalityFading an object in and out involves modifying its opacity over time. For any object in TGB, the opacity is expressed by the object's alpha value. getBlendAlpha() and setBlendAlpha() get and set this alpha value respectively. Any object derived from t2dSceneObject has access to these two functions. Therefore, to make the fading function as applicable as possible, add it to the t2dSceneObject namespace by writing the following: function t2dSceneObject::fade(%this, %toAlpha, %time) By writing t2dSceneObject:: before the function name, you have ensured that any object with "t2dSceneObject" in it's namespace will have access to this function. Let's now move on to implementing the fade functionality. Modify your fade function to read as follows: function t2dSceneObject::fade(%this, %toAlpha, %time)
{
if(%time > $FadeGranularity)
{
%alpha = %this.getBlendAlpha();
%updatesRemaining = %time / $FadeGranularity;
%alpha += (%toAlpha - %alpha) / %updatesRemaining;
%this.setBlendAlpha(%alpha);
%this.schedule($FadeGranularity, fade, %toAlpha, %time - $FadeGranularity);
}
else
{
%this.setBlendAlpha(%toAlpha);
}
}
And add the following global variable to your script: $FadeGranularity = 50; There it is: the basic fading function in all its glory. Try these function calls to test it, replacing %myObj with an object in your project: %myObj.fade(0, 1000); // Fades an object out over one second. %myObj.fade(1, 1000); // Fades an object in over one second. fade() incrementally changes the opacity of an object to a target value, given a time (in milliseconds). Let's walk through this function, starting with the $FadeGranularity variable. $FadeGranularity determines how frequently the object is faded; it is used directly as "the interval between successive calls to fade()". The larger this value is, the more coarse the fading will appear as it won't receive as many updates. The smaller the value, the smoother the fading transition. The importance of this variable will become clearer as you grasp the guts of the fade function. The first thing we do in the fade function is ask if the overall fading time is greater than the predetermined interval. if(%time > $FadeGranularity)
{
If this is the case then we have at least one more remaining incremental update left and we start working on it. First, we get the object's current alpha value (a value between 0 and 1, inclusive) and store it. %alpha = %this.getBlendAlpha(); Next, we calculate the remaining number of updates by dividing the time left by the increment. %updatesRemaining = %time / $FadeGranularity; The next line of code is where the heavy lifting is done. Essentially we update the object's [locally stored] alpha value with the change required by a single incremental update. %alpha += (%toAlpha - %alpha) / %updatesRemaining; The easiest way to interpret all of this is to look at %alpha as a point on a race track. %toAlpha - %alpha, then, is actually finishLine - currentPosition. You then divide that "distance to the finish line" by the number of updates remaining to get the distance traveled in this current increment (read: the current call to fade()). (Note that this works even if you are walking backwards - fading out. You are encouraged to try some quick calculations to see that this is indeed the case.) "The distance traveled in the current increment" value is then immediately applied to the previous location (alpha value). Next we tell the object of its new alpha value: %this.setBlendAlpha(%alpha); And we're done! Well, not really. We know from the if-statement that there's still a bit of time left before the object should hit its target alpha value; we have at least one more increment left! We must now schedule the next update to the alpha value by calling schedule() on the object with the fade() function: %this.schedule($FadeGranularity, fade, %toAlpha, %time - $FadeGranularity);
}
schedule() is an incredibly powerful function and integral to any time-based simulation (fading over time being one of these time-based simulations). The above line of code can be read as "schedule a call to fade() on the %this object to occur in $FadeGranularity milliseconds using the arguments %toAlpha and %time - $FadeGranularity". This line results in a call to %this.fade(%toAlpha, %time - $FadeGranularity) after approximately $FadeGranularity (currently 50) milliseconds have passed. Note that we use the %toAlpha value as is while %time is decremented by $FadeGranularity. Decrementing %time with each successive call to schedule() will eventually result in if(%time > $FadingGranularity) evaluating to false, putting us into the last part of the function: else
{
%this.setBlendAlpha(%toAlpha);
}
If there is less time left than the predefined increment, we finish up by setting the object's alpha to the target alpha and return. Congratulations! By now you should have a working fade function that you feel comfortable enough with to start expanding upon. [edit] Adding a CallbackCallbacks are a great way to add functionality to your game scripts. Lots of callbacks are provided by default in TGB but you will likely come upon some instances in which you think to yourself "Gosh, I really wish I could have some added functionality once that doohickity-operation finishes up!" This could even turn into something like "Boy, I really don't need that object after it finishes fading. I'd really just like to safeDelete() it!" //function t2dSceneObject::fade(%this, %toAlpha, %time, %command)
{
if(%time > $FadeGranularity)
{
%alpha = %this.getBlendAlpha();
%updatesRemaining = %time / $FadeGranularity;
%alpha += (%toAlpha - %alpha) / %updatesRemaining;
%this.setBlendAlpha(%alpha);
// %this.schedule($FadeGranularity, fade, %toAlpha, %time - $FadeGranularity, %command);
}
else
{
%this.setBlendAlpha(%toAlpha);
// eval(%command);
}
}
The %command variable is any string accepted by the eval() function. It also happens to be optional. Let's take a look at how it's used. The first thing we do is add it to the function definition. Plain and simple. Next we add it to the list of arguments given to fade() by schedule(), ensuring that the parameter is not lost after an increment. Finally, we add a line to the else-statement in which we evaluate %command using eval() . Interestingly enough, the following two commands are identical: %myObj.fade(0, 1000); // Fades an object out over one second. %myObj.fade(0, 1000, ""); // Fades an object out over one second. This is because %command is an optional parameter. If nothing is passed by the programmer, TGB fills the variable with a default value of the empty string "". Calling eval() on an empty string does nothing. Useful, huh? Now try out the following commands: $testX = 0; %myObj.fade(0, 1000, "$testX = 255;"); echo($testX); After one second, %myObj will be faded out and $testX will be set to 255. If you check your console you should see that number printed at the bottom. [edit] A Few Words On Using Eval()eval() is a surprisingly powerful function. It's also a bit confusing to use at first. Your life will be simplified, however, if you follow these three simple rules:
Rule 1 speaks to code completeness. If you wish to evaluate a statement that would require a ";" for proper compilation in a script file, then you must include it in the string that you pass to eval(). Rule 2 is required in complex cases involving multiple strings. As an example, let's see how the following would get printed to the console: Fading Done! Fade says "Hello!" At first you may try to write something akin to the following: %myObj.fade(0, 1000, "echo("Fading Done! Fade says "Hello!"");");
This, however, would result in a parsing error (there are three strings and one chunk of garbage in the call. Can you identify them all?). We can get around this by using escaped characters. The properly formatted call follows: %myObj.fade(0, 1000, "echo(\"Fading Done! Fade says \\\"Hello!\\\"\");"); The first and last quotation marks within the echo() statement are escaped normally. The embedded string, however, is surrounded by an escaped backslash and an escaped quotation mark on each side. You can embed strings ad infinitum in this way but you must be extremely careful. Rule 3 is extremely important. The only data (variables, objects, etc.) to which statements evaluated by eval() have access are those that are readily accessible within the context in which eval() is called. Take a look at the following: function preFadeBAD(%myObj, %multiplier)
{
%newTime = %multiplier * $fadeTime;
%myObj.fade(0, %newTime, "echo(%newTime / 1000);");
}
function preFadeGOOD(%myObj, %multiplier)
{
%newTime = %multiplier * $fadeTime;
%myObj.fade(0, %newTime, "echo(" @ %newTime @ "/ 1000);");
}
Both functions call fade() on %myObj with a new time based on a multiplier. The difference is in the command string passed to fade(). The %newTime variable is defined within the preFade functions only; there is no %newTime variable in the definition of fade(). The command passed to fade() in preFadeBAD() asks the engine to divide a %newTime variable by 1000. No %newTime variable exists when the command is passed to eval() (within fade()) so the expression evaluates to zero which is what gets printed to the console by echo(). The command in preFadeGOOD() is very different. In the preFadeGOOD() function, %newTime is passed-by-value; %newTime is evaluated to its value and spliced into the rest of the string as such. This results in dividing your %newTime value by 1000 within the context of the fade() function, the desired functionality. [edit] Recursive Recursion?You will likely have noticed that fade() is a tail-recursive function: it calls itself until a base case is met while requiring a constant number of simultaneous stack frames (in this case, one). Specifically, fade() asks the engine to schedule another call to itself. This happens repeatedly until %time gets low enough at which point fade() no longer schedules a call to itself. But this doesn't have to be the case! We used the eval() function in our else-statement and can ask fade() to call itself there as well! Consider the following function: function t2dSceneObject::startFadeLooping(%this, %time, %lowAlpha, %highAlpha)
{
%this.fadeInCallback = %this.getID() @ ".fade(" @ %lowAlpha @ ", " @ %time @ ", " @ %this.getID() @ ".fadeOutCallback);";
%this.fadeOutCallback = %this.getID() @ ".fade(" @ %highAlpha @ ", " @ %time @ ", " @ %this.getID() @ ".fadeInCallback);";
%this.fade(%lowAlpha, %time, %this.fadeOutCallback);
}
startFadeLooping() adds two member variables, fadeInCallback and fadeOutCallback, to the %this object. The two variables are eval()-valid strings. It then calls fade() on %this with %this.fadeOutCallback as a command. But %this.fadeOutCallback contains the instructions for another call to fade(), specifically a fade-in! With this one function, we've built ourselves an infinite loop that fades an object in and out endlessly! Note that we could simply replace the calls to getID() with the string "%this" because of how fade() is written. We stay away from this option for clarity. There is a way to break the loop, however. We can write the following function: function t2dSceneObject::stopFadeLooping(%this)
{
%this.fadeInCallback = "";
%this.fadeOutCallback = "";
}
After a call to stopFadeLooping(), the fadeInCallback and fadeOutCallback member variables will evaluate to the empty string, causing fade() to exit after its current fade-in or fade-out operation completes. [edit] Event ManagementYou may have noticed that there is one glaring problem with the function as it stands: multiple calls to fade() on a single object can result in some strange activity. For example, if you attempt to fade out an object while it's in the process of fading in the object will appear to flicker visibly. Wouldn't it be nice if we could cancel an active fade process? Well, it is possible! Fortunately for us, the schedule() function returns a reference to the event that we just scheduled. In all versions of fade() in the tutorial thus far, the returned event was essentially lost to the wind. We will change that now by creating a new member variable (as before, all modified or new lines have been commented - please uncomment them for use): function t2dSceneObject::fade(%this, %toAlpha, %time, %command)
{
if(%time > $FadeGranularity)
{
%alpha = %this.getBlendAlpha();
%updatesRemaining = %time / $FadeGranularity;
%alpha += (%toAlpha - %alpha) / %updatesRemaining;
%this.setBlendAlpha(%alpha);
// %this.fadeEvent = %this.schedule($FadeGranularity, fade, %toAlpha, %time - $FadeGranularity, %command);
}
else
{
%this.setBlendAlpha(%toAlpha);
eval(%command);
}
}
%this.fadeEvent will now store the eventID of the most recent call to fade(). With this variable stored, we now have access to a number of interesting Event Scheduling functions, most notably isEventPending() and cancel(). With these two functions, we can easily check to see if an object is currently fading and, if it is, cancel the fading before starting a new fade sequence: // Check to see if we're already fading. If we are then stop.
if(isEventPending(%myObj.fadeEvent))
{
cancel(%myObj.fadeEvent);
}
// We're no longer fading so start a new fade operation.
%myObj.fade(%newAlpha, %fadeTime);
[edit] Error CheckingWith any function as universal as our t2dSceneObject::fade() implementation, including some good error checking can prove extremely useful for debugging. Perhaps you wish to dynamically calculate an object's target transparency based on various metrics. Was the player hit with something? Does the transparency relate to the change in score? Ammo? How many jewels were flipped in the fifth row? All of the above? Certainly you could check to see that all of your values are in the proper range before calling fade() but you'd be rewriting the same checks all over the place. Why not push some error checking into fade() itself and then keep an eye on the console? This is actually very easy. There are only two values that we really need to check, $FadeGranularity and %toAlpha. All values of %time are handled by the engine so in general we don't need to worry about it (though you may wish to add a check to ensure that your calculations are working as you expect). Acceptable values of $FadeGranularity are positive values. Acceptable values of %toAlpha are anything between 0 and 1. Add the following snippet of code to the beginning of your fade() function. if($FadeGranularity < 1)
{
echo("\c2FADE: Invalid $FadeGranularity value of \c1" @ $FadeGranularity @ "\c2.");
return;
}
if((%toAlpha < 0) || (%toAlpha > 1))
{
echo("\c2FADE: Invalid Target Alpha value. Received \c1" @ %toAlpha @ "\c2.");
return;
}
If any external influence changed $FadeGranularity to an invalid value, the above code will write a message about it in the console. Similarly, any invalid %toAlpha value will show up in the console. You can expand these blocks to report more information about the %this object, modify them to apply pre-specified default values instead of simply returning, etc. You will notice that the above code includes some new escape sequences. \c0 through \c9 will color any subsequent text in the console with the associated color. Coloring in this way can be extremely useful for readability. As ever, you are encouraged to experiment! [edit] Beyond FadingFading in and out is great and everything, but what if we want to fade something other than opacity? Why not write a function to change the size of an object? Why stop there? Why not write a general function that allows you to specify what value you want to change? This is all possible and, in this section, you'll learn how. How about speeding up the process a bit? In every version of fade() presented so far we have been recomputing the change-per-step on each call. This function, however, is linear in nature and that means we have a constant change-per-step. Can we eliminate this? Sure! Read on to see how! [edit] Taking it Further: Object SizeIf you really examine fade() closely you'll see that it boils down to little more than a loop that increments or decrements a value on each pass. Currently fade() is written to modify the opacity of an object. But why not have a version that changes the object's size? Modifications to the fade() function are actually quite minimal. Compare your fade() function to the following example: // Resizes an object linearly to a specified size over the specified amount of time.
function t2dSceneObject::fadeSize(%this, %toWidth, %toHeight, %time, %command)
{
if(%time > $FadeGranularity)
{
%width = %this.getWidth();
%height = %this.getHeight();
%updatesRemaining = %time / $FadeGranularity;
%width += (%toWidth - %width) / %updatesRemaining;
%height += (%toHeight - %height) / %updatesRemaining;
%this.setWidth(%width);
%this.setHeight(%height);
%this.fadeSizeEvent = %this.schedule($FadeGranularity, fadeSize, %toWidth, %toHeight, %time - $FadeGranularity, %command);
}
else
{
%this.setWidth(%toHeight);
%this.setHeight(%toHeight);
eval(%command);
}
}
Note that both fade() and fadeSize() above rely upon the same global variable $FadeGranularity. [edit] Taking it Further: GeneralizedWhy stop with single modifications? Why not create an extremely general version of the function that executes the functions you tell it to? If we're going super-general, why don't we make a version that even SimObjects can use, right? After all, we can easily write get and set functions for variables handled entirely in script! However, not all of the required functions are automatically included in default builds of TGB (at the time of writing, the most recent version of TGB is 1.1.3). Necessary is the function ObjId.call(). This section is included for completeness as GarageGames has stated their intention to include this function in future versions (TGB 1.5 Beta 3 supports this function call out of the box). See the forums for information on how to build it into earlier versions of TGB. // Resizes an object's value linearly to a specified size over the specified amount of time.
function SimObject::LinearChangeOverTime(%this, %getFunction, %setFunction, %toValue, %time, %command)
{
if(%time > $FadeGranularity)
{
%currentVal = %this.call(%getFunction);
%updatesRemaining = %time / $FadeGranularity;
%currentVal += (%toValue - %currentVal) / %updatesRemaining;
%this.call(%setFunction, %currentVal);
%this.schedule($FadeGranularity, LinearChangeOverTime, %getFunction, %setFunction, %toValue, %time - $FadeGranularity, %command);
}
else
{
%this.call(%setFunction, %toValue);
eval(%command);
}
}
Note that, as implemented, there is no Event Management implementation and the function can only change one setting at a time (unlike fadeSize() above which simultaneously changes both width and height). [edit] Taking it Further: OptimizationYou may have noticed that fade() applies a constant change to an object's opacity on each call until the process completes. This change, however, is recomputed every time. If you have lots of scene objects fading at one time, this negligence could impact your performance. Below is an optimized version of the function that avoids this constant-time computation altogether: function t2dSceneObject::fadeFast(%this, %toAlpha, %time, %command)
{
%alpha = %this.getBlendAlpha();
%updatesRemaining = %time / $FadeGranularity; // Find the number of updates left.
%delta = (%toAlpha - %alpha) / %updatesRemaining;
%this.fadeFastHelp(%toAlpha, %delta, %time, %command);
}
// Blindly fades based on a preset increment to a specified opacity [in percentage] over the specified amount of time..
// Does not adapt to sudden changes in opacity.
// Does not adapt to sudden changes in FadeGranularity constant.
function t2dSceneObject::fadeFastHelp(%this, %toAlpha, %delta, %time, %command)
{
if(%time > $FadeGranularity)
{
%this.setBlendAlpha(%this.getBlendAlpha()+%delta);
%this.fadeEvent = %this.schedule($FadeGranularity, fadeFastHelp, %toAlpha, %delta, %time - $FadeGranularity, %command);
}
else
{
%this.setBlendAlpha(%toAlpha);
eval(%command);
}
}
fadeFast() computes the change that will occur each update. It then passes that value with other pertinent values to fadeFastHelp() which takes care of all of the actual updating. There are two large caveats to using this version as mentioned in the comments above fadeFastHelp(). The first is that any external changes to an object's opacity are not taken into consideration on an update. fade() computes the change based on the object's current opacity whereas fadeFast() updates it solely based upon a precomputed value. This can be avoided by checking to see if an object has a pending fadeEvent before modifying the opacity directly. The second caveat is that any change to $FadeGranularity will break any ongoing fadeFastHelp() processes. There are several ways to overcome this issue and you are encouraged to solve them on your own with a little exploration and ingenuity. This tutorial can't do everything for you: that would take all the fun out of it! [edit] EpilogueIf you've made it this far then give yourself a pat on the back. While this tutorial certainly teaches you how to implement a fade function, it covers much, much more. Hopefully you will take what you have learned here and implement some truly fantastic functionality. Get on out there and make some stuff disappear! And reappear!
|
Categories: TGB | T2D | TorqueScript | Tutorial



