ZunepadDaemon
So when I realized that the zune is just hacked xbox controller I decided it was time for a better way. I wrote the following code in order to wrap stuff up nicely and create an event based driven system. I call it the ZunepadDaemon, and it is capable of monitoring clicks (up and down), touches (presses and releases), flicks and taps. It is a work in progress but it is to the point where it seems to work pretty well. It is a lot of code, but the great thing is that it is a GameComponent, so you can just add it to your game's component collection and enable it and you're good to go! You should be able to copy and paste this code and use it right away, I made sure all of the code compiles. ZunepadDaemon Tips for how to use this code.
Code written by Jonathan Knuth.
/// <summary>
/// A class that constantly monitors the zune inputs.
/// </summary>
public class ZunepadDaemon : IUpdateable, IGameComponent
{
/// <summary>
/// This number was determined by testing; same as below
/// </summary>
private static readonly int FLICK_QUEUE_SIZE = 4;
/// <summary>
/// This number was determined by testing; updates at 33 millis so the interval is
/// value * .033 = seconds. Must be strictly less than.
/// </summary>
private static readonly int TAP_TIME_OUT = 2;
/// <summary>
/// This number is under test
/// </summary>
private static readonly float THRESHHOLD = .8f;
private static ScreenOrientation orientation;
#region Event Declarations
public static event FlickHandler Flick;
public static event DpadDownHandler ClickDown;
public static event DpadUpHandler ClickUp;
public static event TouchPressHandler TouchPress;
public static event TouchReleaseHandler TouchRelease;
public static event TapHandler Tap;
#endregion
/// <summary>
/// Stores the previous state of the zunepad
/// </summary>
private GamePadState previousState;
/// <summary>
/// Stores the past touch vectors of the zunepad. used for calculating flicks
/// </summary>
private Queue<Vector2> flickQueue;
/// <summary>
/// TapCount must be below ZunepadDaemon.TAP_TIME_OUT in order to be considered a "tap"
/// </summary>
private int tapCount;
/// <summary>
/// a timer for flick
/// </summary>
private int flickCount;
/// <summary>
/// true if zunepad is enabled
/// </summary>
private bool enabled;
/// <summary>
/// true if in landscape mode
/// </summary>
private bool landscape;
private int updateOrder;
public ZunepadDaemon()
{
this.flickQueue = new Queue<Vector2>(FLICK_QUEUE_SIZE);
for (int i = 0; i < FLICK_QUEUE_SIZE; i++)
this.flickQueue.Enqueue(Vector2.Zero);
this.tapCount = 0;
this.flickCount = 0;
//zunepaddaemon is potentially expensive. only turn on when the user explicitly wants it on.
this.enabled = false;
this.updateOrder = 0;
}
public void SetOrientation(ScreenOrientation New_Orientation)
{
landscape = New_Orientation == ScreenOrientation.Portrait ? false : true;
ZunepadDaemon.orientation = New_Orientation;
}
/// <summary>
/// This method grabs info from the zunepad and pushes it to events when the events should be raised.
/// Note that this method pushes raw data. The data is converted in the event classes.
/// </summary>
/// <param name="gameTime"></param>
public void Update(GameTime gameTime)
{
GamePadState gps = GamePad.GetState(PlayerIndex.One);
#region In the previous state the zunepad is touched
if (this.previousState.Buttons.LeftStick == ButtonState.Pressed)
{
this.tapCount++;
this.flickCount++;
#region In the current state the zunepad is touched (touch continues)
if (gps.Buttons.LeftStick == ButtonState.Pressed)
{
Vector2 delta = gps.ThumbSticks.Left - flickQueue.Dequeue();
//enough time has elapsed. see if ANY flick has occurred, then pass raw data to event to
//decide which flick occurred.
if (this.flickCount >= ZunepadDaemon.FLICK_QUEUE_SIZE)
{
if (Math.Abs(delta.X) > ZunepadDaemon.THRESHHOLD
|| Math.Abs(delta.Y) > ZunepadDaemon.THRESHHOLD)
ZunepadDaemon.Flick(new FlickEventArgs(gps.ThumbSticks.Left));
//prevent multiple flicks from occurring.
this.Flood(gps.ThumbSticks.Left);
this.flickCount = 0;
}
//just proceed with advancing the state of the queue
else
this.flickQueue.Enqueue(gps.ThumbSticks.Left);
}
#endregion
#region In the current state the zunepad is not touched (release)
else
{
//touchrelease()
if(ZunepadDaemon.TouchRelease != null)
ZunepadDaemon.TouchRelease(new TouchEventArgs(gps.ThumbSticks.Left));
//if the time between touch and release is small enough, tap()
if(this.tapCount < ZunepadDaemon.TAP_TIME_OUT
&& ZunepadDaemon.Tap != null)
ZunepadDaemon.Tap(new TapEventArgs(gps.ThumbSticks.Left));
}
#endregion
}
#endregion
#region In the previous state the zunepad was not touched
else
{
#region In the current state the zunepad is touched (touch begins)
//touch is new. touchpress() and set up flick and tap.
if (gps.Buttons.LeftStick == ButtonState.Pressed)
{
if (ZunepadDaemon.TouchPress != null)
ZunepadDaemon.TouchPress(new TouchEventArgs(gps.ThumbSticks.Left));
this.tapCount = 0;
this.flickCount = 0;
this.Flood(gps.ThumbSticks.Left);
}
#endregion
}
#endregion
ZuneButtons buttonsPressed = ZuneButtons.None;
ZuneButtons buttonsReleased = ZuneButtons.None;
#region Center
if (this.previousState.Buttons.A == ButtonState.Pressed)
{
if (gps.Buttons.A == ButtonState.Released)
buttonsReleased |= ZuneButtons.Center;
}
else if (gps.Buttons.A == ButtonState.Pressed)
buttonsPressed |= ZuneButtons.Center;
#endregion
#region Back
if (this.previousState.Buttons.Back == ButtonState.Pressed)
{
if (gps.Buttons.Back == ButtonState.Released)
buttonsReleased |= ZuneButtons.Back;
}
else if (gps.Buttons.Back == ButtonState.Pressed)
buttonsPressed |= ZuneButtons.Back;
#endregion
#region Play
if (this.previousState.Buttons.B == ButtonState.Pressed)
{
if (gps.Buttons.B == ButtonState.Released)
buttonsReleased |= ZuneButtons.Play;
}
else if (gps.Buttons.B == ButtonState.Pressed)
buttonsPressed |= ZuneButtons.Play;
#endregion
#region Dpad up (DYpos)
if (this.previousState.DPad.Up == ButtonState.Pressed)
{
if (gps.DPad.Up == ButtonState.Released)
buttonsReleased |= ZuneButtons.DYpos;
}
else if (gps.DPad.Up == ButtonState.Pressed)
buttonsPressed |= ZuneButtons.DYpos;
#endregion
#region Dpad down (DYneg)
if (this.previousState.DPad.Down == ButtonState.Pressed)
{
if (gps.DPad.Down == ButtonState.Released)
buttonsReleased |= ZuneButtons.DYneg;
}
else if (gps.DPad.Down == ButtonState.Pressed)
buttonsPressed |= ZuneButtons.DYneg;
#endregion
#region Dpad left (DXneg)
if (this.previousState.DPad.Left == ButtonState.Pressed)
{
if (gps.DPad.Left == ButtonState.Released)
buttonsReleased |= ZuneButtons.DXneg;
}
else if (gps.DPad.Left == ButtonState.Pressed)
buttonsPressed |= ZuneButtons.DXneg;
#endregion
#region Dpad right (DXpos)
if (this.previousState.DPad.Right == ButtonState.Pressed)
{
if (gps.DPad.Right == ButtonState.Released)
buttonsReleased |= ZuneButtons.DXpos;
}
else if (gps.DPad.Right == ButtonState.Pressed)
buttonsPressed |= ZuneButtons.DXpos;
#endregion
if(buttonsPressed != ZuneButtons.None && ZunepadDaemon.ClickDown != null)
ZunepadDaemon.ClickDown(new DpadEventArgs(buttonsPressed));
if(buttonsReleased != ZuneButtons.None && ZunepadDaemon.ClickUp != null)
ZunepadDaemon.ClickUp(new DpadEventArgs(buttonsReleased));
this.previousState = gps;
}
/// <summary>
/// Fills the queue with the item value
/// </summary>
/// <param name="value"></param>
private void Flood(Vector2 value)
{
this.flickQueue.Clear();
for (int i = 0; i < ZunepadDaemon.FLICK_QUEUE_SIZE; i++)
this.flickQueue.Enqueue(value);
}
public static float Threshold
{
get
{
return ZunepadDaemon.THRESHHOLD;
}
}
public bool Enabled
{
get
{
return enabled;
}
set
{
if ((this.enabled != (this.enabled = value)))
{
//reset the queue after an enabled change
this.Flood(Vector2.Zero);
if (this.EnabledChanged != null)
this.EnabledChanged(this, new EventArgs());
}
}
}
public static ScreenOrientation Orientation
{
get
{
return ZunepadDaemon.orientation;
}
}
#region IUpdateable Members
/// <summary>
/// This works.
/// </summary>
public event EventHandler EnabledChanged;
public int UpdateOrder
{
get
{
return this.updateOrder;
}
set
{
if (this.updateOrder != value)
{
this.updateOrder = value;
this.UpdateOrderChanged(this, new EventArgs());
}
}
}
public event EventHandler UpdateOrderChanged;
#endregion
#region IGameComponent Members
/// <summary>
/// This is just required for the ZunepadDaemon to be a game component so it can be updated etc.
/// </summary>
public void Initialize()
{
}
#endregion
}
//no sense in passing the sender, the sender is always ZunepadDaemon, and it doesn't contain any information.
public delegate void FlickHandler(FlickEventArgs e);
public delegate void DpadDownHandler(DpadEventArgs e);
public delegate void DpadUpHandler(DpadEventArgs e);
public delegate void TouchPressHandler(TouchEventArgs e);
public delegate void TouchReleaseHandler(TouchEventArgs e);
public delegate void TapHandler(TapEventArgs e);
//TODO: perhaps negative magnitudes?
public class FlickEventArgs : EventArgs
{
private FlickMagnitude horizontal;
private FlickMagnitude vertical;
private FlickDirections type;
private Vector2 rawVelocity;
public FlickEventArgs(Vector2 rawVelocity)
: base()
{
this.rawVelocity = rawVelocity;
//set the magnitude of the flick
float tempx = Math.Abs(rawVelocity.X);
float tempy = Math.Abs(rawVelocity.Y);
FlickMagnitude magX;
FlickMagnitude magY;
#region Set X Magnitude
if (tempx < .9f)
magX = FlickMagnitude.Very_Low;
else if (tempx < 1.2f)
magX = FlickMagnitude.Low;
else if (tempx < 1.5f)
magX = FlickMagnitude.Medium;
else if (tempx < 1.75f)
magX = FlickMagnitude.High;
else
magX = FlickMagnitude.Very_High;
#endregion
#region Set Y Magnitude
if (tempy < .9f)
magY = FlickMagnitude.Very_Low;
else if (tempy < 1.2f)
magY = FlickMagnitude.Low;
else if (tempy < 1.5f)
magY = FlickMagnitude.Medium;
else if (tempy < 1.75f)
magY = FlickMagnitude.High;
else
magY = FlickMagnitude.Very_High;
#endregion
#region Set type of flick
if (rawVelocity.Y < -ZunepadDaemon.Threshold)
this.type |= FlickDirections.Yneg;
else if (rawVelocity.Y > ZunepadDaemon.Threshold)
this.type |= FlickDirections.Ypos;
if (rawVelocity.X < -ZunepadDaemon.Threshold)
this.type |= FlickDirections.Xneg;
else if (rawVelocity.X > ZunepadDaemon.Threshold)
this.type |= FlickDirections.Xpos;
#endregion
if (ZunepadDaemon.Orientation == ScreenOrientation.Portrait)
{
this.horizontal = magX;
this.vertical = magY;
}
else
{
this.horizontal = magY;
this.vertical = magX;
}
}
public Vector2 RawVelocity
{
get
{
return this.rawVelocity;
}
}
public FlickDirections FlickType
{
get
{
return this.type;
}
}
public FlickMagnitude XMagnitude
{
get
{
return this.horizontal;
}
}
public FlickMagnitude YMagnitude
{
get
{
return this.vertical;
}
}
}
//done
public class DpadEventArgs : EventArgs
{
private ZuneButtons absoluteButtons;
private DPadDirection directions;
public DpadEventArgs(ZuneButtons buttons)
: base()
{
this.absoluteButtons = buttons;
#region Assign directions. This depends on on the ZuneUIManager.Orientation property
if (ZunepadDaemon.Orientation == ScreenOrientation.Landscape)
{
if ((buttons & ZuneButtons.DXpos) != ZuneButtons.None)
directions |= DPadDirection.Up;
else if ((buttons & ZuneButtons.DXneg) != ZuneButtons.None)
directions |= DPadDirection.Down;
if ((buttons & ZuneButtons.DYneg) != ZuneButtons.None)
directions |= DPadDirection.Right;
else if ((buttons & ZuneButtons.DYpos) != ZuneButtons.None)
directions |= DPadDirection.Left;
}
else
{
if ((buttons & ZuneButtons.DXpos) != ZuneButtons.None)
directions |= DPadDirection.Right;
else if ((buttons & ZuneButtons.DXneg) != ZuneButtons.None)
directions |= DPadDirection.Left;
if ((buttons & ZuneButtons.DYpos) != ZuneButtons.None)
directions |= DPadDirection.Up;
else if ((buttons & ZuneButtons.DYneg) != ZuneButtons.None)
directions |= DPadDirection.Down;
}
#endregion
}
public ZuneButtons AbsoluteButtons
{
get
{
return this.absoluteButtons;
}
}
public DPadDirection Direction
{
get
{
return this.directions;
}
}
}
/// <summary>
/// This class presents raw data about a touch
/// </summary>
public class TouchEventArgs : EventArgs
{
private Vector2 velocity;
public TouchEventArgs(Vector2 velocity)
{
this.velocity = velocity;
}
public Vector2 Velocity
{
get
{
return this.velocity;
}
}
}
/// <summary>
/// this class presents raw data about a tap
/// </summary>
public class TapEventArgs : EventArgs
{
private Vector2 position;
public TapEventArgs(Vector2 position)
: base()
{
this.position = position;
}
public Vector2 Position
{
get
{
return this.position;
}
}
}
[Flags]
public enum ZuneButtons
{
None = 0x0,
Center = 0x1,
DYpos = 0x2,
DYneg = 0x4,
DXpos = 0x8,
DXneg = 0x10,
Back = 0x20,
Play = 0x40,
}
[Flags]
public enum FlickDirections
{
None = 0,
Ypos = 1,
Yneg = 2,
Xpos = 4,
Xneg = 8
}
public enum FlickMagnitude
{
Very_Low,
Low,
Medium,
High,
Very_High
}
[Flags]
public enum DPadDirection
{
None = 0x0,
Up = 0x1,
Down = 0x2,
Left = 0x4,
Right = 0x8
}
public enum ScreenOrientation
{
Portrait,
Landscape
}