- Messages
- 240
- Country
That's another tutorial commenting the GDI+ C++ sample I've posted a few days ago.
Some people had a look at the source code and already noticed it included some classes to handle L: variables, here's the explanation how they work. This is not by any means the only way it could be done, it's more like an example to have a chance to discuss how C++ and OOP techniques could be used to write code that will speed up development.
Here's the link to the project:
http://www.mediafire.com/download/zidu446ar6l2awz
The class is named CNamedVar and it's declared in the CSimulation.h file of the example, let's have a look at its source:
This is the class constructor, which is called automatically when a new variable of that class is instantiated. The constructor has many parameters, but only the first one is mandatory, which is a string containing the name of the variable. All other parameters are optional, which means if you don't use them, they will have default values. In order to keep this post shorter, we'll discuss the other parameters in a separate thread.
This means, in the simplest way, you could create a new L: variable this way:
The constructor will call the register_named_variable() from the Panels SDK for you, will store the ID as an internal member ( so you won't have to be concerned in your code ) and will call the Set() member function to initialize it to a default starting value. Since the start_value parameter is optional, we haven't use it, and has a default value of 0, the variable will be automatically set to 0.
Using default values and constructors is a good way to be absolutely *sure* your variables WILL be initialized, since many difficult to find bugs in C (where you could easily declare a variable without initializing it, pointing to random memory data) are usually related to having forgot to initialize a variable. C++ tries to help you by using constructors and initialization lists to reduce this risk.
A good place to declare this new variable could be in the CSimulation Singleton. Although the notation L: seems to suggest that L: variable are "Local", they are in effect Globals, because an airplane gauge declaring an L: variable, would expose it to the scenery engine on a global basis. Since they *are* globals by any means, we could put them in our "box of globals", which is the CSimulation Singleton.
A convenient way of doing it would be declaring the variable in the Singleton constructor declaration, and instancing it in the Singleton constructor initialization list, this way:
I've removed the other variables used in the example to make this post shorter (you'll find them in the .ZIP file), and to make easier to see how the variable is declared. Declaration only is made in the Singleton constructor declaration, and initialization is made in the constructor initialization list, the one that starts after the : sign and before the {} brackets, which makes the constructor body.
To read/write the variable, we'll use the Get() and Set() member functions. The Get() function is very straightforward:
It takes no parameters in input, and it will return the result as a double. L: variables are always doubles, so there's no need to use any kind of polymorphism here. The function checks the mID member to be sure the variable is valid and an ID has been assigned to it by FSX, and it will set the mValue member to the value returned by the Panels SDK function get_named_variable_value(), so now our internal member is holding the value as returned by FSX. Finally, it just returns the mValue member to the caller.
The Set() function is also very easy to understand:
It takes a double named "val" as input, and will not return anything. Again, we check if the mID is valid, and we'll both set the mValue member to the value of the "val" parameter, and then we call the set_named_variable_value() function of the Panels SDK to set the variable.
Since we put this L: variable inside our Singleton, which has been declared as a global pointer at the start of the GaugeDeclarations.cpp this way:
And instanced in the module_init() function of the same file:
We can now access to the Singleton using the Sim pointer, which is the "remote control" of our global Singleton object, which now CONTAINS our new L: variable.
Now that our variable has its place in the simulation, so let's do something with it, (it doesn't make much sense, but it would clear the concept), we'll change the variable value to a random factor every second, and we'll reset it to 0 when it reaches a certain value, pretending is some kind of a system simulation of sort, just for the example's sake.
So, in the Update1Sec() function, defined in the CSimulation.cpp file, we can add the following:
Very silly code, but it shows how to use the variable, we set it to a random value from 0 to 100, but we reset it to 0 if the value was higher than 75. This is happening every second.
Note that, we don't need to use the Sim-> notation to access the variable, because we ARE inside the Singleton class, so we can just use the variable name directly. The Sim-> notation will have to be used in case we'll need to access the variable from OUTSIDE the Singleton class (like in a gauge mouse handler routine, for example), and we can access it from anywhere, since we declared the Sim as a global pointer to the Singleton.
Now, we would probably want to display that value, so we go into the GDIPlus_Gauge.cpp file, in the DRAW cycle, doing this:
We just added MyVar to the list of variables to be printed. Here, we MUST access the variable through the Sim-> pointer, because we are in a gauge function, not in the Singleton class anymore.
A nice thing of using an OOP approach, is that you can be consistent and have an easy to understand code. Here, we have SOME variables that are internal of the FSX simulation ( the A: variables ), and one custom L: var we just made up. But regardless of what they are, you use it in the same way, accessing the Get() member function, that will do the different kind of interfacing with the sim, depending on the variable.
And, OOP would shield you from doing conceptual mistakes. For example, A: variables cannot be set through the panels SDK (there's no aircraft_varset() as a counterpart to the aircraft_varget() ), but they have key events instead that will act on their values. So, if you tried to do something like this:
The C++ compiler won't let you, because we haven't defined a Set() member function for objects of the CFSVar, which is the type associated to A: variables, we discussed in the other tutorial. So, you don't risk writing something that doesn't make sense, because you can't even compile it so, your own *design* of the class can help you from doing mistakes.
This concludes the first part of the tutorial. In the next one, I'll discuss some extra features of the CNamedVar class, using the other optional parameters and how they could be used in a typical airplane project.
Some people had a look at the source code and already noticed it included some classes to handle L: variables, here's the explanation how they work. This is not by any means the only way it could be done, it's more like an example to have a chance to discuss how C++ and OOP techniques could be used to write code that will speed up development.
Here's the link to the project:
http://www.mediafire.com/download/zidu446ar6l2awz
The class is named CNamedVar and it's declared in the CSimulation.h file of the example, let's have a look at its source:
Code:
CNamedVar(const char* varname, const double start_value = 0, const double min_value = 1, const double max_value = 999999, const bool wrap = false, const double max_frames = 200, const double not_animate_if = 0)
: mVarName(varname), mMinValue(min_value), mMaxValue(max_value), mWrap(wrap), mValue(start_value), mID(-1), mNotAnimateIf(not_animate_if), mMaxFrames(max_frames)
{
mID = register_named_variable(mVarName);
Set(start_value);
}
This is the class constructor, which is called automatically when a new variable of that class is instantiated. The constructor has many parameters, but only the first one is mandatory, which is a string containing the name of the variable. All other parameters are optional, which means if you don't use them, they will have default values. In order to keep this post shorter, we'll discuss the other parameters in a separate thread.
This means, in the simplest way, you could create a new L: variable this way:
Code:
CNamedVar MyVar = CNamedVar( "MyVar" );
The constructor will call the register_named_variable() from the Panels SDK for you, will store the ID as an internal member ( so you won't have to be concerned in your code ) and will call the Set() member function to initialize it to a default starting value. Since the start_value parameter is optional, we haven't use it, and has a default value of 0, the variable will be automatically set to 0.
Using default values and constructors is a good way to be absolutely *sure* your variables WILL be initialized, since many difficult to find bugs in C (where you could easily declare a variable without initializing it, pointing to random memory data) are usually related to having forgot to initialize a variable. C++ tries to help you by using constructors and initialization lists to reduce this risk.
A good place to declare this new variable could be in the CSimulation Singleton. Although the notation L: seems to suggest that L: variable are "Local", they are in effect Globals, because an airplane gauge declaring an L: variable, would expose it to the scenery engine on a global basis. Since they *are* globals by any means, we could put them in our "box of globals", which is the CSimulation Singleton.
A convenient way of doing it would be declaring the variable in the Singleton constructor declaration, and instancing it in the Singleton constructor initialization list, this way:
Code:
class CSimulation : public CSingleton<CSimulation>
{
friend class CSingleton<CSimulation>;
public:
void Start(void) { mStarted = true; };
void Stop(void) { mStarted = false; };
const bool IsStarted(void) { return mStarted; };
void Update6hz(void);
void Update1Sec(void);
void Update4Sec(void);
void UpdateFrame(void);
CNamedVar MyVar;
private:
CSimulation()
: mStarted(false), MyVar( "MyVar" )
{};
~CSimulation()
{};
bool mStarted;
};
I've removed the other variables used in the example to make this post shorter (you'll find them in the .ZIP file), and to make easier to see how the variable is declared. Declaration only is made in the Singleton constructor declaration, and initialization is made in the constructor initialization list, the one that starts after the : sign and before the {} brackets, which makes the constructor body.
To read/write the variable, we'll use the Get() and Set() member functions. The Get() function is very straightforward:
Code:
double Get(void)
{
if (mID >= 0)
{
mValue = get_named_variable_value(mID);
}
return mValue;
}
It takes no parameters in input, and it will return the result as a double. L: variables are always doubles, so there's no need to use any kind of polymorphism here. The function checks the mID member to be sure the variable is valid and an ID has been assigned to it by FSX, and it will set the mValue member to the value returned by the Panels SDK function get_named_variable_value(), so now our internal member is holding the value as returned by FSX. Finally, it just returns the mValue member to the caller.
The Set() function is also very easy to understand:
Code:
void Set(const double val)
{
if (mID >= 0)
{
mValue = val;
set_named_variable_value(mID, (FLOAT64)mValue);
}
}
It takes a double named "val" as input, and will not return anything. Again, we check if the mID is valid, and we'll both set the mValue member to the value of the "val" parameter, and then we call the set_named_variable_value() function of the Panels SDK to set the variable.
Since we put this L: variable inside our Singleton, which has been declared as a global pointer at the start of the GaugeDeclarations.cpp this way:
Code:
CSimulation* Sim = NULL;
And instanced in the module_init() function of the same file:
Code:
Sim = CSimulation::Instance();
We can now access to the Singleton using the Sim pointer, which is the "remote control" of our global Singleton object, which now CONTAINS our new L: variable.
Now that our variable has its place in the simulation, so let's do something with it, (it doesn't make much sense, but it would clear the concept), we'll change the variable value to a random factor every second, and we'll reset it to 0 when it reaches a certain value, pretending is some kind of a system simulation of sort, just for the example's sake.
So, in the Update1Sec() function, defined in the CSimulation.cpp file, we can add the following:
Code:
void CSimulation::Update1Sec(void)
{
MyVar.Set( (double)rand() / RAND_MAX * 100.0);
if( MyVar.Get() > 75 )
MyVar.Set( 0 );
}
Note that, we don't need to use the Sim-> notation to access the variable, because we ARE inside the Singleton class, so we can just use the variable name directly. The Sim-> notation will have to be used in case we'll need to access the variable from OUTSIDE the Singleton class (like in a gauge mouse handler routine, for example), and we can access it from anywhere, since we declared the Sim as a global pointer to the Singleton.
Now, we would probably want to display that value, so we go into the GDIPlus_Gauge.cpp file, in the DRAW cycle, doing this:
Code:
StringCbPrintf(mpszDest, mcbDest, L"LAT: %6.3f°\nLON: %6.3f°\nALT: %8.0f ft.\n\nHeading: %6.3f°\nMyVar: %.2f",
Sim->PlaneLatitude.Get(),
Sim->PlaneLongitude.Get(),
Sim->PlaneAltitude.Get(),
Sim->Heading.Get(),
Sim->MyVar.Get()
);
We just added MyVar to the list of variables to be printed. Here, we MUST access the variable through the Sim-> pointer, because we are in a gauge function, not in the Singleton class anymore.
A nice thing of using an OOP approach, is that you can be consistent and have an easy to understand code. Here, we have SOME variables that are internal of the FSX simulation ( the A: variables ), and one custom L: var we just made up. But regardless of what they are, you use it in the same way, accessing the Get() member function, that will do the different kind of interfacing with the sim, depending on the variable.
And, OOP would shield you from doing conceptual mistakes. For example, A: variables cannot be set through the panels SDK (there's no aircraft_varset() as a counterpart to the aircraft_varget() ), but they have key events instead that will act on their values. So, if you tried to do something like this:
Code:
Sim->PlaneLatitude.Set( 10 );
The C++ compiler won't let you, because we haven't defined a Set() member function for objects of the CFSVar, which is the type associated to A: variables, we discussed in the other tutorial. So, you don't risk writing something that doesn't make sense, because you can't even compile it so, your own *design* of the class can help you from doing mistakes.
This concludes the first part of the tutorial. In the next one, I'll discuss some extra features of the CNamedVar class, using the other optional parameters and how they could be used in a typical airplane project.
Last edited: