Coding a Lunar Lander from the ground up: Parts 3 - 6 (Animation)

Hlynkacg

Aspiring rocket scientist
Addon Developer
Tutorial Publisher
Donator
Joined
Dec 27, 2010
Messages
1,870
Reaction score
3
Points
0
Location
San Diego
So now our ShuttlePB looks like a Lunar Lander...

I know I said that we'd talk about animations next, but as I was writing this I realised that even doing something comparatively simple like making the EVA Hatch open and close requires us to understand a bit about the way that orbiter controls individual vessels.

PART 3: Call Back Functions

Call Back functions are used by the Orbiter core to update a vessel's state within the simulation and notify it of events. These functions will run without any input from you (the addon developer) but by [ame="http://en.wikipedia.org/wiki/Function_overloading"]overloading[/ame] them we can add additional custom behaviors and states to our vessel.

It should be noted that our vessel already overload ones of Orbiter's default Call Back functions, "clbkSetClassCaps". Without any input from us, Orbiter's core would have simply read whatever data it could from our vessel's config file. This is the principal behind "Config-based vessels" like the Carina as well as SC3's *.ini derived vessels.

Anyway a full list of Orbiter's default callback functions and what they do is included in the API documentation but the one that we are interested in at the moment is "clbkPostStep".

clbkPostStep step is called at the end of every simuation frame (time-step) and is ordinarily used to update a vessel's satus/state. Seeing as animation is essentially nothing more than manipulating the state of a mesh over time one should be able to see how this function could be useful to us.

To overload clbkPostStep go to your vessel's class definition/interface in the header file and add the following line.

Code:
class LM: public VESSEL3 {
public:
	LM (OBJHANDLE hVessel, int flightmodel);
	~LM ();

	// Overloaded callback functions
	void	clbkSetClassCaps (FILEHANDLE cfg);
	[COLOR="red"]void	clbkPostStep(double simt, double simdt, double mjd);[/COLOR]

private:
	MESHHANDLE	mh_descent, mh_ascent;	// Mesh handles
	UINT		mesh_Descent;			// Descent stage mesh index
	UINT		mesh_Ascent;			// Ascent stage mesh index
};
NOTE: I'm using "LM" as the name of my vessel, unless you're planning to nest your vessel within a larger project with it's one directories/file-structure I'd recomend choosing a longer and more distinctive name.

then in your source file, below (but not inside) your "clbkSetClassCaps" add the following...

Code:
	// associate a mesh for the visual
	mesh_Descent	= AddMesh( mh_descent, &LM_DES_OFFSET);
	mesh_Ascent	= AddMesh( mh_ascent, &LM_ASC_OFFSET);	

} // End "LM::clbkSetClassCaps"

[COLOR="red"]// --------------------------------------------------------------
// Manage Animations and Post-Step processes
// --------------------------------------------------------------
void LM::clbkPostStep (double simt, double simdt, double mjd)
{

} // End "LM::clbkPostStep"
[/COLOR]

You have now overloaded clbkPostStep, compile your code to make sure you didn't break anything.

Nothing should happen when you open the simulation because you haven't given clbkPostStep anything to do, but we'll take care of that in a moment.

PART 4: Process Values, Enumerators, and Logic Gates

Once again, as per wikipedia, an Enumerator is a data type consisting of a set of named values called elements. A variable that has been declared as having an enumerated type can be assigned any of the enumerators as a value. In other words, an enumerated type has values that are different from each other, and that can be compared and assigned, but which do not have a particular concrete representation in the computer's memory; compilers and interpreters can represent them arbitrarily. (C++ uses integers, 0,1,2,3,etc...)

For example, the four suits in a deck of playing cards may be a set of four elements named CLUB (0), DIAMOND (1), HEART (2), and SPADE (3), belonging to an enumerated type named "suit". If a variable V is declared having suit as its data type, one can assign any of those four values to it.


A "Process Value" in this case is simply a decimal number representing how far-along a certain process is.

So let's go to our back to our class interface and add both an enumerator and a process value.

Code:
private:
[COLOR="red"]	enum		doorstate {CLOSED, OPEN, CLOSING, OPENING} HatchStatus;
	double		Hatch_Proc;[/COLOR]


	MESHHANDLE	mh_descent, mh_ascent;	// Mesh handles
	UINT		mesh_Descent;			// Descent stage mesh index
	UINT		mesh_Ascent;			// Ascent stage mesh index
};

Now because we are animating the EVA Hatch I named my enumerator's data type "doorstate". and the actual variable that we'll be manipulating "HatchStatus" (the names of the individual elements "CLOSED", "OPEN", etc... should be self-explanitory). "Hatch_Proc" will be our process value.

We are declaring them inside our class interface because by declaring it here we ensure that each instance of our vessel in the simulation will have it's own value for "HatchStatus" and "Hatch_Proc" and make sure that these variables will be visible to any function or sub-function associated with that vessel.

Now let's go back to our "clbkPostStep" in the source file.

The following set of logic gates will control our hatch.
Code:
	// EVA hatch control logic
	if (hatch_proc < 0)			// If process value is less than 0...
	{
		HatchStatus = CLOSED;	// ...set status to "CLOSED"
		hatch_proc = 0;		// and process value to 0
	}

	else if (hatch_proc > 1)	// If process value is greater than 1...
	{
		HatchStatus = OPEN;	// ...set status to "OPEN"
		hatch_proc = 1;		// and process value to 1
	}

	if (HatchStatus > CLOSED)	
	{
		double	delta = simdt / 3;	

		if (HatchStatus == OPENING)		// if Status equals "OPENING"...
		{
			hatch_proc += delta;	// ...add delta to process value
		}
		
		if (HatchStatus == CLOSING)		// if Status equals "CLOSING"...
		{
			hatch_proc -= delta;	// ...subtract it.
		}
	}

	// Debuggery
	sprintf(oapiDebugString(), "Hatch Status %0.0f, hatch_proc %0.3f", (float)HatchStatus, hatch_proc);

I've done my best to label everything with comments but let's break it down anyway...

Code:
	// EVA hatch control logic
	if (hatch_proc < 0)			// If process value is less than 0...
	{
		HatchStatus = CLOSED;	// ...set status to "CLOSED"
		hatch_proc = 0;			// and process value to 0
	}

	else if (hatch_proc > 1)	// If process value is greater than 1...
	{
		HatchStatus = OPEN;		// ...set status to "OPEN"
		hatch_proc = 1;			// and process value to 1
	}

This part is simple, it restricts "hatch_proc" to values between 0 and 1 and sets "HatchStatus" accordingly. In essance what we are doing is setting up hatch_proc as a decimal percentage of "how open" the hatch is. If the hatch is 0% (or less) open "HatchStatus" equals "CLOSED". If the hatch is 100% open then "HatchStatus" equals "OPEN"

Moving on...

Code:
	if (HatchStatus > CLOSED)	
	{
		double	delta = simdt / 3;	

		if (HatchStatus == OPENING)		// if Status equals "OPENING"...
		{
			hatch_proc += delta;			// ...add delta to process value
		}
		
		if (HatchStatus == CLOSING)		// if Status equals "CLOSING"...
		{
			hatch_proc -= delta;	// ...subtract it.
		}
	}

sprintf(oapiDebugString(), "Hatch Status %0.0f, hatch_proc %0.3f", (float)HatchStatus, hatch_proc);
NOTE: "hatch_proc -= delta" is equivelent to writing "hatch_proc = hatch_proc - delta"

Now things get a bit more complex. If the value of "HatchStatus" is greater than "CLOSED" it must be either "OPENING" or "CLOSING". As such we need to modify the process value.

"delta" is how quickly we want the value of hatch_proc to change per simulation frame/time-step. As a general rule this value will be equal to the length of the time-step "simdt" divided by the duration of the animation. (3 seconds, in this example)

From here what we are doing should be apparent. If the hatch is opening, we add "delta" to the process value, if not, we subtract it.

"oapiDebugString" is an orbiter function that prints a string of characters, or set of inputed variables in a white box along the bottom right corner of the simualtion window. It is based on C++'s standard "sprintf" function and follows the same rules for formating.

In this case we will be using it to monitor the values of "HatchStatus" and "hatch_proc" to make sure that our code is performing as predicticted.

lets add our hatch logic to "clbkPostStep" and make sure it compiles.

picture.php

NOTE: My lander is sinking into the Earth because it's still using the ShuttlePB's touchdown points. I should probably do something about that. :rolleyes:

So by looking at the debug string we can see that our hatch logic is in there and that as far as we can tell it is working. However, we have no way to manipulate it.

PART 5: Custom User Inputs

In order to make the hatch open or clsoe we need to be able to TELL it to open or close, this requires overloading yet another call back function. "clbkConsumeBufferedKey" is the call back function that orbiter uses to track key board inputs. By overloading it we can change orbiter's default keyboard comands as well as add new ones.

In this specific case we are going to make so that pressing "K" opens and closes the hatch.

Follow the same steps we did to overload clbkPostStep. declare it in your class interface...

Code:
	// Overloaded callback functions
	void	clbkSetClassCaps (FILEHANDLE cfg);								// Set the capabilities of the vessel class
	void	clbkPostStep(double simt, double simdt, double mjd);			// Manage Animations and Post-Step processes
	[COLOR="Red"]int		clbkConsumeBufferedKey (DWORD key, bool down, char *kstate);	// Process keyboard inputs[/COLOR]
private:

...and then add it to your source file.

Code:
	// Debuggery
	sprintf(oapiDebugString(), "Hatch Status %0.0f, hatch_proc %0.3f", (float)HatchStatus, hatch_proc);

} // End "LM::clbkPostStep"

[COLOR="Red"]// --------------------------------------------------------------
// Process keyboard inputs
// --------------------------------------------------------------
int  LM::clbkConsumeBufferedKey (DWORD key, bool down, char *kstate)
{

	return 0; // if no keys are pressed the function returns '0' and nothing happens
}[/COLOR]

clbkConsumeBufferedKey is an int function and works a bit differently from the voids we've been using thus far. This function must return a value, if it doesn't you will get all sorts of errors and possibly a CTD when you try to compile or run it.

Orbiter recognizes two valid values for clbkConsumeBufferedKey, 0 and 1. As you can see the function is currently returning '0' which Orbiter's core interprets as "Carry on, nothing to see here".

NOTE: it is important that "return 0;" remain the last line in the function. If we tell orbiter "nothing to see here" at the beginning of the function orbiter will assume that there is nothing to see, and as such ignore everything that comes after it.

So let's give it something to see, add the following to clbkConsumeBufferedKey (above the "return 0" line)

Code:
	// Open hatch when [K] is pressed.
	if (key == OAPI_KEY_K  && down && !KEYMOD_SHIFT(kstate) && !KEYMOD_CONTROL (kstate) && !KEYMOD_ALT(kstate)) // [K] is down, no [shift], no [ctrl], no [alt]
	{
		if (HatchStatus == CLOSED) HatchStatus = OPENING;	// If the hatch is closed, open it
		else HatchStatus = CLOSING;							// If not, close it 
		return 1;
	}

Once again, I've done my best to label everything but I'll break it down anyway...

The first (very long) if statement is reading the sate of the keyboard. the K key is being pressed down, the Shift key is not, etc...

Then, if all the above conditions are met we activate the control logic. If the hatch is closed, open it. Otherwise, close it.

Finally, returning a value of "1" tells orbiter "Hey! there's :censored: going on here"

Compile and test...

picture.php


If everything has been assembled correctly the value of HatchStatus should change and hatch_proc start start counting up (or down) when you press the K key. Likewise the amount of time it takes to complete a cycle should match the duration assigned to it back in clbkPostStep.

Mess around with it a bit.

The basic framework we've built here can be used for pretty much anything time-based. From animations like opening a hatch/deploying landing gear, to depressurization of an airlock, to something even more complex like a countdown or ignition sequence.

Any way it's getting late and this post has gone on a lot longer than I expected so I'm calling it a night.

Part 6 will be animations, I promise.

---------- Post added at 11:42 PM ---------- Previous post was at 07:13 AM ----------

Now that we've laid the ground-work let's start putting things in motion.

PART 6: Animations and Mesh Groups

As you should already be aware. Orbiter breaks its meshes into "groups". Orbiter performs Animations by transforming these groups using the "MGROUP_TRANSFORM" class. As such, all parts of the mesh participating in an animation must be defined as separate groups from the vessel proper. Multiple groups can participate in a single animation.

Our hatch animation is going to be comprised of two components. The first is the turning of the hatch handle, the second is the actual opening of the hatch. As such I made sure to define the hatch and it's handle as two groups from the seperate rest of the ascent stage.

Orbiter's "MGROUP_TRANSFORM" class is further divided into three subclasses/functions, "TRANSLATE", "ROTATE", and "SCALE". We will be focusing on ROTATE but further explanation of these functions and how they work can be found in "API_Guide.pdf" located in the OrbiterSDK/doc folder.

Let's get to it...

due to the way Orbiter's Core handles animations a vessel's need to be declared immediatly on creation. Trying to declare them on the fly, after the simulation is already in progess will only lead to instability and sorrow, and bugs. As such we will be declaring our animations from within our vessel's constructor function. (You remember where to find it, yes?)

MGROUP_ROTATE's documentation in the API_Guide gives us the basic format to use but let's review it.

We declare it in much the same way we would declare a variable or call back function only we use "MGROUP_ROTATE" as the keyword, we asign a name to it, and then (in perenthesis) give it the set of values to work with.

These values are...
the mesh we are manipulating.
the specific group/s within that mesh to be manipulated
the number of groups being manipulated
the origin (pivot) point of the rotation relative to the mesh
the axis of rotation
and finally the amount of rotation (in radians)

It is very important that ALL of these values be declared, and in the proper order. Failure to do so (best-case) will cause the animation to not work or (worst-case) cause a CTD.

So lets write a "MGROUP_ROTATE" that will make our hatch open...

Code:
	// EVA Hatch animation
	static UINT HatchGrp	= 8;		// participating groups
	
	static MGROUP_ROTATE	mgt_Hatch ( mesh_Ascent, &HatchGrp, 1, _V( 0.394,-0.578, 1.661), _V( 0.0, 1.0, 0.0),	(float)-90*RAD);
NOTE: Ordinarily Orbiter (and C++ programs in general) will delete/forget anything declared within a function once that function is complete. Under normal circumstances this conserves memory and prevents the generation of multiple conflicting instructions. However, we do not want our animation to be forgotten so we've add a "static" keyword to the front of it so that Orbiter understands "hey this is important, so save it". You should be very careful about where you use "static" because it can cause all sort of messy bugs/crashes if they start to pile up on eachother.

Anyway let's look at our function...

"HatchGrp" is a label that we will be using for our hatch's mesh group (8th group in the ascent stage's mesh).

I've named the rotation function itself "mgt_Hatch" and assigned the following values...

  1. Mesh to be manipulated = mesh_Ascent

    NOTE: this is why I assigned indices to my meshes back in PART 2, I can simply reference the mesh by name rather than having to assign a number value to it or generate a handle on the fly.
  2. Group/s to be manipulated = HatchGrp
  3. Number of groups being manipulated = 1
  4. The origin (pivot) point (x,y,z coordinates) = 0.394,-0.578, 1.661
  5. The axis of rotation = Y (vertical)
  6. Amount of rotation =-90 degrees (convert to radians)

Ok we now have a rotation function but as with our hatch logic back in Part 4 we have no way to control it. To fix this we need to declare an animation in our vessel class and add mgt_Hatch to it as a component.

So lets pop back up to our header file and do so...

Code:
private:
	// Variables
	enum		doorstate {CLOSED, OPEN, CLOSING, OPENING} HatchStatus, GearStatus;
	double		hatch_proc, gear_proc;

[COLOR="Red"]	// Animations
	UINT		anim_Hatch, anim_Gear;[/COLOR]

	// Meshes
	MESHHANDLE	mh_descent, mh_ascent;	// Mesh handles
	UINT		mesh_Descent;			// Descent stage mesh index
	UINT		mesh_Ascent;			// Ascent stage mesh index

}; // End "class LM:"
NOTE: you may have noticed that I've added references to "Gear" as well, I gaven't actually done anything with them yet but i'm planning ahead ;).

Now that our animation is declared return to the constructor and add mgt_hatch to it.

Code:
	// EVA Hatch animation
	static UINT HatchGrp	= 8;	// participating groups
	static MGROUP_ROTATE	mgt_Hatch ( mesh_Ascent, &HatchGrp, 1, _V( 0.394,-0.578, 1.661), _V( 0.0, 1.0, 0.0),	(float)-90*RAD); 
	
[COLOR="red"]	anim_Hatch = CreateAnimation(0);
	AddAnimationComponent ( anim_Hatch, 0.0f, 1.0f, &mgt_Hatch);[/COLOR]

the first line commits anim_Hatch to the vessel and sets it's default (starting) position. NOTE: This position corrisponds to the group's state in the mesh file itself. Our ascent stage mesh show the hatch as being closed when you initially load it therefore the starting position should be 0. If the hatch were open the starting position would be 1.

The second line adds mgt_Hatch to our animation and declares it's start/end points (0 and 1).

We now have a way to control our animation. all that's left now, is to actually control it.

drop down to our hatch logic in "clbkPostStep" and add this line...

Code:
	if (HatchStatus > CLOSED)	
	{
		double	delta = simdt / 3;	

		if (HatchStatus == OPENING)				// if Status equals "OPENING"...
		{
			hatch_proc += delta;				// ...add delta to process value
		}
		
		if (HatchStatus == CLOSING)				// if Status equals "CLOSING"...
		{
			hatch_proc -= delta;				// ...subtract it.
		}

[COLOR="red"]		SetAnimation( anim_Hatch, hatch_proc);	// Apply process value to animation.[/COLOR]
	}

Like the comment says, this will apply our process value to anim_Hatch.

Compile and test...

picture.php


Success! :thumbup:

Now obviously the fact that the handle is just hanging there not attatched to the hatch is a bit of a problem Our next part will cover how till fix this.

For the moment though, we can see that our basic principals are sound.

and because it's new years I have an apartment to clean and a party to go to I'll be signing off for now.
 
Top