Force Feedback

[Previous] [Next]

Force feedback can add a great deal of realism to your 3D game or simulation. In fact, I recommend including it in every 3D application you create. DirectX has supported force feedback since version 5, with the DirectInputEffect interface. Using DirectInput, you can control force-feedback devices. You can use these devices to produce effects such as resistance or vibration when a character or vehicle collides with another prop or when the user presses a button.

In DirectInput, an effect is an instance of movement or resistance over a period of time. A number of standard categories of effects, called forces, are available for use in your applications. Here are some examples:

  • Constant force A steady force exerted in a single direction
  • Ramp force A force that increases or decreases in magnitude
  • Periodic effect A force that pulsates according to a defined wave pattern, such as a sine wave or a square wave
  • SawtoothUp/SawtoothDown force A waveform that rises or drops after reaching a maximum positive or negative force

DirectInput defines the following force types: GUID_ConstantForce, GUID_CustomForce, GUID_Damper, GUID_Friction, GUID_Inertia, GUID_RampForce, GUID_SawtoothDown, GUID_SawtoothUp, GUID_Sine, GUID_Spring, GUID_Square, and GUID_Triangle.

To create a force-feedback_capable device, you need to define an IDirectInputDevice2 interface along with an IDirectInputEffect interface. You need the following variables:

 //------------------------------------------------------------------- // Global variables for the DirectMusic sample  //------------------------------------------------------------------- LPDIRECTINPUT7        g_pDI       = NULL;          LPDIRECTINPUTDEVICE2  g_pJoystick = NULL; LPDIRECTINPUTEFFECT   g_pEffect   = NULL; HINSTANCE             g_hInst     = NULL; BOOL                  g_bActive   = TRUE; int                   g_nXForce; int                   g_nYForce; 

To handle a force-feedback device, you need to add a number of new features to your code. The first (labeled STEP 1) is to modify the call to the IDirectInput7::EnumDevices method in the CMyD3DApplication::CreateDInput routine. In this call, you set the first parameter to DIDEVTYPE_JOYSTICK to restrict the enumeration to just joystick-type devices, as we did for regular joystick enumeration. You also now need to pass the DIEDFL_FORCEFEEDBACK flag to restrict the enumeration further to devices attached to the system that actually support force feedback.

Next you need to disable the autocentering spring of the joystick so that it will be able to properly generate a force (labeled STEP 2). Finally, in STEP 3, you place the call to the routine that creates the force you want to play.

 // Because we'll be playing force-feedback effects, disable the // autocentering spring.     HRESULT hr;          // Create a DInput object.     hr = DirectInputCreateEx( g_hInst, DIRECTINPUT_VERSION,                                IID_IDirectInput7,                               (VOID**)&g_pDI, NULL );     if( FAILED(hr) )          return hr;     // STEP 1         // Look for a usable force-feedback joystick.      hr = g_pDI->EnumDevices( DIDEVTYPE_JOYSTICK,                              EnumFFJoysticksCallback,                              NULL, DIEDFL_ATTACHEDONLY |                               DIEDFL_FORCEFEEDBACK );     if( FAILED(hr) )          return hr;     if( NULL == g_pJoystick )     {         MessageBox( NULL, "Force-feedback joystick not found",                      "DirectInput Sample", MB_ICONERROR | MB_OK );         return E_FAIL;     }     // Set the data format to "simple joystick" - a predefined data      // format. A data format specifies which controls on a device we      // are interested in, and how they should be reported.     //     // This setting tells DirectInput that at some point in the     // application (though not in this sample) it will be passed a      // DIJOYSTATE structure to IDirectInputDevice2::GetDeviceState.     // Setting the data format is important so that the DIJOFS_*     // values work properly.     hr = g_pJoystick->SetDataFormat( &c_dfDIJoystick );     if( FAILED(hr) )          return hr;     // Set the cooperative level to let DInput know how this device      // should interact with the system and with other DInput      // applications. Exclusive access is required to perform force     // feedback.     hr = g_pJoystick->SetCooperativeLevel( hDlg,                                            DISCL_EXCLUSIVE |                                            DISCL_FOREGROUND );     if( FAILED(hr) )          return hr;     // Because we'll be playing force-feedback effects, disable      // the autocentering spring.     DIPROPDWORD dipdw;     dipdw.diph.dwSize       = sizeof(DIPROPDWORD);     dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER);     dipdw.diph.dwObj        = 0;     dipdw.diph.dwHow        = DIPH_DEVICE;     dipdw.dwData            = FALSE;      //     // STEP 2         //     hr = g_pJoystick->SetProperty( DIPROP_AUTOCENTER, &dipdw.diph );     if( FAILED(hr) )          return hr;     //     // STEP 3      //     // Call the routine to create the force-feedback effect here.     //     return S_OK; 

Creating a Force

Magnitude, a force's strength, is measured in units ranging from 0 (no force) to 10,000 (the maximum force for the device). In DirectInput, magnitudes are linear, which means that a force of 5000 is twice as great as one of 2500. A negative value for magnitude will cause a device to exert force in the opposite direction of a positive value. The direction of a force refers to the direction it is exerted from. If you create a force on an axis, it will push along that axis from the positive direction toward the negative.

Each DirectInput effect has a duration that is measured in microseconds. If you define a periodic effect, you must specify its period (the duration of one cycle) in microseconds. The phase of this periodic effect describes the point along the wave at which playback begins. The magnitude of this periodic effect is the force at the peak of the wave.

You can change a force using a wrapper, known in DirectInput as an envelope. This envelope constrains the force in a set of ranges over time. It can define attack values and fade values, which you can use to modify the beginning and ending magnitude of the effect. These attack and fade values have durations that define the time it takes the magnitude to reach or recede from the sustain value, which is the magnitude in the middle portion of the effect.

Creating DirectInput Forces

To add force feedback to your code, you'll need to create the force-feedback effects and the routines to play them back either through a code control or a user hardware event (such as the user pressing a button).

By specifying periodic forces exerted in both the x-axis and the y-axis of a device, you can control joystick devices that have two axes (x and y) and devices with only one axis (x), such as a force-feedback steering wheel. If your code can drive (cause a device to exert a force on) both axes, it can handle any input device; the code can drive one axis for a steering wheel or both axes for a traditional joystick.

This code block shows how to describe two DirectInput effects, m_pPeriodicX and m_pPeriodicY, which are used to create forces for a force-feedback device:

 void UpdatePeriodicType(int);     LPDIRECTINPUTEFFECT m_pPeriodicX;     LPDIRECTINPUTEFFECT m_pPeriodicY;     BOOL CreateEffect(void);     BOOL UpdatePeriodic(DWORD, DWORD); 

You can use the UpdatePeriodicType routine to create DirectInput effects. This routine takes one argument specifying a force type (square, triangle, and so on). The local variables you define include the GUID variable used to request a force type and diEffect, and a DirectInput effect structure used to define a new effect (such as a periodic effect). The first segment of the routine, which appears below, declares these variables.

 void UpdatePeriodicType(int type) {     HRESULT hRes;     GUID tmpguid;     DIEFFECT diEffect;     DIPERIODIC diPer;     DWORD rgdwAxes[2];     LONG rglDirection[2]; 

The DirectInput DIEFFECT structure you use is defined as:

 typedef struct DIEFFECT {      DWORD dwSize;      DWORD dwFlags;      DWORD dwDuration;      DWORD dwSamplePeriod;      DWORD dwGain;      DWORD dwTriggerButton;      DWORD dwTriggerRepeatInterval;     DWORD cAxes;      LPDWORD rgdwAxes;      LPLONG rglDirection;      LPDIENVELOPE lpEnvelope;      DWORD cbTypeSpecificParams;      LPVOID lpvTypeSpecificParams;      DWORD  dwStartDelay;  } DIEFFECT, *LPDIEFFECT;    typedef const DIEFFECT *LPCDIEFFECT; 

The DIEFFECT structure has the following members.

dwSize Specifies the size, in bytes, of the structure. This member must be initialized before the structure is used.

dwFlags Flags associated with the effect. This value can be a combination of one or more of the following values:

  • DIEFF_CARTESIAN The values of rglDirection are to be interpreted as Cartesian coordinates.
  • DIEFF_OBJECTIDS The values of dwTriggerButton and rgdwAxes are object identifiers as obtained by IDirectInputDevice7::EnumObjects.
  • DIEFF_OBJECTOFFSETS The values of dwTriggerButton and rgdwAxes are data format offsets, relative to the data format selected by IDirectInputDevice7::SetDataFormat.
  • DIEFF_POLAR The values of rglDirection are to be interpreted as polar coordinates.
  • DIEFF_SPHERICAL The values of rglDirection are to be interpreted as spherical coordinates.

dwDuration The total duration of the effect, in microseconds. If this value is INFINITE, the effect has infinite duration. If an envelope has been applied to the effect, the attack is applied, followed by an infinite sustain.

dwSamplePeriod The period at which the device should play back the effect, in microseconds. A value of 0 indicates that the default playback sample rate should be used. If the device isn't capable of playing back the effect at the specified rate, it chooses the supported rate that is closest to the requested value. Setting a custom dwSamplePeriod can be used for special effects. For example, playing a sine wave at an artificially large sample period results in a rougher texture.

dwGain The gain to be applied to the effect, in the range from 0 through 10,000. The gain is a scaling factor applied to all magnitudes of the effect and its envelope.

dwTriggerButton The identifier or offset of the button to be used to trigger playback of the effect. The flags DIEFF_OBJECTIDS and DIEFF_OBJECTOFFSETS determine the semantics of the value. If this member is set to DIEB_NOTRIGGER, no trigger button is associated with the effect.

dwTriggerRepeatInterval The interval, in microseconds, between the end of one playback and the start of the next when the effect is triggered by a button press and the button is held down. Setting this value to INFINITE suppresses repetition. Support for trigger repeat for an effect is indicated by the presence of the DIEP_TRIGGERREPEATINTERVAL flag in the dwStaticParams member of the DIEFFECTINFO structure.

cAxes The number of axes involved in the effect. This member must be filled in by the caller if changing or setting the axis list or the direction list. Once it's been set, the number of axes for an effect can't be changed.

rgdwAxes Pointer to a DWORD array (of cAxes elements) containing identifiers or offsets identifying the axes to which the effect is to be applied. The flags DIEFF_OBJECTIDS and DIEFF_OBJECTOFFSETS determine the semantics of the values in the array. Once it's been set, the list of axes associated with an effect can't be changed. No more than 32 axes can be associated with a single effect.

rglDirection Pointer to a LONG array (of cAxes elements) containing either Cartesian coordinates or polar coordinates. The flags DIEFF_CARTESIAN, DIEFF_POLAR, and DIEFF_SPHERICAL determine the semantics of the values in the array.

If Cartesian, each value in rglDirection is associated with the corresponding axis in rgdwAxes. If polar, the angle is measured in hundredths of degrees from the (0, _1) direction, rotated in the direction of (1, 0). This usually means that north is away from the user and east is to the user's right. The last element isn't used. If spherical, the first angle is measured in hundredths of a degree from the (1, 0) direction, rotated in the direction of (0, 1). The second angle (if the number of axes is three or more) is measured in hundredths of a degree toward (0, 0, 1). The third angle (if the number of axes is four or more) is measured in hundredths of a degree toward (0, 0, 0, 1), and so on. The last element isn't used.

NOTE
The rglDirection array must contain cAxes entries, even if polar or spherical coordinates are given. In these cases, the last element in the rglDirection array is reserved for future use and must be 0.

lpEnvelope Optional pointer to a DIENVELOPE structure that describes the envelope to be used by this effect. Not all effect types use envelopes. If no envelope is to be applied, the member should be set to NULL.

cbTypeSpecificParams Number of bytes of additional type-specific parameters for the corresponding effect type.

lpvTypeSpecificParams Pointer to type-specific parameters, or NULL if there are no type-specific parameters. If the effect is of type DIEFT_CONDITION, this member contains a pointer to an array of DICONDITION structures that define the parameters for the condition. A single structure can be used, in which case the condition is applied in the direction specified in the rglDirection array. Otherwise, there must be one structure for each axis, in the same order as the axes in the rgdwAxes array. If a structure is supplied for each axis, the effect should not be rotated; you should use the following values in the rglDirection array:

  • DIEFF_SPHERICAL: 0, 0, ...
  • DIEFF_POLAR: 9000, 0, ...
  • DIEFF_CARTESIAN: 1, 0, ...

If the effect is of type DIEFT_CUSTOMFORCE, this member contains a pointer to a DICUSTOMFORCE structure that defines the parameters for the custom force.

If the effect is of type DIEFT_PERIODIC, this member contains a pointer to a DIPERIODIC structure that defines the parameters for the effect. If the effect is of type DIEFT_CONSTANTFORCE, this member contains a pointer to a DICONSTANTFORCE structure that defines the parameters for the constant force. If the effect is of type DIEFT_RAMPFORCE, this member contains a pointer to a DIRAMPFORCE structure that defines the parameters for the ramp force.

dwStartDelay Time (in microseconds) that the device should wait after an IDirectInputEffect::Start call before playing the effect. If this value is 0, effect playback begins immediately. This member isn't present in versions prior to DirectX 7.

After specifying the variables, you can fill in the periodic force structure, defined as follows:

 typedef struct DIPERIODIC {      DWORD dwMagnitude;      LONG  lOffset;      DWORD dwPhase;      DWORD dwPeriod;  } DIPERIODIC, *LPDIPERIODIC;    typedef const DIPERIODIC *LPCDIPERIODIC; 

The values you need to be concerned with follow.

dwMagnitude As you just learned, the magnitude of an effect ranges from 0 to 10,000. If no envelope is applied, this value will describe the amplitude of the effect. If you choose to apply an envelope to an effect (as shown in Figure 7-1), dwMagnitude defines the effect's sustain value.

click to view at full size.

Figure 7-1 DirectInput envelope

lOffset This value specifies the amount the waveform is shifted up or down from the base level.

dwPhase As mentioned earlier, this is the point along the wave at which playback begins.

dwPeriod This value marks the duration of one cycle, in microseconds.

This is the code for setting up a periodic force:

 // Initialize DIEFFECT and DIENVELOPE structures.     ZeroMemory(&diEffect, sizeof(DIEFFECT));     //     // Prepare the DIPERIODIC structure.     //     // This is the type-specific data for this force.     diPer.dwMagnitude = 5000;      diPer.lOffset     = 0;      diPer.dwPhase     = 0;      diPer.dwPeriod    = 100000; //10Hz 

Once you've defined these parameters, you can fill the DirectInput Effect structure used to create a force-feedback effect. The dwSize field of this structure describes its size in bytes. The dwSamplePeriod field defines the period in the number of microseconds in which the device will play back the effect. By specifying the value 0, you're requesting that the system use the default playback sample rate. The dwTriggerButton member indicates the trigger button you want to use. In the code segment that follows, the DIEB_NOTRIGGER value indicates that no trigger button will be associated with this effect. The final variable that this code segment sets, the dwTriggerRepeatInterval member of the structure, defines the interval (in microseconds) between the end of one playback and the beginning of the next when the effect is triggered by a button press and the user holds down the button.

 // Prepare the DIEFFECT structure.     //     // Fill in the force-specific values.     // These fields are the same for all effects you'll be creating.     diEffect.dwSize                     = sizeof(DIEFFECT);     // Use the default sample period.     diEffect.dwSamplePeriod             = 0;          diEffect.dwTriggerButton            = DIEB_NOTRIGGER;     diEffect.dwTriggerRepeatInterval    = 0; 

The rgdwAxes field specifies the axes you'll use. Both axes are used for a joystick, and one—the x-axis—is used for a steering wheel. The direction of an effect refers to the direction that it originates from. For example, if an effect has a direction along the negative y-axis (defined in the rglDirection field of the DIEFFECT structure), the effect will push the joystick toward the user along the positive yaxis. The dwGain field defines a gain between 0 and 10,000 that will be applied to the effect. This gain is a scaling factor that's applied to all magnitudes of the effect and its envelope. The last field, the dwFlags field, is set to a combination of the DIEFF_CARTESIAN and DIEFF_OBJECTOFFSETS flags. The DIEFF_CARTESIAN flag requests that values in the rglDirection field be interpreted as Cartesian coordinates. The DIEFF_OBJECTOFFSETS flag indicates that the values of the dwTriggerButton and rgdwAxes fields be used as data format offsets that are relative to the data format requested by a call to IDirectInput::SetDataFormat.

 // Which axes and directions to use?     rgdwAxes[0]                     = DIJOFS_X;     rgdwAxes[1]                     = DIJOFS_Y;     diEffect.rgdwAxes               = rgdwAxes;     rglDirection[0]                 = 0;     rglDirection[1]                 = 1;     diEffect.rglDirection           = rglDirection;     diEffect.dwGain                 = 10000;      diEffect.dwFlags                = DIEFF_OBJECTOFFSETS |                                       DIEFF_CARTESIAN; 

The last values you must set in the effect's structure are dwDuration, cAxes, lpEnvelope, cbTypeSpecificParams, and lpvTypeSpecificParams. The dwDuration field describes the duration of an effect in microseconds. If you set it to a value of INFINITE, the effect will occur infinitely. The cAxes field defines the number of axes in the effect. For a force-feedback steering wheel, you'd use only the x-axis. The lpEnvelope field defines the effect's envelope. By specifying NULL, you won't need an envelope for the effect you're creating. The cbTypeSpecificParams field defines the number of bytes used for the effect. The lpvTypeSpecificParams field defines a pointer to the type-specific parameters, contained in the DIPERIODIC structure specified earlier.

 diEffect.dwDuration                 = INFINITE;     diEffect.cAxes                      = 2;     diEffect.lpEnvelope                 = NULL;     diEffect.cbTypeSpecificParams       = sizeof(DIPERIODIC);     diEffect.lpvTypeSpecificParams      = &diPer; 

In the next segment of code, you'll set the GUID for the effect type, selecting from the types we discussed earlier:

 switch(type)     {         case 0:             tmpguid = GUID_Sine;             break;         case 1:             tmpguid = GUID_Square;             break;         case 2:             tmpguid = GUID_Triangle;             break;         case 3:             tmpguid = GUID_SawtoothUp;             break;         case 4:             tmpguid = GUID_SawtoothDown;             break;         default:             tmpguid = GUID_Sine;     }     if (m_pPeriodicY)         m_pPeriodicY->Release();     if (m_pPeriodicX)         m_pPeriodicX->Release(); 

Now that you've defined your forces in the x and y directions, you have only one more step to perform. Call the IDirectInputDevice2::CreateEffect method to create and initialize two effects that are identified by the GUID selected in the previous case statement:

 //         // Call CreateEffect.     //     hRes = g_pJoystick->CreateEffect(tmpguid, &diEffect,                                      &m_pPeriodicY, NULL);     if(FAILED(hRes))     {         OutputDebugString("CreateEffect(VibrationY) failed\n");     }     rgdwAxes[0]                       = DIJOFS_X;     rgdwAxes[1]                       = DIJOFS_Y;     rglDirection[0]                   = 1;     rglDirection[1]                   = 0;     //     // Call CreateEffect.     //     hRes = g_pJoystick->CreateEffect(tmpguid, &diEffect,                                      &m_pPeriodicX, NULL);     if(FAILED(hRes))     {         OutputDebugString("CreateEffect(VibrationX) failed\n");     } } 

The SetAcquire routine determines whether the application is active; if the application is active, this routine calls the IDirectInputDevice2::Acquire method to gain access to the device. Once you have access to the device, you can call the IDirectInputEffect::Start method.

The IDirectInputEffect::Start method is used to start playing an effect. If the effect happens to already be playing, it will be started again from the beginning. DirectInput devices need to have the effects we want played downloaded to them initially. So if the effect hasn't yet been downloaded to the device, or if it's been modified since it was last downloaded, it is downloaded first. This method is defined as follows:

 HRESULT Start(     DWORD dwIterations,      DWORD dwFlags,  ); 

The IDirectInputEffect::Start method has the following parameters.

dwIterations Number of times to play the effect in sequence. The envelope is rearticulated with each iteration. To play the effect exactly once, pass 1. To play the effect repeatedly until explicitly stopped, pass INFINITE. To play the effect until explicitly stopped without rearticulating the envelope, modify the effect parameters with the IDirectInputEffect::SetParameters method and change the dwDuration member to INFINITE.

dwFlags Flags that describe how the effect should be played by the device. The value can be 0 or one or more of the following values:

  • DIES_SOLO All other effects on the device should be stopped before the specified effect is played. If this flag is omitted, the effect is mixed with existing effects already started on the device.
  • DIES_NODOWNLOAD Do not automatically download the effect.

The SetAcquire function is defined as follows.

 //------------------------------------------------------------------- // Name: SetAcquire // Desc: Acquire or unacquire the mouse, depending on whether the // application is active. The input device must be acquired before // GetDeviceState is called. //------------------------------------------------------------------- HRESULT SetAcquire( HWND hDlg ) {     if( NULL == g_pJoystick )         return S_FALSE;     if( g_bActive )      {         // Acquire the input device.          g_pJoystick->Acquire();         if( g_pEffect )              g_pEffect->Start( 1, 0 ); // Start the effect.     }      else      {         // Unacquire the input device.          g_pJoystick->Unacquire();     }     return S_OK; } 

If you want to play forces to both the x and y axes rather than on just one axis, you would perform the following:

 BOOL PlayVibration() {     HRESULT hRes;     if(m_bYAxisOn)         hRes = m_pPeriodicY->Start(1,0);          if (FAILED(hRes))         return FALSE;     if(m_bXAxisOn)         hRes = m_pPeriodicX->Start(1,0);              if (FAILED(hRes))         return FALSE;         return TRUE;     UpdatePeriodic(1); } 

The last routine you'll need to define is one to modify the effect you create so that you can vary the force-feedback effect that is produced. Doing this is basically the same as producing a new effect. You just set the parameters of the effect to the new settings you want and call the IDirectInputEffect::SetParameters method to set the effect to the new values.

 //------------------------------------------------------------------- // Name: SetJoyForcesXY // Desc: Apply the X and Y forces to the effect we prepared. //------------------------------------------------------------------- HRESULT SetJoyForcesXY() {     // Modifying an effect is basically the same as creating a new      // one except that you need to specify only the parameters you're     // modifying.     LONG rglDirection[2] = { g_nXForce, g_nYForce };     DICONSTANTFORCE cf;     cf.lMagnitude =         (DWORD)sqrt( (double)g_nXForce * (double)g_nXForce +                      (double)g_nYForce * (double)g_nYForce );     DIEFFECT eff;     eff.dwSize                = sizeof(DIEFFECT);     eff.dwFlags               = DIEFF_CARTESIAN |                                 DIEFF_OBJECTOFFSETS;     eff.cAxes                 = 2;     eff.rglDirection          = rglDirection;     eff.lpEnvelope            = 0;     eff.cbTypeSpecificParams  = sizeof(DICONSTANTFORCE);     eff.lpvTypeSpecificParams = &cf;     // Now set the new parameters and start the effect immediately.     return g_pEffect->SetParameters( &eff, DIEP_DIRECTION |                                      DIEP_TYPESPECIFICPARAMS |                                      DIEP_START ); } 



Inside Direct3D
Inside Direct3D (Dv-Mps Inside)
ISBN: 0735606137
EAN: 2147483647
Year: 1999
Pages: 131

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net