Professional Documents
Culture Documents
--------------Point Of
How To
View
Control a
SimConnect
Custom
Tutorial
Camera
For Flight
in FSX SDK
Simulator X ---------------
N0801 Tutorial
What is this tutorial for? The tutorial is to understand how we can control a camera
using SimConnect. The camera can be moved using the keyboard or using pre-recorded
paths. This allows to show points of view not accessible with the default cameras.
The image on the left is a standard screenshot from FSX (the external spot view). In
this type of view, the camera is linked to the aircraft (distance and angle), the camera is
centered on the aircraft (the camera is tracking the center of the plane). In addition the
view is always aligned with the horizon, whatever the bank amount of the aircraft.
What do you need to complete this tutorial? MS FSX SDK, which is on the CD
of MS FSX Professional. A compiler, which can be MS Visual Studio C++, C# or VB,
which is available for free at Microsoft.
Suggested pre-read: We wont re-explain in detail how SimConnect clients talk with
FSX, nor what are the basic events sent by SimConnect. This was the goal of the previous
tutorial N0800, Follow This Plane - How to fly AI Aircrafts Using Waypoints in FSX SDK.
Our Goal
The image on the right shows what we can do with SimConnect: we can use the six
degrees of freedom of the camera, i.e. we can move at any distance from the aircraft on
each of the three axes X, Y, Z; and we can rotate the camera any angle around the three
same axes (giving the camera some pitch, bank and heading). This way we can center the
camera on any point, not only the aircraft.
Each default camera has some degrees of freedom frozen or locked to something. Well
create a new camera in the list of global cameras . Well link the camera to the aircraft
and using appropriate commands, place the camera at some distance. Using the same
commands, well orient it. Well change the settings for each frame, according to some
law. This will create a camera movement...
void C A LL B A C K
P ro g ra m S ta rt
M yD isp atch P ro c
(S IM C O N N E C T_ R E C V * pD ata)
C ontinue = true
s w itc h(pD ata->dw ID )
S leep()
C ontinue = = true
{
c a se S IM C O N N EC T_R E C V_ ID _ E XC E P T IO N :
bre a k;
c a se S IM C O N N EC T_R E C V_ ID _ E V EN T :
bre a k;
c a se S IM C O N N EC T_R E C V_ ID _ E V EN T _O BJ E C T _ AD D R EM O V E :
bre a k;
c a se S IM C O N N EC T_R E C V_ ID _ S IM O BJ EC T_ D AT A :
bre a k;
c a se
P ro g ra m E n d
R o u tin e E n d
not. The connection between the server and the clients relies on IP streams and pipes,
but these details are mostly transparent to the clients programmers. The SimConnect
API manages the communication when data transfers are needed. Clients may ask other
clients to be notified of what they send to SimConnect. The FSX simulation engine by itself
is also a SimConnect client. All messages inbound and outbound are processed by the
server based on priorities that may be assigned by clients. The final order for processing
messages of identical priority is managed by SimConnect.
If we want to rotate the camera relatively to the aircraft, we need to provide the desired
values for Heading, Pitch, and Bank. SimConnect provides a function to control the active
camera position:
SimConnect_CameraSetRelative6DOF(
hConnection, X, Y, Z, P, B, H)
You may have noticed that the angle are not provided in a range 0 to 360. Instead there
are given using -180 to 180. For the pitch value, we are even limited to look forward.
To hide this limitation to the user, well need to perform some adjustments when looking
behind.
Classes
We have different tasks to do, i.e. listening to incoming notifications, controlling the
camera position based on users inputs, and playing back the pre-recorded paths. We keep
these functions separate in different classes of C++.
RecordedPath holds a pre-recorded path, its task is to feed the camera with a position
for a given frame number.
If we want to move the camera in the direction of an arrow, the translation amount is a
positive number, it is negative if we want to move in the opposite direction.
PathPlayer manage all required RecordedPath, and ensure all paths are replayed one
after the other.
Camera maintains the current cameras position, taking care of updating it when the user
presses assigned keys, and also requesting position from the PathPlayer when the user
selects the automatic mode.
PointOfView is the entry point with the message loop, and also contains the callback
procedure. The callback procedure transfer non trivial actions to the Camera object.
SnapPbhAdjust = Swivel
PanPbhAdjust = Swivel
SnapPbhReturn = FALSE
PanPbhReturn = FALSE
AllowZoom = Yes
InitialZoom = 2
SmoothZoomTime = 0
ShowWeather = Yes
ShowLensFlare=TRUE
We create this camera because default cameras dont have the criteria we are looking for.
The camera must be an external view, adjustable on its 6 degrees of freedom (swivel),
and tracking no object. To facilitate its positionning relative to the users aircraft, it will
have its origin at the aircrafts center. Well have it displayed in the list of views, at the
root level.
After saving the file and re-starting FSX, you should have a new camera named Moving
Camera. You wont see your airplane first, because the external camera is in the aircraft.
Youll have to move it outside in the direction of the rear (repeat Ctrl + Enter a zillion
times, default motion is sloooow). Alternatively you may create it with a different initial
position InitialXyz = 0, 0, -30.
Cameras can be described at FSX level, at aircraft level and at mission / flight level. Our
camera is at FSX level, available anytime. Locate your cameras.cfg file in
Lets define now what will be our camera keyboard. We use the numeric keypad. We will
define our key mapping in a way that the initial function of the key will be deactivated.
We need three translations and three rotations. in addition well have a key that toggles
on / off the automatic replay of paths:
+X
6
+Bank
Shift+6
-X
4
-Bank
Shift +4
+Y
9
+Head.
Shift +9
-Y
7
-Head.
Shift +7
+Z
8
+Pitch
Shift +8
-Z
2
-Pitch
Shift +2
Replay
5
Well ask SimConnect to notify our client when the user presses these keys. SimConnect
will need to associate an event number of our choice (but unique) with such event, so lets
define our events IDs in PointOfView.cpp:
// clients events triggered by key inputs
enum EVENTS {
EVENT_CAMERA_XP, EVENT_CAMERA_XM,
EVENT_CAMERA_YP, EVENT_CAMERA_YM,
EVENT_CAMERA_ZP, EVENT_CAMERA_ZM,
EVENT_CAMERA_PP, EVENT_CAMERA_PM,
EVENT_CAMERA_BP, EVENT_CAMERA_BM,
EVENT_CAMERA_HP, EVENT_CAMERA_HM,
EVENT_CAMERA_AUTO_TOGGLE,
};
Well also need a notification group ID, the only requirement is still it must be unique
among notification group IDs. Weve talked about these kind of IDs in the first tutorial
N0800 - Follow This Plane, so if youre not confortable, try it before going further with
this one. Well need another group, not for notification of events, but for toggling On / Off
our key access. Our groups:
enum GROUPS {
CAMERA_KEYS,
EVENTS_CAMERA,
};
Client Initialization
We have an initial function called when our client is launched. Itll contain all the code
that is necessary for the initialization:
void PointOfView() { ... }
The first important thing we need in this function is to establish a connection with the
SimConnect server (refer to the first tutorial for details). The connection handle is defined
as a global variable:
HANDLE
hSimConnect = NULL;
This if will contains most of our intialization code. For instance we need to create
private events. Private events are events that are defined by the client, and which are
not mapped (bound) to server events. At some point in time, we need to be inform that
the user has pressed the key for incrementing the pitch, or decrementing the X axis value,
etc. Private events are defined on the server like other events, with a map function, but
we dont provide an argument for the server event mapped. We suffix the name of our
event with P for Plus (increment the value) or M for Minus (decrement):
// Let SimConnect register these private events
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_XP);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_XM);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_YP);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_YM);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_ZP);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_ZM);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_PP);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_PM);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_BP);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_BM);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_HP);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_HM);
SimConnect_MapClientEventToSimEvent(
hSimConnect, EVENT_CAMERA_AUTO_TOGGLE);
These private events need to be given a priority, but this is done by inserting them in
groups, which are given a priority. Well need to provide the ID of a group later, so we
have no choice, we need to include our events in a group:
// include all events into an event notification group
SimConnect_AddClientEventToNotificationGroup(
hSimConnect, EVENTS_CAMERA, EVENT_CAMERA_XP);
SimConnect_AddClientEventToNotificationGroup(
hSimConnect, EVENTS_CAMERA, EVENT_CAMERA_XM);
SimConnect_AddClientEventToNotificationGroup(
hSimConnect, EVENTS_CAMERA, EVENT_CAMERA_YP);
... etc ...
SimConnect_AddClientEventToNotificationGroup(
hSimConnect, EVENTS_CAMERA, EVENT_CAMERA_HM);
SimConnect_AddClientEventToNotificationGroup(
hSimConnect, EVENTS_CAMERA, EVENT_CAMERA_AUTO_TOGGLE);
Now we need to bind the assigned keys with our private events. This way SimConnect will
notify us of the occurence of a private event when the corresponding key is pressed. The
key detection by SimConnect is named an InputEvent. An input event must be assigned
an InputGroup as said earlier, this is done while mapping it to the client private event.
When notifying the client, SimConnect will provide a value. We dont need it, so well ask
to return 0. When mapping a key to a client event, we have actually two events to map,
one when the key is pressed, one when the key is released. We are not interested in the
key released detection, so normally we would just request our mapping this way:
EVENT_CAMERA_AUTO_TOGGLE, 0,
(SIMCONNECT_CLIENT_EVENT_ID)SIMCONNECT_UNUSED, 0,
true);
Lets assign a priority to our input group, and also lets switch it on (when on, the
detection of the keys is allowed):
SimConnect_MapInputEventToClientEvent(
hSimConnect, CAMERA_KEYS, Num_6,
EVENT_CAMERA_XP, 0);
SimConnect_SetInputGroupPriority(
hSimConnect, CAMERA_KEYS, SIMCONNECT_GROUP_PRIORITY_
HIGHEST);
SimConnect_SetInputGroupState(hSimConnect,
CAMERA_KEYS, SIMCONNECT_STATE_ON);
But, because we need to specify the last parameter of the mapping function (maskable),
we are requiered to specificy all the optional parameters before it, and the event ID and
value for the key release are part of them, so we just put here dummy values:
When we will replay a pre-recorded set of positions, well need to take into consideration
the current status of the simulation (paused or unpaused). We need to be notified of pause
events. This is how we subscribe to these notifications (explained in the first tutorial):
SimConnect_MapInputEventToClientEvent(
hSimConnect, CAMERA_KEYS, Num_6,
EVENT_CAMERA_XP, 0,
(SIMCONNECT_CLIENT_EVENT_ID)SIMCONNECT_UNUSED, 0,
true);
maskable = true will instruct SimConnect to stop notififying other clients after us,
The variable pause is a bool member which stores the current state of the simulation.
because we completely take care of this event ourselves. The notification of an event is
done by decreasing priority order. So clients that have associated this input event Num_
6 with a group with a higher priority than our CAMERA_KEYS group will anyway receive
it.
In replay mode, well also need to be informed about the rendering of frames. For each
frame, well provide a new camera position. We also explained how to do in the first
tutorial:
We mask the event here, in the sole purpose of preventing it to be given to FSX itself.
This way FSX will not interpret the key. All the keys are mapped the same way, we dont
provide all the code, just this addional example:
SimConnect_MapInputEventToClientEvent(
hSimConnect, CAMERA_KEYS, Shift+Num_8,
EVENT_CAMERA_PP, 0,
(SIMCONNECT_CLIENT_EVENT_ID)SIMCONNECT_UNUSED, 0,
true);
to illustrate how we define the event input Shift + 8 on the keypad. We finish with our
Start / Stop replay key:
SimConnect_MapInputEventToClientEvent(
hSimConnect, CAMERA_KEYS, Num_5,
The event ID TIMER_FRAME has been added to our existing list of event IDs.
Callback Procedure
We have subscribed to a number of events. SimConnect will send us messages to notify
about some event having occurred. All notifications are passed by SimConnect to the
callback procedure. We put some code here to process these notifications:
void CALLBACK MyDispatchProc(
SIMCONNECT_RECV* pData,
DWORD cbData, void *pContext) {
switch(pData->dwID) {
As already seen in the first tutorial, this procedure is a giant switch based on the value of
pData->dwID which identifies the type of notification received. Well look at these case
in particular:
case
case
case
case
SIMCONNECT_RECV_ID_EVENT:
SIMCONNECT_RECV_ID_EVENT_FRAME:
SIMCONNECT_RECV_ID_EXCEPTION:
SIMCONNECT_RECV_ID_QUIT:
The two last ones are special cases we need to detect errors reported by SimConnect,
and the moment it is terminating. Youll be able to look at the related code in the files,
well only talk about the others cases here. The second is to process the notification of the
rendering of a new frame by the simulation. We put here the code we need to update our
camera position if we are in replay mode.
if (moving && !pause) {
camera.computeAutoForFrame(frame);
camera.sendPosition();
++frame;
if (frame >= maxFrame) {
frame = 0;
}
}
break;
Well see later that moving is a flag to remember when we are in replay mode. pause
is another flag to remember when the simulation is paused. frame is an integer holding
the next frame number to be computed by the replay system. Our Camera object will
contain the code needed to generate the position of the camera at any time during the
replay. The replay itself will demonstrate a number of recorded camera paths, totalizing
a few hundred of frames. When the camera object reaches the end of this sequence, the
sequence is restarted at the beginning.
camera.computeAutoForFrame(frame);
sets the camera position in our client, but doesnt send it to SimConnect. This is done by:
camera.sendPosition();
Well see the code for the Camera class after we have completed our callback procedure
discussion. We already talked about case SIMCONNECT_RECV_ID_EVENT_FRAME,
lets see what we do for case SIMCONNECT_RECV_ID_EVENT. First we cast the
notification data structure to the appropriate one:
SIMCONNECT_RECV_EVENT *evt = (SIMCONNECT_RECV_EVENT*)pData;
and then we identify the event we are notified using switch(evt->uEventID). Well
be notified for any input event that was mapped to one of our private events. For instance,
when the user wants to increase the distance on X axis:
case EVENT_CAMERA_XP:
camera.incX();
camera.sendPosition();
printf(\nCamera X = %f, camera.getcX());
break;
When we are notified, we request the Camera object to compute the position of the
camera after adding some value to the X axis. Then we ask to send this position to
SimConnect.
If the user asks to decrease the X value, well have a similar code:
case EVENT_CAMERA_XM:
camera.decX();
camera.sendPosition();
printf(\nCamera X = %f, camera.getcX());
break;
and actually well have such code for all keys, with a corresponding method for Camera.
If the user presses the replay start / stop key, we register the current state of the replay:
case EVENT_CAMERA_AUTO_TOGGLE:
moving = !moving;
printf(\nMove = %s, moving ? Yes : No);
break;
And this is it! The rest is mostly how to actually do computation of the camera position. It
has been moved to the Camera class and the two other classes it uses.
We delegate to the camera the task of processing users inputs and translating them into
appropriate changes to the camera position. For that we call methods associated with
requests for incrementing or decrementing values:
void
void
void
void
incX(void);
incP(void);
decX(void);
decP(void);
void
void
void
void
incY(void);
incH(void);
decY(void);
decH(void);
void
void
void
void
incZ(void);
incB(void);
decZ(void);
decB(void);
The amount that needs to be used to increment and decrement is also provided to the
camera object:
float dX, dY, dZ; float dP, dH, dB;
void setIncrementsXYZPHB(
float dx, float dy, float dz,
float dp, float dh, float db);
The camera object manages this player and can tell how much frames it can deliver
before restarting to the first one:
int getAutoFrames();
Camera Class
We split our class between a header (camera.h) and a body (camera.cpp) though the
split is not emphasized in this explanation.
The camera object will have to send commands to SimConnect. We also need to store a
connection handle to SimConnect:
HANDLE hConnection;
void setConnection(HANDLE hConnection);
void sendPosition(void);
The camera object needs to store the current position of the camera:
The camera may be requested to calculate the position for any frame number in this
sequence:
void computeAutoForFrame(int frame);
The camera object is created with the default constructor Camera(void). This allows to
create it while we declare it. Later we initialize it by using setConnection(HANDLE
hConnection) which contains only:
this->hConnection = hConnection;
Changing the angles requires an additional step to ensure the values are in the
appropriate range which is -180 +180 for heading and bank:
void Camera::decH(void) { cH -= dH; normalizeInRange180(&cH); }
void Camera::normalizeInRange180(float *angle) {
while (*angle < -180) { *angle += 360; }
while (*angle > 180) { *angle -= 360; }
}
The pitch value has to be managed differently because it has a range -90 to 90 only, and
if when incrementing / decrementing the pitch, we are suddenlty looking backward (i.e.
outside this range), we need to change the heading by 180 and get back the pitch to a
value in the -90 +90 range. To do that we have appropriate functions:
void Camera::incPitch(float delta) {
float inc;
if (cH > -90 && cH <= 90) { inc = delta; }
else { inc = -delta; }
cP += inc;
normalizePitch();
void Camera::normalizePitch() {
// first, normalize pitch to 180
normalizeInRange180(&cP);
// then get it back within -90 +90
The pitch normalization also restores a normal up-down direction by changing the bank
by 180 if the heading has been reversed.
We also use the normalization step after the camera obtains a new position from the path
player, because we allow the player to just increment the current values by some quantity,
without checking itself for staying safely in the allowed interval. The camera always takes
care of this adjustment.
for the first frame (frame 0), the X value would be the initial value 120. for the frame n
(between 1 and 129), X would be equal to 120 + n * -1.0f.
For the outside, the path player has two methods:
int framesCount();
The first method returns the number of frames the player can render. In turn the
player can compute this number by asking each path object the length of its
sequence and summing the lengths. The second method allows the camera object to
request that the player updates the current position to reflect the values for a given
frame. In this case the player has to find which path this frame number belongs to,
and then ask the camera position to the related path object.