Skip to content

Instantly share code, notes, and snippets.

@f0ff886f
Created March 27, 2015 11:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save f0ff886f/7a93a98060a360494d20 to your computer and use it in GitHub Desktop.
Save f0ff886f/7a93a98060a360494d20 to your computer and use it in GitHub Desktop.
Android Gamepad Input for MonoGame
// MonoGame - Copyright (C) The MonoGame Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.
using Android.Views;
namespace Microsoft.Xna.Framework.Input
{
internal class AndroidGamePad
{
public InputDevice _device;
public int _deviceId;
public string _descriptor;
public bool _isConnected;
public Buttons _buttons;
public float _leftTrigger, _rightTrigger;
public Vector2 _leftStick, _rightStick;
public readonly GamePadCapabilities _capabilities;
public AndroidGamePad(InputDevice device)
{
_device = device;
_deviceId = device.Id;
_descriptor = device.Descriptor;
_isConnected = true;
_capabilities = CapabilitiesOfDevice(device);
}
private static GamePadCapabilities CapabilitiesOfDevice(InputDevice device)
{
var capabilities = new GamePadCapabilities();
capabilities.IsConnected = true;
capabilities.GamePadType = GamePadType.GamePad;
capabilities.HasLeftVibrationMotor = capabilities.HasRightVibrationMotor = device.Vibrator.HasVibrator;
// build out supported inputs from what the gamepad exposes
int[] keyMap = new int[16];
keyMap[0] = (int)Keycode.ButtonA;
keyMap[1] = (int)Keycode.ButtonB;
keyMap[2] = (int)Keycode.ButtonX;
keyMap[3] = (int)Keycode.ButtonY;
keyMap[4] = (int)Keycode.ButtonThumbl;
keyMap[5] = (int)Keycode.ButtonThumbr;
keyMap[6] = (int)Keycode.ButtonL1;
keyMap[7] = (int)Keycode.ButtonR1;
keyMap[8] = (int)Keycode.ButtonL2;
keyMap[9] = (int)Keycode.ButtonR2;
keyMap[10] = (int)Keycode.DpadDown;
keyMap[11] = (int)Keycode.DpadLeft;
keyMap[12] = (int)Keycode.DpadRight;
keyMap[13] = (int)Keycode.DpadUp;
keyMap[14] = (int)Keycode.ButtonStart;
keyMap[15] = (int)Keycode.Back;
bool[] hasMap = new bool[16];
// get a bool[] with indices matching the keyMap
hasMap = device.HasKeys(keyMap);
capabilities.HasAButton = hasMap[0];
capabilities.HasBButton = hasMap[1];
capabilities.HasXButton = hasMap[2];
capabilities.HasYButton = hasMap[3];
// we only check for the thumb button to see if we have 2 thumbsticks
// if ever a controller doesn't support buttons on the thumbsticks,
// this will need fixing
capabilities.HasLeftXThumbStick = hasMap[4];
capabilities.HasLeftYThumbStick = hasMap[4];
capabilities.HasRightXThumbStick = hasMap[5];
capabilities.HasRightYThumbStick = hasMap[5];
capabilities.HasLeftShoulderButton = hasMap[6];
capabilities.HasRightShoulderButton = hasMap[7];
capabilities.HasLeftTrigger = hasMap[8];
capabilities.HasRightTrigger = hasMap[9];
capabilities.HasDPadDownButton = hasMap[10];
capabilities.HasDPadLeftButton = hasMap[11];
capabilities.HasDPadRightButton = hasMap[12];
capabilities.HasDPadUpButton = hasMap[13];
capabilities.HasStartButton = hasMap[14];
capabilities.HasBackButton = hasMap[15];
return capabilities;
}
}
static partial class GamePad
{
// we will support up to 4 local controllers
private static readonly AndroidGamePad[] GamePads = new AndroidGamePad[4];
// support the back button when we don't have a gamepad connected
internal static bool Back;
private static GamePadCapabilities PlatformGetCapabilities(int index)
{
var gamePad = GamePads[index];
if (gamePad != null)
return gamePad._capabilities;
// we need to add the default "no gamepad connected but the user hit back"
// behaviour here
GamePadCapabilities capabilities = new GamePadCapabilities();
capabilities.IsConnected = (index == 0);
capabilities.HasBackButton = true;
return capabilities;
}
private static GamePadState PlatformGetState(int index, GamePadDeadZone deadZoneMode)
{
var gamePad = GamePads[index];
GamePadState state = GamePadState.Default;
if (gamePad != null && gamePad._isConnected)
{
// Check if the device was disconnected
var dvc = InputDevice.GetDevice(gamePad._deviceId);
if (dvc == null)
{
Android.Util.Log.Debug("MonoGame", "Detected controller disconnect [" + index + "] ");
gamePad._isConnected = false;
return state;
}
GamePadThumbSticks thumbSticks = new GamePadThumbSticks(gamePad._leftStick, gamePad._rightStick, deadZoneMode);
state = new GamePadState(
thumbSticks,
new GamePadTriggers(gamePad._leftTrigger, gamePad._rightTrigger),
new GamePadButtons(gamePad._buttons),
new GamePadDPad(gamePad._buttons));
}
// we need to add the default "no gamepad connected but the user hit back"
// behaviour here
else {
if (index == 0 && Back)
{
// Consume state
Back = false;
state = new GamePadState(new GamePadThumbSticks(), new GamePadTriggers(), new GamePadButtons(Buttons.Back), new GamePadDPad());
}
else
state = new GamePadState();
}
return state;
}
private static bool PlatformSetVibration(int index, float leftMotor, float rightMotor)
{
var gamePad = GamePads[index];
if (gamePad == null)
return false;
var vibrator = gamePad._device.Vibrator;
if (!vibrator.HasVibrator)
return false;
vibrator.Vibrate(500);
return true;
}
internal static AndroidGamePad GetGamePad(InputDevice device)
{
if (device == null || (device.Sources & InputSourceType.Gamepad) != InputSourceType.Gamepad)
return null;
int firstDisconnectedPadId = -1;
for (int i = 0; i < GamePads.Length; i++)
{
var pad = GamePads[i];
if (pad != null && pad._isConnected && pad._deviceId == device.Id)
{
return pad;
}
else if (pad != null && !pad._isConnected && pad._descriptor == device.Descriptor)
{
Android.Util.Log.Debug("MonoGame", "Found previous controller [" + i + "] " + device.Name);
pad._deviceId = device.Id;
pad._isConnected = true;
return pad;
}
else if (pad == null)
{
Android.Util.Log.Debug("MonoGame", "Found new controller [" + i + "] " + device.Name);
pad = new AndroidGamePad(device);
GamePads[i] = pad;
return pad;
}
else if (!pad._isConnected && firstDisconnectedPadId < 0)
{
firstDisconnectedPadId = i;
}
}
// If we get here, we failed to find a game pad or an empty slot to create one.
// If we're holding onto a disconnected pad, overwrite it with this one
if (firstDisconnectedPadId >= 0)
{
Android.Util.Log.Debug("MonoGame", "Found new controller in place of disconnected controller [" + firstDisconnectedPadId + "] " + device.Name);
var pad = new AndroidGamePad(device);
GamePads[firstDisconnectedPadId] = pad;
return pad;
}
// All pad slots are taken so ignore further devices.
return null;
}
internal static bool OnKeyDown(Keycode keyCode, KeyEvent e)
{
var gamePad = GetGamePad(e.Device);
if (gamePad == null)
return false;
gamePad._buttons |= ButtonForKeyCode(keyCode);
return true;
}
internal static bool OnKeyUp(Keycode keyCode, KeyEvent e)
{
var gamePad = GetGamePad(e.Device);
if (gamePad == null)
return false;
gamePad._buttons &= ~ButtonForKeyCode(keyCode);
return true;
}
internal static bool OnGenericMotionEvent(MotionEvent e)
{
var gamePad = GetGamePad(e.Device);
if (gamePad == null)
return false;
if (e.Action != MotionEventActions.Move)
return false;
gamePad._leftStick = new Vector2(e.GetAxisValue(Axis.X), -e.GetAxisValue(Axis.Y));
gamePad._rightStick = new Vector2(e.GetAxisValue(Axis.Z), -e.GetAxisValue(Axis.Rz));
gamePad._leftTrigger = e.GetAxisValue(Axis.Ltrigger);
gamePad._rightTrigger = e.GetAxisValue(Axis.Rtrigger);
return true;
}
private static Buttons ButtonForKeyCode(Keycode keyCode)
{
switch (keyCode)
{
case Keycode.ButtonA:
return Buttons.A;
case Keycode.ButtonX:
return Buttons.X;
case Keycode.ButtonY:
return Buttons.Y;
case Keycode.ButtonB:
return Buttons.B;
case Keycode.ButtonL1:
return Buttons.LeftShoulder;
case Keycode.ButtonL2:
return Buttons.LeftTrigger;
case Keycode.ButtonR1:
return Buttons.RightShoulder;
case Keycode.ButtonR2:
return Buttons.RightTrigger;
case Keycode.ButtonThumbl:
return Buttons.LeftStick;
case Keycode.ButtonThumbr:
return Buttons.RightStick;
case Keycode.DpadUp:
return Buttons.DPadUp;
case Keycode.DpadDown:
return Buttons.DPadDown;
case Keycode.DpadLeft:
return Buttons.DPadLeft;
case Keycode.DpadRight:
return Buttons.DPadRight;
case Keycode.ButtonStart:
return Buttons.Start;
case Keycode.Back:
return Buttons.Back;
}
return 0;
}
internal static void Initialize()
{
//Iterate and 'connect' any detected gamepads
foreach (var deviceId in InputDevice.GetDeviceIds())
{
GetGamePad(InputDevice.GetDevice(deviceId));
}
}
}
}
// MonoGame - Copyright (C) The MonoGame Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.
using System;
using System.Collections.Generic;
using Android.Content;
using Android.Media;
using Android.Views;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using OpenTK.Graphics;
using OpenTK.Graphics.ES20;
using OpenTK.Platform.Android;
namespace Microsoft.Xna.Framework
{
/// <summary>
/// Our override of OpenTK.AndroidGameView. Provides Touch and Key Input handling.
/// </summary>
internal class MonoGameAndroidGameView : AndroidGameView, View.IOnTouchListener, ISurfaceHolderCallback
{
private readonly AndroidGameWindow _gameWindow;
private readonly Game _game;
private readonly AndroidTouchEventManager _touchManager;
public bool IsResuming { get; private set; }
private bool _lostContext;
private bool backPressed;
public MonoGameAndroidGameView(Context context, AndroidGameWindow androidGameWindow, Game game)
: base(context)
{
_gameWindow = androidGameWindow;
_game = game;
_touchManager = new AndroidTouchEventManager(androidGameWindow);
}
public bool TouchEnabled
{
get { return _touchManager.Enabled; }
set
{
_touchManager.Enabled = value;
SetOnTouchListener(value ? this : null);
}
}
#region IOnTouchListener implementation
bool IOnTouchListener.OnTouch(View v, MotionEvent e)
{
_touchManager.OnTouchEvent(e);
return true;
}
#endregion
#region ISurfaceHolderCallback implementation
//AndroidGameView also implements ISurfaceHolderCallback and has these methods.
//That is why these get called even though we never register as a SurfaceHolderCallback
private bool _isSurfaceChanged = false;
private int _prevSurfaceWidth = 0;
private int _prevSurfaceHeight = 0;
void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Android.Graphics.Format format, int width, int height)
{
if ((int)Android.OS.Build.VERSION.SdkInt >= 19)
{
if (!_isSurfaceChanged)
{
_isSurfaceChanged = true;
_prevSurfaceWidth = width;
_prevSurfaceHeight = height;
}
else
{
// Forcing reinitialization of the view if SurfaceChanged() is called more than once to fix shifted drawing on KitKat.
// See https://github.com/mono/MonoGame/issues/2492.
if (!ScreenReceiver.ScreenLocked && Game.Instance.Platform.IsActive &&
(_prevSurfaceWidth != width || _prevSurfaceHeight != height))
{
_prevSurfaceWidth = width;
_prevSurfaceHeight = height;
base.SurfaceDestroyed(holder);
base.SurfaceCreated(holder);
}
}
}
// When the game is resumed from a portrait orientation it may receive a portrait surface at first.
// If the game does not support portrait we should ignore it because we will receive the landscape surface a moment later.
if (width < height && (_game.graphicsDeviceManager.SupportedOrientations & DisplayOrientation.Portrait) == 0)
return;
var manager = _game.graphicsDeviceManager;
manager.PreferredBackBufferWidth = width;
manager.PreferredBackBufferHeight = height;
if (manager.GraphicsDevice != null)
manager.GraphicsDevice.Viewport = new Viewport(0, 0, width, height);
_gameWindow.ChangeClientBounds(new Rectangle(0, 0, width, height));
manager.ApplyChanges();
SurfaceChanged(holder, format, width, height);
Android.Util.Log.Debug("MonoGame", "MonoGameAndroidGameView.SurfaceChanged: format = " + format + ", width = " + width + ", height = " + height);
if (_game.GraphicsDevice != null)
_game.graphicsDeviceManager.ResetClientBounds();
}
void ISurfaceHolderCallback.SurfaceDestroyed(ISurfaceHolder holder)
{
SurfaceDestroyed(holder);
Android.Util.Log.Debug("MonoGame", "MonoGameAndroidGameView.SurfaceDestroyed");
}
void ISurfaceHolderCallback.SurfaceCreated(ISurfaceHolder holder)
{
SurfaceCreated(holder);
Android.Util.Log.Debug("MonoGame", "MonoGameAndroidGameView.SurfaceCreated: surfaceFrame = " + holder.SurfaceFrame.ToString());
_isSurfaceChanged = false;
}
#endregion
#region AndroidGameView
protected override void OnLoad(EventArgs eventArgs)
{
base.OnLoad(eventArgs);
try
{
MakeCurrent();
}
catch (Exception e)
{
throw new NoSuitableGraphicsDeviceException(e.Message, e);
}
}
public override void Resume()
{
if (!ScreenReceiver.ScreenLocked && Game.Instance.Platform.IsActive)
base.Resume();
}
protected override void OnContextLost(EventArgs e)
{
base.OnContextLost(e);
// OnContextLost is called when the underlying OpenGL context is destroyed
// this usually happens on older devices when other opengl apps are run
// or the lock screen is enabled. Modern devices can preserve the opengl
// context along with all the textures and shaders it has attached.
Android.Util.Log.Debug("MonoGame", "MonoGameAndroidGameView Context Lost");
// DeviceResetting events
_game.graphicsDeviceManager.OnDeviceResetting(EventArgs.Empty);
if (_game.GraphicsDevice != null)
_game.GraphicsDevice.OnDeviceResetting();
_lostContext = true;
}
protected override void OnContextSet(EventArgs e)
{
// This is called when a Context is created. This will happen in
// two ways, first when the activity is first created. Second if
// (and only if) the context is lost
// When an acivity is now paused we correctly preserve the context
// rather than destoying it along with the Surface which is what
// used to happen.
base.OnContextSet(e);
Android.Util.Log.Debug("MonoGame", "MonoGameAndroidGameView Context Set");
if (_lostContext)
{
_lostContext = false;
if (_game.GraphicsDevice != null)
{
_game.GraphicsDevice.Initialize();
IsResuming = true;
if (_gameWindow.Resumer != null)
{
_gameWindow.Resumer.LoadContent();
}
// Reload textures on a different thread so the resumer can be drawn
System.Threading.Thread bgThread = new System.Threading.Thread(
o =>
{
Android.Util.Log.Debug("MonoGame", "Begin reloading graphics content");
Microsoft.Xna.Framework.Content.ContentManager.ReloadGraphicsContent();
Android.Util.Log.Debug("MonoGame", "End reloading graphics content");
// DeviceReset events
_game.graphicsDeviceManager.OnDeviceReset(EventArgs.Empty);
_game.GraphicsDevice.OnDeviceReset();
IsResuming = false;
});
bgThread.Start();
}
}
}
protected override void CreateFrameBuffer()
{
Android.Util.Log.Debug("MonoGame", "MonoGameAndroidGameView.CreateFrameBuffer");
ContextRenderingApi = GLVersion.ES2;
int depth = 0;
int stencil = 0;
switch (_game.graphicsDeviceManager.PreferredDepthStencilFormat)
{
case DepthFormat.Depth16:
depth = 16;
break;
case DepthFormat.Depth24:
depth = 24;
break;
case DepthFormat.Depth24Stencil8:
depth = 24;
stencil = 8;
break;
case DepthFormat.None:
break;
}
List<GraphicsMode> modes = new List<GraphicsMode>();
if (depth > 0)
{
modes.Add(new AndroidGraphicsMode(new ColorFormat(8, 8, 8, 8), depth, stencil, 0, 0, false));
modes.Add(new AndroidGraphicsMode(new ColorFormat(5, 6, 5, 0), depth, stencil, 0, 0, false));
modes.Add(new AndroidGraphicsMode(0, depth, stencil, 0, 0, false));
if (depth > 16)
{
modes.Add(new AndroidGraphicsMode(new ColorFormat(8, 8, 8, 8), 16, 0, 0, 0, false));
modes.Add(new AndroidGraphicsMode(new ColorFormat(5, 6, 5, 0), 16, 0, 0, 0, false));
modes.Add(new AndroidGraphicsMode(0, 16, 0, 0, 0, false));
}
}
else
{
modes.Add(new AndroidGraphicsMode(new ColorFormat(8, 8, 8, 8), 0, 0, 0, 0, false));
modes.Add(new AndroidGraphicsMode(new ColorFormat(5, 6, 5, 0), 0, 0, 0, 0, false));
}
modes.Add(null); // default mode
modes.Add(new AndroidGraphicsMode(0, 0, 0, 0, 0, false)); // low mode
Exception innerException = null;
foreach (GraphicsMode mode in modes)
{
if (mode != null)
Android.Util.Log.Debug("MonoGame", "Creating Color: {0}, Depth: {1}, Stencil: {2}, Accum:{3}", mode.ColorFormat, mode.Depth, mode.Stencil, mode.AccumulatorFormat);
else
Android.Util.Log.Debug("MonoGame", "Creating default mode");
GraphicsMode = mode;
try
{
base.CreateFrameBuffer();
}
catch (Exception e)
{
innerException = e;
continue;
}
Android.Util.Log.Debug("MonoGame", "Created format {0}", GraphicsContext.GraphicsMode);
var status = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
Android.Util.Log.Debug("MonoGame", "Framebuffer Status: " + status.ToString());
MakeCurrent();
return;
}
throw new NoSuitableGraphicsDeviceException("Could not create OpenGLES 2.0 frame buffer", innerException);
}
#endregion
#region Key and Motion and Gamepad
public override bool OnKeyDown(Keycode keyCode, KeyEvent e)
{
// Handle gamepad inputs in Android/OUYA
if ((e.Source & InputSourceType.Gamepad) == InputSourceType.Gamepad)
return GamePad.OnKeyDown(keyCode, e);
Keyboard.KeyDown(keyCode);
// we need to handle the Back key here because it doesnt work any other way
if (keyCode == Keycode.Back && !this.backPressed)
{
this.backPressed = true;
GamePad.Back = true;
return true;
}
if (keyCode == Keycode.VolumeUp)
{
AudioManager audioManager = (AudioManager)Context.GetSystemService(Context.AudioService);
audioManager.AdjustStreamVolume(Stream.Music, Adjust.Raise, VolumeNotificationFlags.ShowUi);
return true;
}
if (keyCode == Keycode.VolumeDown)
{
AudioManager audioManager = (AudioManager)Context.GetSystemService(Context.AudioService);
audioManager.AdjustStreamVolume(Stream.Music, Adjust.Lower, VolumeNotificationFlags.ShowUi);
return true;
}
return true;
}
public override bool OnKeyUp(Keycode keyCode, KeyEvent e)
{
if ((e.Source & InputSourceType.Gamepad) == InputSourceType.Gamepad)
return GamePad.OnKeyUp(keyCode, e);
Keyboard.KeyUp(keyCode);
// we need to handle the Back key here because it doesnt work any other way
if (keyCode == Keycode.Back)
this.backPressed = false;
return true;
}
public override bool OnGenericMotionEvent(MotionEvent e)
{
if ((e.Source & InputSourceType.Gamepad) == InputSourceType.Gamepad || (e.Source & InputSourceType.Joystick) == InputSourceType.Joystick)
return GamePad.OnGenericMotionEvent(e);
return base.OnGenericMotionEvent(e);
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment