Created
March 27, 2015 11:42
-
-
Save f0ff886f/7a93a98060a360494d20 to your computer and use it in GitHub Desktop.
Android Gamepad Input for MonoGame
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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