HTML5 gamepad support

Monkey Forums/Monkey Programming/HTML5 gamepad support

Skn3(Posted 2015) [#1]
Hey all,

Just spent a little while today adding gamepad support to the HTML5 target. It is currently on a pull request into official, but if you want to test it out or is not accepted into official then here it is below.

Example:
http://www.skn3.com/junk/gamepad/MonkeyGame.html

In targets/html5/modules/native/html5game.js, make changes to the top of the file.

You can see there are 2 blocks of code to add inbetween // --- start gamepad api by skn3 --------- and // --- end gamepad api by skn3 ---------

var webglGraphicsSeq=1;

function BBHtml5Game( canvas ){

	BBGame.call( this );
	BBHtml5Game._game=this;
	this._canvas=canvas;
	this._loading=0;
	this._timerSeq=0;
	this._gl=null;
	
	if( CFG_OPENGL_GLES20_ENABLED=="1" ){

		//can't get these to fire!
		canvas.addEventListener( "webglcontextlost",function( event ){
			event.preventDefault();
//			print( "WebGL context lost!" );
		},false );

		canvas.addEventListener( "webglcontextrestored",function( event ){
			++webglGraphicsSeq;
//			print( "WebGL context restored!" );
		},false );

		var attrs={ alpha:false };
	
		this._gl=this._canvas.getContext( "webgl",attrs );

		if( !this._gl ) this._gl=this._canvas.getContext( "experimental-webgl",attrs );
		
		if( !this._gl ) this.Die( "Can't create WebGL" );
		
		gl=this._gl;
	}
	
	// --- start gamepad api by skn3 ---------
	this._gamepads = null;
	this._gamepadLookup = [-1,-1,-1,-1];//support 4 gamepads
	var that = this;
	window.addEventListener("gamepadconnected", function(e) {
		that.connectGamepad(e.gamepad);
	});
	
	window.addEventListener("gamepaddisconnected", function(e) {
		that.disconnectGamepad(e.gamepad);
	});
	
	//need to process already connected gamepads (before page was loaded)
	var gamepads = this.getGamepads();
	if (gamepads && gamepads.length > 0) {
		for(var index=0;index < gamepads.length;index++) {
			this.connectGamepad(gamepads[index]);
		}
	}
	// --- end gamepad api by skn3 ---------
}

BBHtml5Game.prototype=extend_class( BBGame );

BBHtml5Game.Html5Game=function(){
	return BBHtml5Game._game;
}

// --- start gamepad api by skn3 ---------
BBHtml5Game.prototype.getGamepads = function() {
	return navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads : []);
}

BBHtml5Game.prototype.connectGamepad = function(gamepad) {
	if (!gamepad) {
		return false;
	}
	
	//check if this is a standard gamepad
	if (gamepad.mapping == "standard") {
		//yup so lets add it to an array of valid gamepads
		//find empty controller slot
		var slot = -1;
		for(var index = 0;index < this._gamepadLookup.length;index++) {
			if (this._gamepadLookup[index] == -1) {
				slot = index;
				break;
			}
		}
		
		//can we add this?
		if (slot != -1) {
			this._gamepadLookup[slot] = gamepad.index;
			
			//console.log("gamepad at html5 index "+gamepad.index+" mapped to monkey gamepad unit "+slot);
		}
	} else {
		console.log('Monkey has ignored gamepad at raw port #'+gamepad.index+' with unrecognised mapping scheme \''+gamepad.mapping+'\'.');
	}
}

BBHtml5Game.prototype.disconnectGamepad = function(gamepad) {
	if (!gamepad) {
		return false;
	}
	
	//scan all gamepads for matching index
	for(var index = 0;index < this._gamepadLookup.length;index++) {
		if (this._gamepadLookup[index] == gamepad.index) {
			//remove this gamepad
			this._gamepadLookup[index] = -1
			break;
		}
	}
}

BBHtml5Game.prototype.PollJoystick=function(port, joyx, joyy, joyz, buttons){
	//is this the first gamepad being polled
	if (port == 0) {
		//yes it is so we use the web api to get all gamepad info
		//we can then use this in subsequent calls to PollJoystick
		this._gamepads = this.getGamepads();
	}
	
	//dont bother processing if nothing to process
	if (!this._gamepads) {
	  return false;
	}
	
	//so use the monkey port to find the correct raw data
	var index = this._gamepadLookup[port];
	if (index == -1) {
		return false;
	}

	var gamepad = this._gamepads[index];
	if (!gamepad) {
		return false;
	}
	//so now process gamepad axis/buttons according to the standard mappings
	//https://w3c.github.io/gamepad/#remapping
	
	//left stick axis
	joyx[0] = gamepad.axes[0];
	joyy[0] = -gamepad.axes[1];
	
	//right stick axis
	joyx[1] = gamepad.axes[2];
	joyy[1] = -gamepad.axes[3];
	
	//triggers
	//emulate same functionality of GLFW
	joyz[0] = gamepad.buttons[6].value;
	joyz[1] = gamepad.buttons[7].value;
	
	//clear button states
	for(var index = 0;index <32;index++) {
		buttons[index] = false;
	}
	
	//map html5 "standard" mapping to monkeys joy codes
	/*
	Const JOY_A=0
	Const JOY_B=1
	Const JOY_X=2
	Const JOY_Y=3
	Const JOY_LB=4
	Const JOY_RB=5
	Const JOY_BACK=6
	Const JOY_START=7
	Const JOY_LEFT=8
	Const JOY_UP=9
	Const JOY_RIGHT=10
	Const JOY_DOWN=11
	Const JOY_LSB=12
	Const JOY_RSB=13
	Const JOY_MENU=14
	*/
	buttons[0] = gamepad.buttons[0] && gamepad.buttons[0].pressed;
	buttons[1] = gamepad.buttons[1] && gamepad.buttons[1].pressed;
	buttons[2] = gamepad.buttons[2] && gamepad.buttons[2].pressed;
	buttons[3] = gamepad.buttons[3] && gamepad.buttons[3].pressed;
	buttons[4] = gamepad.buttons[4] && gamepad.buttons[4].pressed;
	buttons[5] = gamepad.buttons[5] && gamepad.buttons[5].pressed;
	buttons[6] = gamepad.buttons[8] && gamepad.buttons[8].pressed;
	buttons[7] = gamepad.buttons[9] && gamepad.buttons[9].pressed;
	buttons[8] = gamepad.buttons[14] && gamepad.buttons[14].pressed;
	buttons[9] = gamepad.buttons[12] && gamepad.buttons[12].pressed;
	buttons[10] = gamepad.buttons[15] && gamepad.buttons[15].pressed;
	buttons[11] = gamepad.buttons[13] && gamepad.buttons[13].pressed;
	buttons[12] = gamepad.buttons[10] && gamepad.buttons[10].pressed;
	buttons[13] = gamepad.buttons[11] && gamepad.buttons[11].pressed;
	buttons[14] = gamepad.buttons[16] && gamepad.buttons[16].pressed;
	
	//success
	return true
}
// --- end gamepad api by skn3 ---------


The code for the example uses mojo2 and is here:
Import mojo2

Function Main()
	New Game()
End

'app
Class Game Extends App
	Field canvas:Canvas
	
	Field stickLeft:Float[2]
	Field stickRight:Float[2]
	
	Field triggerLeft:float
	Field triggerRight:float
	
	Field buttons:Bool[JOY_MENU + 1]
	
	Method OnCreate()
		SetUpdateRate(60)
		canvas = New Canvas
	End

	Method OnUpdate()
		'update all states
		stickLeft[0] = JoyX(0)
		stickLeft[1] = JoyY(0)
		
		stickRight[0] = JoyX(1)
		stickRight[1] = JoyY(1)
		
		'have to do this (Max) otherwise GLFW seems to combine both triggers incorrectly!
		triggerLeft = Max(0.0, JoyZ(0))
		triggerRight = Max(0.0, JoyZ(1))
		
		For Local index:= JOY_A To JOY_MENU
			buttons[index] = JoyHit(index) Or JoyDown(index)
		Next
	End

	Method OnRender()
		canvas.Clear(0, 0, 0)
		
		Local padCenterX:= DeviceWidth() / 2.0
		Local padCenterY:= DeviceHeight() / 2.0
		
		'left stick
		DrawAnalog(canvas, padCenterX - 125, padCenterY - 50, 40, stickLeft[0], -stickLeft[1], buttons[JOY_LSB])
		
		'right stick
		DrawAnalog(canvas, padCenterX + 60, padCenterY + 50, 40, stickRight[0], -stickRight[1], buttons[JOY_RSB])
		
		'dpad
		DrawDPad(canvas, padCenterX - 100, padCenterY + 10, 80, buttons[JOY_UP], buttons[JOY_DOWN], buttons[JOY_LEFT], buttons[JOY_RIGHT])
		
		'left trigger
		DrawAxis(canvas, padCenterX - 165, padCenterY - 150, 80, 20, triggerLeft)
		
		'right trigger
		DrawAxis(canvas, padCenterX + 85, padCenterY - 150, 80, 20, triggerRight)
		
		'left shoulder
		DrawButton(canvas, padCenterX - 165, padCenterY - 120, 80, 20, buttons[JOY_LB])
		
		'right shoulder
		DrawButton(canvas, padCenterX + 85, padCenterY - 120, 80, 20, buttons[JOY_RB])
		
		'buttons
		Local buttonSize:= 30.0
		Local buttonCenterX:= padCenterX + 125
		Local buttonCenterY:= padCenterY - 50
		
		'a
		DrawButton(canvas, buttonCenterX, buttonCenterY + 40 - (buttonSize / 2.0), buttonSize / 2.0, buttons[JOY_A])

		'b
		DrawButton(canvas, buttonCenterX + 40 - (buttonSize / 2.0), buttonCenterY, buttonSize / 2.0, buttons[JOY_B])
				
		'x
		DrawButton(canvas, buttonCenterX - 40 + (buttonSize / 2.0), buttonCenterY, buttonSize / 2.0, buttons[JOY_X])
		
		'y
		DrawButton(canvas, buttonCenterX, buttonCenterY - 40 + (buttonSize / 2.0), buttonSize / 2.0, buttons[JOY_Y])
		
		'start
		DrawButton(canvas, padCenterX + buttonSize, buttonCenterY - (buttonSize / 4.0), buttonSize, buttonSize / 2.0, buttons[JOY_START])
		
		'back
		DrawButton(canvas, padCenterX - buttonSize - buttonSize, buttonCenterY - (buttonSize / 4.0), buttonSize, buttonSize / 2.0, buttons[JOY_BACK])
		
		'menu
		DrawButton(canvas, padCenterX, buttonCenterY, buttonSize * 0.75, buttons[JOY_MENU])
		
		
		canvas.Flush()
	End
End

'functions
Function DrawAxis:Void(canvas:Canvas, x:Float, y:float, width:Float, height:float, axis:float)
	canvas.SetColor(0.5, 0.5, 0.5)
	canvas.DrawRect(x, y, width, height)
		
	Local size:= Min(16.0, Max(2.0, width * 0.1))
	Local offset:= (width / 2.0) + (axis * ( (width - size) / 2.0)) - (size / 2.0)
		
	If offset >= 0 And offset < width
		canvas.SetColor(0.3, 0.3, 0.3)
		canvas.DrawRect(x + offset, y, size, height)
	EndIf
End
	
Function DrawDPad:Void(canvas:Canvas, x:Float, y:Float, size:Float, up:Bool, down:Bool, left:Bool, right:Bool)
	Local buttonSize:= size / 3.0
	Local buttonPadding:= Min(6.0, Max(2.0, size * 0.04))
		
	canvas.SetColor(0.5, 0.5, 0.5)
	canvas.DrawRect(x, y + buttonSize, size, buttonSize)
	canvas.DrawRect(x + buttonSize, y, buttonSize, size)
		
	canvas.SetColor(0.3, 0.3, 0.3)
	canvas.DrawRect(x + buttonSize, y + buttonSize, buttonSize, buttonSize)
		
	'up
	If up
		canvas.SetColor(0.0, 1.0, 0.0)
	Else
		canvas.SetColor(0.3, 0.3, 0.3)
	EndIf
	canvas.DrawRect(x + buttonSize + buttonPadding, y + buttonPadding, buttonSize - buttonPadding - buttonPadding, buttonSize - buttonPadding)
		
	'down
	If down
		canvas.SetColor(0.0, 1.0, 0.0)
	Else
		canvas.SetColor(0.3, 0.3, 0.3)
	EndIf
	canvas.DrawRect(x + buttonSize + buttonPadding, y + size - buttonSize, buttonSize - buttonPadding - buttonPadding, buttonSize - buttonPadding)
		
	'left
	If left
		canvas.SetColor(0.0, 1.0, 0.0)
	Else
		canvas.SetColor(0.3, 0.3, 0.3)
	EndIf
	canvas.DrawRect(x + buttonPadding, y + buttonSize + buttonPadding, buttonSize - buttonPadding, buttonSize - buttonPadding - buttonPadding)
		
	'right
	If right
		canvas.SetColor(0.0, 1.0, 0.0)
	Else
		canvas.SetColor(0.3, 0.3, 0.3)
	EndIf
	canvas.DrawRect(x + size - buttonSize, y + buttonSize + buttonPadding, buttonSize - buttonPadding, buttonSize - buttonPadding - buttonPadding)
End
	
Function DrawAnalog:Void(canvas:Canvas, x:Float, y:Float, radius:Float, axisX:Float, axisY:Float, pressed:Bool)
	Local padding:= Min(8.0, Max(1.0, radius * 0.1))
	
	canvas.SetColor(0.5, 0.5, 0.5)
	canvas.DrawCircle(x, y, radius)
	
	If pressed
		canvas.SetColor(0, 1.0, 0)
		canvas.DrawCircle(x, y, radius - padding)
	EndIf
	
	canvas.SetColor(0.3, 0.3, 0.3)
		
	Local length:= Sqrt( (axisX * axisX) + (axisY * axisY))
	Local size:= Min(19.0, Max(7.0, radius * 0.1))
	Local cursorX:Float
	Local cursorY:Float
		
	If length > 0
		Local ratio:Float
		If Abs(axisX) > Abs(axisY)
			ratio = Abs(axisX / 1.0) / length
		Else
			ratio = Abs(axisY / 1.0) / length
		EndIf
			
		cursorX = axisX * radius * ratio
		cursorY = axisY * radius * ratio
	EndIf
		
	DrawCross(canvas, x + cursorX - (size / 2.0), y + cursorY - (size / 2.0), size, size)
End

Function DrawCross:Void(canvas:Canvas, x:Float, y:Float, width:Float, height:Float, diagonal:Bool = True)
	' --- draw a cross ---
	If diagonal
		canvas.DrawLine(x, y, x + width, y + height)
		canvas.DrawLine(x + width, y, x, y + height)
	Else
		Local halfWidth:= width / 2.0
		Local halfHeight:= height / 2.0
		canvas.DrawLine(x + halfWidth, y, x + halfHeight, y + height)
		canvas.DrawLine(x, y + halfHeight, x + width, y + halfHeight)
	EndIf
End
	
Function DrawButton:Void(canvas:Canvas, x:Float, y:Float, radius:Float, pressed:Bool)
	Local padding:= Min(8.0, Max(1.0, radius * 0.1))
	
	canvas.SetColor(0.5, 0.5, 0.5)
	canvas.DrawCircle(x, y, radius)
	
	If pressed
		canvas.SetColor(0, 1.0, 0)
		canvas.DrawCircle(x, y, radius - padding)
	EndIf
End

Function DrawButton:Void(canvas:Canvas, x:Float, y:Float, width:Float, height:Float, pressed:Bool)
	Local padding:= Min(8.0, Max(1.0, width * 0.05))
	
	canvas.SetColor(0.5, 0.5, 0.5)
	canvas.DrawRect(x, y, width, height)
	
	If pressed
		canvas.SetColor(0, 1.0, 0)
		canvas.DrawRect(x + padding, y + padding, width - padding - padding, height - padding - padding)
	EndIf
End



Nobuyuki(Posted 2015) [#2]
interesting. JoyZ seems to have changed in your fork; It's nice to have access to the independent trigger axes here, but I'm not sure this is supported on all desktop targets! (glfw3 still uses the ancient windows 3.1 way of detecting a joypad AFAIK and so you get the legacy combined JoyZ axis)

Would be cool if xinput was supported on all desktop targets, lol.....


Skn3(Posted 2015) [#3]
To separate the triggers, just do:
triggerLeft = Max(0.0, JoyZ(0))
triggerRight = Max(0.0, JoyZ(1))


I have updated the code so that the html5 native emulates the windows GLFW behaviour.

[edit]
Oh no, if you hold both triggers it fails... hmm!

Reverted the changes to keep Z axis separate!


ImmutableOctet(SKNG)(Posted 2015) [#4]
@Skn3: That's because GLFW uses WinMM (On Windows), and similar setups for other platforms. So, for (Some of) those APIs, Z is a proper shared-axis. XInput, on the other hand, doesn't do this. That's why it's different on the XNA target (Completely separate axes). This isn't really a bug, as much as it is a target-specific quirk. Having the option to change it would still be a good idea, though. Although, it should be noted that what GLFW effectively does is take the other axis and negate it from the current axis.

Something akin to this:


Again, it's target-specific, but keeping things consistent is always better. Slightly more specific fix.


Skn3(Posted 2015) [#5]
I notice that it has been an issue on GLFW issues for quite a while. Seems like quite a game breaker missing feature..?

Posting cross reference to your xinput module for future reference.

http://www.monkey-x.com/Community/posts.php?topic=8506&post=110945


Nobuyuki(Posted 2015) [#6]
Yes, xinput has been on some mystical glfw roadmap to infinity for quite some time. I'm guessing that most people just implement it themselves when working with glfw directly, seeing as how WinMM has a load of limitations, and support is getting worse with each OS release (GLFW broke my sf4 fightpad so bad on Windows 8 that it can't be calibrated to work properly and the only workaround is to detect the bad inputs and disable joysticks completely).

https://github.com/glfw/glfw/issues/232

Edit: It would be cool once GLFW 3.2 is out to unify the joystick support with things like the separate z triggers, and player/home button polling. GLFW 3.1 already allows polling more modifier keys (finally), so again it would be nice to see an update to mojo letting us catch those keys as well. (I may patch this in myself if it's not too difficult...)


Skn3(Posted 2015) [#7]
Yeah even if 3.2 doesn't get xinput support, it would be sensible to unify it from monkey/native code at least.


arawkins(Posted 2015) [#8]
Hey, just wanted to say thanks for this, I am working on a local multiplayer game right now and being able to test gamepads in html5 is awesome :)


Neuro(Posted 2015) [#9]
Perfect! Just what i needed since that GLFW target was a bit of a pain to test gamepads...