Let's face it. Rumble and XNA haven't had the best history. I still hear people refer to "massage games" and such when people talk about Xbox Live Indies, but there are many out there who can now look past the sea of undesirables and see the potential and successful. It's come a long way since it's innocent beginnings. Yet many developers still shy away from utilizing force feedback and some that do, use it in a brute force kind of way that can annoy the player. Even some AAA titles seem to just set the motors at full blast for three seconds when you crash off the road. Ugh. When it comes to rumble, subtle is the way to go.
Before I go on, I'd like to say that the following code is free to use in your game and is licensed under the Microsoft Permissive License with one caveat. You can not use this component in a game where the sole purpose is to vibrate the controller. You cannot use this for a "massage game" or any other where the purpose of the application is rumble based. You can only use it to enhance existing gameplay where otherwise, not having rumble will not change the objective of the game. Hate that I have to say it, but it needs to be said.
Here I've implemented a time dependent, dynamic based rumble component. I've played many indie games that accidentally leave the motors running until I quit to the dashboard. You can get around this by using time based triggers. When an event happens that requires force feedback (say a collision) just add a new rumble for a set amount of time and when the times up, the rumble stops.
To make things subtle I use the idea of dynamics. Since sound is a vibration, we can use it as an example in which to compare. In music, dynamics are control over the volume of the sound, or intensity of the vibration. By varying the dynamics over time, I could apply a "shape" to the controller's rumble. I've added the ability to change to different rumble shapes by calling them by name, such as binary (on/off), linear (start at minimum and increase to maximum over time) or parabolic (fade in to maximum by time/2 and fade out to minimum by time).
Before I go on, I'd like to say that the following code is free to use in your game and is licensed under the Microsoft Permissive License with one caveat. You can not use this component in a game where the sole purpose is to vibrate the controller. You cannot use this for a "massage game" or any other where the purpose of the application is rumble based. You can only use it to enhance existing gameplay where otherwise, not having rumble will not change the objective of the game. Hate that I have to say it, but it needs to be said.
Here I've implemented a time dependent, dynamic based rumble component. I've played many indie games that accidentally leave the motors running until I quit to the dashboard. You can get around this by using time based triggers. When an event happens that requires force feedback (say a collision) just add a new rumble for a set amount of time and when the times up, the rumble stops.
To make things subtle I use the idea of dynamics. Since sound is a vibration, we can use it as an example in which to compare. In music, dynamics are control over the volume of the sound, or intensity of the vibration. By varying the dynamics over time, I could apply a "shape" to the controller's rumble. I've added the ability to change to different rumble shapes by calling them by name, such as binary (on/off), linear (start at minimum and increase to maximum over time) or parabolic (fade in to maximum by time/2 and fade out to minimum by time).
I'll show the core mechanics here and you can download the code at the bottom of this post. It's well commented so you shouldn't haven't any problems following along. The component uses a reusable pool of RumbleInstance objects to draw from when a request to shake the controller is made. Here is the RumbleInstance.
struct RumbleInstance
{
//Control over the motors and for how long.
public float LeftMotorAmount;
public float RightMotorAmount;
public float TimeLeft;
public PlayerIndex playerIndex;
//This instance's state members. These define if it is alive,
//for how long, and the shape of the rumble.
public bool IsAlive { get; private set; }
public Rumble.Shape Shape { get; private set; }
public float Time { get; private set; }
//This initializes this instance with these attributes.
public void Rumble(
PlayerIndex playerIndex,
float leftMotorAmount,
float rightMotorAmount,
float rumbleTime,
Rumble.Shape Shape)
{
if (rumbleTime <= 0)
throw new Exception("rumbleTime must be greater than zero");
IsAlive = true;
LeftMotorAmount = MathHelper.Clamp(leftMotorAmount, 0 ,1);
RightMotorAmount = MathHelper.Clamp(rightMotorAmount, 0, 1);
TimeLeft = rumbleTime;
Time = rumbleTime;
this.playerIndex = playerIndex;
this.Shape = Shape;
}
//Set this instance to null values.
public void Kill()
{
IsAlive = false;
LeftMotorAmount = 0;
RightMotorAmount = 0;
TimeLeft = 0;
Time = 0;
}
}
{
//Control over the motors and for how long.
public float LeftMotorAmount;
public float RightMotorAmount;
public float TimeLeft;
public PlayerIndex playerIndex;
//This instance's state members. These define if it is alive,
//for how long, and the shape of the rumble.
public bool IsAlive { get; private set; }
public Rumble.Shape Shape { get; private set; }
public float Time { get; private set; }
//This initializes this instance with these attributes.
public void Rumble(
PlayerIndex playerIndex,
float leftMotorAmount,
float rightMotorAmount,
float rumbleTime,
Rumble.Shape Shape)
{
if (rumbleTime <= 0)
throw new Exception("rumbleTime must be greater than zero");
IsAlive = true;
LeftMotorAmount = MathHelper.Clamp(leftMotorAmount, 0 ,1);
RightMotorAmount = MathHelper.Clamp(rightMotorAmount, 0, 1);
TimeLeft = rumbleTime;
Time = rumbleTime;
this.playerIndex = playerIndex;
this.Shape = Shape;
}
//Set this instance to null values.
public void Kill()
{
IsAlive = false;
LeftMotorAmount = 0;
RightMotorAmount = 0;
TimeLeft = 0;
Time = 0;
}
}
In the Rumble class, inherit from GameComonent. I make a Shape enum with the names of the shapes the rumble can take, like linear, SmoothStep, etc... and a RumbleInstance array that will be used for the pool. After the constructor, add this for the update.
public override void Update(GameTime gameTime)
{
for (int i = 0; i < rumblePool.Length; i++)
{
if (rumblePool[i].IsAlive)
{
//Reduce the time left to vibrate the motors,
//but don't go less than zero.
rumblePool[i].TimeLeft =
MathHelper.Clamp(
rumblePool[i].TimeLeft -
(float)gameTime.ElapsedGameTime.TotalSeconds,
0,
rumblePool[i].Time);
//Reduce or shape the "curve" of the vibration.
Vector2 motorSpeeds = ShapeRumble(rumblePool[i]);
if (rumblePool[i].TimeLeft <= 0)
{
rumblePool[i].Kill(); //Reset the rumble properties.
motorSpeeds = Vector2.Zero; //Stop the active motors.
}
GamePad.SetVibration(
rumblePool[i].playerIndex, motorSpeeds.X, motorSpeeds.Y);
}
}
base.Update(gameTime);
}
{
for (int i = 0; i < rumblePool.Length; i++)
{
if (rumblePool[i].IsAlive)
{
//Reduce the time left to vibrate the motors,
//but don't go less than zero.
rumblePool[i].TimeLeft =
MathHelper.Clamp(
rumblePool[i].TimeLeft -
(float)gameTime.ElapsedGameTime.TotalSeconds,
0,
rumblePool[i].Time);
//Reduce or shape the "curve" of the vibration.
Vector2 motorSpeeds = ShapeRumble(rumblePool[i]);
if (rumblePool[i].TimeLeft <= 0)
{
rumblePool[i].Kill(); //Reset the rumble properties.
motorSpeeds = Vector2.Zero; //Stop the active motors.
}
GamePad.SetVibration(
rumblePool[i].playerIndex, motorSpeeds.X, motorSpeeds.Y);
}
}
base.Update(gameTime);
}
This counts down the time left in each active instance, sets the motor speed based on the shape, and kills the instance when time is up. The "ShapeRumble" method is what defines the strength of the motor over time. All it does is call a switch statement on the Shape enum which in turn calls the appropriate method to shape the vibration. Here are some examples of these methods.
private Vector2 Linear(RumbleInstance rumble)
{
//Start rumble at zero and linearly increase force feedback
//over the length of time until motorAmount is reached.
Vector2 motorSpeeds = Vector2.Zero;
motorSpeeds.X = MathHelper.Lerp(0, rumble.LeftMotorAmount, 1 - (rumble.TimeLeft / rumble.Time));
motorSpeeds.Y = MathHelper.Lerp(0, rumble.RightMotorAmount, 1 - (rumble.TimeLeft / rumble.Time));
return motorSpeeds;
}
private Vector2 Exponential(RumbleInstance rumble)
{
//Start rumble at zero and exponentially increase force feedback
//over the length of time until motorAmount is reached.
Vector2 motorSpeeds = Vector2.Zero;
float amount = (float)Math.Pow(1 - (rumble.TimeLeft / rumble.Time), 2);
motorSpeeds.X = MathHelper.Lerp(0, rumble.LeftMotorAmount, amount);
motorSpeeds.Y = MathHelper.Lerp(0, rumble.RightMotorAmount, amount);
return motorSpeeds;
}
private Vector2 Parabolic(RumbleInstance rumble)
{
//Start rumble at zero and gradually increase force feedback.
//When rumble.Time / 2.0 is reached motors will be at motorAmount.
//Then, gradually reduce force feedback back to zero.
Vector2 motorSpeeds = Vector2.Zero;
//Use the parabolic equation: 4x(1-x)
//This gives us a curve that starts at zero when x = 0,
//reaches its apex at x = 0.5, and ends at zero when x = 1.
float amount = 1 - (rumble.TimeLeft / rumble.Time);
amount = 4 * amount * (1 - amount);
motorSpeeds.X = MathHelper.Lerp(0, rumble.LeftMotorAmount, amount);
motorSpeeds.Y = MathHelper.Lerp(0, rumble.RightMotorAmount, amount);
return motorSpeeds;
}
{
//Start rumble at zero and linearly increase force feedback
//over the length of time until motorAmount is reached.
Vector2 motorSpeeds = Vector2.Zero;
motorSpeeds.X = MathHelper.Lerp(0, rumble.LeftMotorAmount, 1 - (rumble.TimeLeft / rumble.Time));
motorSpeeds.Y = MathHelper.Lerp(0, rumble.RightMotorAmount, 1 - (rumble.TimeLeft / rumble.Time));
return motorSpeeds;
}
private Vector2 Exponential(RumbleInstance rumble)
{
//Start rumble at zero and exponentially increase force feedback
//over the length of time until motorAmount is reached.
Vector2 motorSpeeds = Vector2.Zero;
float amount = (float)Math.Pow(1 - (rumble.TimeLeft / rumble.Time), 2);
motorSpeeds.X = MathHelper.Lerp(0, rumble.LeftMotorAmount, amount);
motorSpeeds.Y = MathHelper.Lerp(0, rumble.RightMotorAmount, amount);
return motorSpeeds;
}
private Vector2 Parabolic(RumbleInstance rumble)
{
//Start rumble at zero and gradually increase force feedback.
//When rumble.Time / 2.0 is reached motors will be at motorAmount.
//Then, gradually reduce force feedback back to zero.
Vector2 motorSpeeds = Vector2.Zero;
//Use the parabolic equation: 4x(1-x)
//This gives us a curve that starts at zero when x = 0,
//reaches its apex at x = 0.5, and ends at zero when x = 1.
float amount = 1 - (rumble.TimeLeft / rumble.Time);
amount = 4 * amount * (1 - amount);
motorSpeeds.X = MathHelper.Lerp(0, rumble.LeftMotorAmount, amount);
motorSpeeds.Y = MathHelper.Lerp(0, rumble.RightMotorAmount, amount);
return motorSpeeds;
}
The only thing left now is to add a public method that allows us to add a rumble. Here it is.
/// <summary>
/// Add a rumble with a predefined shape.
/// </summary>
/// <param name="playerIndex">The player controller to rumble.</param>
/// <param name="leftMotor">The maximum value of the
/// left motor, between 0.0f and 1.0f.</param>
/// <param name="rightMotor">The maximum value of the right motor, between 0.0f and 1.0f.</param>
/// <param name="time">The amount of time to rumble the motor in seconds.</param>
/// <param name="shape">The shape of the rumble.</param>
public void AddRumble(PlayerIndex playerIndex, float leftMotor, float rightMotor, float time, Shape shape)
{
//Find the first unused rumble instance and use it.
//If one is not available, rumble will not be set.
for (int i = 0; i < rumblePool.Length; i++)
{
if (!rumblePool[i].IsAlive)
{
//Use this instance of a rumble and break.
rumblePool[i].Rumble(playerIndex, leftMotor, rightMotor, time, shape);
break;
}
}
}
/// Add a rumble with a predefined shape.
/// </summary>
/// <param name="playerIndex">The player controller to rumble.</param>
/// <param name="leftMotor">The maximum value of the
/// left motor, between 0.0f and 1.0f.</param>
/// <param name="rightMotor">The maximum value of the right motor, between 0.0f and 1.0f.</param>
/// <param name="time">The amount of time to rumble the motor in seconds.</param>
/// <param name="shape">The shape of the rumble.</param>
public void AddRumble(PlayerIndex playerIndex, float leftMotor, float rightMotor, float time, Shape shape)
{
//Find the first unused rumble instance and use it.
//If one is not available, rumble will not be set.
for (int i = 0; i < rumblePool.Length; i++)
{
if (!rumblePool[i].IsAlive)
{
//Use this instance of a rumble and break.
rumblePool[i].Rumble(playerIndex, leftMotor, rightMotor, time, shape);
break;
}
}
}
Now in your main game you can add a rumble class to the list of components. When the time comes to add a rumble sting, simply call the "AddRumble" method. The component class will clean everything up for you. Just be sure to call the method during an event specific time and not in a loop as it will continue to add new instances every time it's called. The full implementation is below, complete with a simple demonstration to show you how it works. The license is included in the zip. Let me know what you think or if you found this useful in the comments below.
rumblecomponentexample.zip |