Written on September 15th, 2010.

So for my first GSoC (2008) I had the pleasure to work on the Haptic Subsystem for SDL 1.3. Now not is probably familiar with the term Haptic, which is greek for “sense of touch”. In computing this means force feedback. I wrote a small document on SDL Haptic for GSoC 2008 which sort of gives a brief overview on what I did. Now, year being 2010 and SDL 1.3 being closer to release than ever (and that being when “it’s ready”) Sam Lantinga (slouken) has asked me to write up a tutorial.

For this tutorial I’ll be using the code from my pet project NAEV that is the first SDL application ever to use the SDL haptic interface. While it may be daunting I’ll try to lead you through it. Specifically the files we’ll be looking at are joystick.c and spfx.c. The first file is for initializing and loading the force feedback, the latter is for actually running the effects.

If you’ve read the document you’ll see that the basic usage of the force feedback is to:

The first two steps should be done as little as possible, ideally only at initialization, while the playback can be done at anytime. Let us look at the initialization code (from joystick.c):

  1. #include "SDL.h"
  2. /* ... */
  3. #if SDL_VERSION_ATLEAST(1,3,0)
  4. static int has_haptic = 0; /**< Does the player have haptic? */
  5. SDL_Haptic *haptic = NULL; /**< Current haptic in use, externed in spfx.c. */
  6. unsigned int haptic_query = 0; /**< Properties of the haptic device. */
  7. #endif /* SDL_VERSION_ATLEAST(1,3,0) */
  8. /* ... */
  9. /**
  10. * @brief Initializes force feedback for the loaded device.
  11. */
  12. static void joystick_initHaptic (void)
  13. {
  14. #if SDL_VERSION_ATLEAST(1,3,0)
  15. if (has_haptic && SDL_JoystickIsHaptic(joystick)) {
  16. /* Close haptic if already open. */
  17. if (haptic != NULL) {
  18. SDL_HapticClose(haptic);
  19. haptic = NULL;
  20. }
  21. /* Try to create haptic device. */
  22. haptic = SDL_HapticOpenFromJoystick(joystick);
  23. if (haptic == NULL) {
  24. WARN("Unable to initialize force feedback: %s", SDL_GetError());
  25. return;
  26. }
  27. /* Check to see what it supports. */
  28. haptic_query = SDL_HapticQuery(haptic);
  29. if (!(haptic_query & SDL_HAPTIC_SINE)) {
  30. SDL_HapticClose(haptic);
  31. haptic = NULL;
  32. return;
  33. }
  34. DEBUG(" force feedback enabled");
  35. }
  36. #endif /* SDL_VERSION_ATLEAST(1,3,0) */
  37. }

As we can see the code is pretty straight forward. You should note the helper macro SDL provides for detecting if the SDL 1.3 version is available. This macro is fundamental if you want your project to work with both SDL 1.2 and SDL 1.3 like NAEV.

The first thing we do is try to create the haptic device (SDL_Haptic *haptic) from a joystick. This is probably the easiest way to get started. Afterwards we query to see if it supports the SDL_HAPTIC_SINE effect which is what you can consider basic rumble. There are other rumbles but the difference isn’t usually noticeable on most devices. However the sine effect is the most standard and widespread so it’s the one I check for. The joystick_initHaptic function is run shortly after the initialization of the SDL_Joystick *joystick which we won’t discuss here.

The next step is to look at the effect playback. Since we are changing it constantly we keep on updating the effects with different parameters, however we must first upload them. Let’s look at how it’s handled in spfx.c:

  1. #if SDL_VERSION_ATLEAST(1,3,0)
  2. extern SDL_Haptic *haptic; /**< From joystick.c */
  3. extern unsigned int haptic_query; /**< From joystick.c */
  4. static int haptic_rumble = -1; /**< Haptic rumble effect ID. */
  5. static SDL_HapticEffect haptic_rumbleEffect; /**< Haptic rumble effect. */
  6. static double haptic_lastUpdate = 0.; /**< Timer to update haptic effect again. */
  7. #endif /* SDL_VERSION_ATLEAST(1,3,0) */
  8. /* ... */
  9. /**
  10. * @brief Initializes the rumble effect.
  11. *
  12. * @return 0 on success.
  13. */
  14. static int spfx_hapticInit (void)
  15. {
  16. #if SDL_VERSION_ATLEAST(1,3,0)
  17. SDL_HapticEffect *efx;
  18. /* Haptic must be enabled. */
  19. if (haptic == NULL)
  20. return 0;
  21. efx = &haptic_rumbleEffect;
  22. memset( efx, 0, sizeof(SDL_HapticEffect) );
  23. efx->type = SDL_HAPTIC_SINE;
  24. efx->periodic.direction.type = SDL_HAPTIC_POLAR;
  25. efx->periodic.length = 1000;
  26. efx->periodic.period = 200;
  27. efx->periodic.magnitude = 0x4000;
  28. efx->periodic.fade_length = 1000;
  29. efx->periodic.fade_level = 0;
  30. haptic_rumble = SDL_HapticNewEffect( haptic, efx );
  31. if (haptic_rumble < 0) {
  32. WARN("Unable to upload haptic effect: %s.", SDL_GetError());
  33. return -1;
  34. }
  35. #endif /* SDL_VERSION_ATLEAST(1,3,0) */
  36. return 0;
  37. }

This is just the initialization code. As you can see, we’re externing some stuff from joystick.c, which is pretty ugly (this code is old) and I should clean up someday. However the import part of this code is to see how it handles the uploading of a new effect. As you can see we must fill out an effect structure and then try to upload it. Similar to SDL_Event, the SDL_HapticEffect is a union of structs. You must fill out the appropriate effect for the type you want. In our case SDL_HAPTIC_SINE is a periodic effect, so we must access the periodic part of the union. Once we fill out all the magnitudes (and memset to zero it out just in case something gets changed or the likes) we then procede to upload it with SDL_HapticNewEffect. We then make sure it was able to work. This is crucial because some operating systems are very picky when it comes to effects or parameters.

We can look at the reference for the effect to see exactly what everything does:

  1. typedef struct SDL_HapticPeriodic
  2. {
  3. /* Header */
  4. Uint16 type; /**< ::SDL_HAPTIC_SINE, ::SDL_HAPTIC_SQUARE,
  5. ::SDL_HAPTIC_TRIANGLE, ::SDL_HAPTIC_SAWTOOTHUP or
  6. ::SDL_HAPTIC_SAWTOOTHDOWN */
  7. SDL_HapticDirection direction; /**< Direction of the effect. */
  8. /* Replay */
  9. Uint32 length; /**< Duration of the effect. */
  10. Uint16 delay; /**< Delay before starting the effect. */
  11. /* Trigger */
  12. Uint16 button; /**< Button that triggers the effect. */
  13. Uint16 interval; /**< How soon it can be triggered again after button. */
  14. /* Periodic */
  15. Uint16 period; /**< Period of the wave. */
  16. Sint16 magnitude; /**< Peak value. */
  17. Sint16 offset; /**< Mean value of the wave. */
  18. Uint16 phase; /**< Horizontal shift given by hundredth of a cycle. */
  19. /* Envelope */
  20. Uint16 attack_length; /**< Duration of the attack. */
  21. Uint16 attack_level; /**< Level at the start of the attack. */
  22. Uint16 fade_length; /**< Duration of the fade. */
  23. Uint16 fade_level; /**< Level at the end of the fade. */
  24. } SDL_HapticPeriodic;

As you can see you can also have a button trigger it with an interval. This is legacy stuff from old game controllers which may or may not work. There’s also other parameters that do not tend to affect much like for example phase or offset. The most important ones for a periodic effect are: type, length, period, magnitude and the envelope parameters.

You may be asking what the envelope does. We can show that with some simple ASCII art:

    Strength
    ^
    |
    |    effect level -->  _________________
    |                     /                 \
    |                    /                   \
    |                   /                     \
    |                  /                       \ 
    | attack_level --> |                        \
    |                  |                        |  <---  fade_level
    |
    +--------------------------------------------------> Time
                       [--]                 [---]
                       attack_length        fade_length
    
    [------------------][-----------------------]
    delay               length

You may also have confusion on the SDL_HapticDirection. That is something mainly used by proper joysticks and mainly for other effects. As you can see in our example we only set them to SDL_HAPTIC_POLAR and don’t even give it any value (the memset clears the memory so it’s all set to 0). However if you do want to play with them you can read up the doxygen documentation to see how everything is done.

After uploading the effect we must procede to play it, to do this we’ll use the following code (also from spfx.c):

  1. /**
  2. * @brief Runs a rumble effect.
  3. *
  4. * @brief Current modifier being added.
  5. */
  6. static void spfx_hapticRumble( double mod )
  7. {
  8. #if SDL_VERSION_ATLEAST(1,3,0)
  9. SDL_HapticEffect *efx;
  10. double len, mag;
  11. if (haptic_rumble >= 0) {
  12. /* Not time to update yet. */
  13. if ((haptic_lastUpdate > 0.) || shake_off || (mod > SHAKE_MAX/3.))
  14. return;
  15. /* Stop the effect if it was playing. */
  16. SDL_HapticStopEffect( haptic, haptic_rumble );
  17. /* Get length and magnitude. */
  18. len = 1000. * shake_rad / SHAKE_DECAY;
  19. mag = 32767. * (shake_rad / SHAKE_MAX);
  20. /* Update the effect. */
  21. efx = &haptic_rumbleEffect;
  22. efx->periodic.magnitude = (int16_t)mag;
  23. efx->periodic.length = (uint32_t)len;
  24. efx->periodic.fade_length = MIN( efx->periodic.length, 1000 );
  25. if (SDL_HapticUpdateEffect( haptic, haptic_rumble, &haptic_rumbleEffect ) < 0) {
  26. WARN("Failed to update haptic effect: %s.", SDL_GetError());
  27. return;
  28. }
  29. /* Run the new effect. */
  30. SDL_HapticRunEffect( haptic, haptic_rumble, 1 );
  31. /* Set timer again. */
  32. haptic_lastUpdate += HAPTIC_UPDATE_INTERVAL;
  33. }
  34. #else /* SDL_VERSION_ATLEAST(1,3,0) */
  35. (void) mod;
  36. #endif /* SDL_VERSION_ATLEAST(1,3,0) */
  37. }

This code updates the current effect by modulating the magnitude and length parameters so that the vibration varies over time. NAEV does it this way because the vibration is highly dependent on the situation. As the player holds afterburner or gets hit, the magnitude increases and then procedes to slowly decay over time. You could argue that you could do this compositing smaller effects, but I decided to do it this way. Please note that the magnitude is a value from 0 to 32767 while length is in ms. Since we have kept the effect structure we uploaded in memory, we can just modify it and use SDL_HapticUpdateEffect to update it. Afterwards it’s run with SDL_HapticRunEffect once. This will propagate the effect.

You can also notice some NAEV specific code like HAPTIC_UPDATE_INTERVAL and haptic_lastUpdate. It is unwise to update and run the effect every frame, so what we do is use a timer to only update once every HAPTI_UPDATE_INTERVAL. In this case the values are:

  1. #define HAPTIC_UPDATE_INTERVAL 0.1 /**< Time between haptic updates. */
  2. /* ... */
  3. #if SDL_VERSION_ATLEAST(1,3,0)
  4. /* Decrement the haptic timer. */
  5. if (haptic_lastUpdate > 0.)
  6. haptic_lastUpdate -= dt;
  7. #endif /* SDL_VERSION_ATLEAST(1,3,0) */

So we’re using a 100 ms delay between update effects. This ensures we do not saturate the device. You can not guarantee how long an SDL_HapticUpdateEffect can take on any particular device. It should be very fast however since most of the haptic stuff is in software and not actually uploaded to the device’s hardware memory like it used to be.

As with all proper coding, we must now clean up the stuff we have initialized. Although if you destroy a haptic device it will clean up the uploaded effects for you it’s more proper to destroy them first (which I totally don’t do). The following code will be used to clean it all up (joystick.c):

  1. /**
  2. * @brief Exits the joystick subsystem.
  3. */
  4. void joystick_exit (void)
  5. {
  6. #if SDL_VERSION_ATLEAST(1,3,0)
  7. if (haptic != NULL) {
  8. SDL_HapticClose(haptic);
  9. haptic = NULL;
  10. }
  11. #endif /* SDL_VERSION_ATLEAST(1,3,0) */
  12. if (joystick != NULL) {
  13. SDL_JoystickClose(joystick);
  14. joystick = NULL;
  15. }
  16. }

Anyway, I hope those of you who were hesitant to use the SDL 1.3 Haptic capabilities now see things more clearly with a real world example. I recommend you to look at the NAEV source if you are interesting at the full code and especially to read the doxygen documentation which is full of insightful ASCII art.

To see what your device supports, you should use the testhaptic.c which is available when you check out the SDL source code in the test/ directory..

gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.