Improved Joystick to Keyboard remap script

Post your working scripts, libraries and tools for AHK v1.1 and older
User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Improved Joystick to Keyboard remap script

17 Jan 2016, 13:43

I noticed that the joystick remap script sample snippets were very basic. Whilst they showed how to get information from sticks, they were not very functional (eg No support for diagonals, not easy to configure, etc), not very elegant (Different code to process X and Y etc) and not very efficient (Repeat concatenations etc).

So I wrote my own.
This one supports remapping of axes, buttons and hats to keyboard (or mouse) buttons, and is fairly easy to configure, you just have to learn how to edit an Indexed Array.

Techniques used in this script show how to:
  • Handle X and Y input of a stick with the same code, through the use of Arrays and Loops.
  • Extract axis state from the POV's degree-based values through the use of pre-built Associative Arrays.
  • Emulate the up event for joystick buttons.
  • Make Joystick axis values easier to work with by shifting the scale from 0->100 to -50->+50, and use of the math function abs() to get magnitude of deflection.
  • Use "function binding" to cause a function to get called when a button is pressed, and have that function be passed the number of the button that was pressed.
  • Create an array of pre-built strings for use with GetKeyState(), to reduce CPU load.
The default configuration remaps:
X and Y axes to the Arrow Keys (With the "AxisRepeat" option on, so keys get repeatedly hit when you hold the joystick) with a 10% deadzone.
Buttons 1-8 to number keys 1-8.
POV hat to WSAD keys.

Code: Select all

#SingleInstance force
#Persistent
; === Edit these vars to set up the script to your liking ===
InputStick := 2												; The ID of your input stick
DeadZone := 10												; Percentage of deadzone
AxisKeysX := ["Left", "Right"]								; Array of keys to map to the X axis
AxisKeysY := ["Up", "Down"]									; Array of keys to map to the Y axis
AxisRepeat := 1												; Set to 1 to repeat down event for keys
HatKeysX := ["a","d"]										; Array of keys to map to the POV hat X axis
HatKeysY := ["w","s"]										; Array of keys to map to the POV hat Y axis
ButtonKeys := ["1","2","3","4","5","6","7","8"]				; Array of keys to map to the Joystick buttons

Axes := ["X", "Y"]											; Names of Axes to remap
AxisKeys := [AxisKeysX, AxisKeysY]							; Build lookup for Axis Keys
AxisStates := [0, 0]										; Current state of each input axis
DZ := DeadZone / 2											; Convert % to number of units (Range is 0 to +50)
if (HatKeysX.length() && HatKeysY.length()){
	HatKeys := [HatKeysX, HatKeysY]							; Build lookup for hat keys
	HatEnabled := 1
} else {
	HatEnabled := 0
}
HatState := [0,0]											; The current state of the hat X and Y axes
; Build an "Associative Array" map of hat angles to X and Y directions
HatMap := {-1: [0,0], 0: [0,1], 4500: [2,1], 9000: [2,0], 13500: [2,2], 18000: [0,2], 22500: [1,2], 27000: [1,0], 31500: [1,1]}
; Pre-assemble GetKeyState strings for performance optimization
JoyString := InputStick "Joy"
AxisStrings := []
Buttonstrings := []
HatString := JoyString "POV"
Loop 2 {
	AxisStrings.push(JoyString Axes[A_Index])
}

; Bind Buttons
Loop % ButtonKeys.length(){
	fn := Func("ButtonPressed").Bind(A_Index)
	hotkey, % JoyString A_Index, % fn
	Buttonstrings[A_Index] := JoyString A_Index				; Pre-assemble GetKeyState string for performance optimization
}
; Watch Axes and Hats
SetTimer, WatchStick, 5
return

WatchStick:
	; === Axes ===
	; Loop through axes. 1st loop is X, 2nd is Y
	Loop 2 {
		value := (GetKeyState(AxisStrings[A_Index])) - 50	; Range is now -50 to +50
		if (abs(value) < DZ)				; Enforce DeadZone setting
			value := 0
		; Work out current state of axis
		if (value = 0)
			state := 0
		else if (value < 0)
			state := 1
		else
			state := 2
		; Has the state changed?
		diff_state := ( AxisStates[A_Index] != state )
		; If the state changed, and wasn't "centered", then release the old key
		if (AxisStates[A_Index] && diff_state)
			Send % "{" AxisKeys[A_Index, AxisStates[A_Index]] " up}"
		
		; If the new state is not "centered", and is different to the old state (or AxisRepeat is 1), then press the new key
		if (state && (AxisRepeat || diff_state))
			Send % "{" AxisKeys[A_Index, state] " down}"
		
		; Update the States array
		AxisStates[A_Index] := state
	}
	
	; === Hat ===
	if (!HatEnabled)
		return
	state := GetKeyState(HatString)
	; Process Hat X axis then Y Axis
	Loop 2 {
		new_state := HatMap[state,A_Index]
		old_state := HatState[A_Index]
		if (old_state != new_state){
			if (old_state)
				Send % "{" HatKeys[A_Index, old_state] " up}"
			if (new_state)
				Send % "{" HatKeys[A_Index, new_state] " down}"
			HatState[A_Index] := new_state
		}
	}
	return

; Remap buttons, and make up event of buttons fire when button is actually released
ButtonPressed(btn){
	global ButtonKeys, InputStick, Buttonstrings
	Send % "{" ButtonKeys[btn] " down}"
	while(GetKeyState(Buttonstrings[btn])){
		Sleep 10
	}
	Send % "{" ButtonKeys[btn] " up}"
}
Edited:
Fixed bug with HatKeys and AxisKeys being mixed up
Fixed commented out send strings. Whoops!
Removed maths from hat direction calculations, used associative array.
Last edited by evilC on 21 Feb 2018, 10:56, edited 1 time in total.
User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: Improved Joystick to Keyboard remap script

17 Jan 2016, 15:43

Or if you want to avoid all string concatenation while processing input:

Code: Select all

#SingleInstance force
#Persistent
; === Edit these vars to set up the script to your liking ===
InputStick := 2												; The ID of your input stick
DeadZone := 10												; Percentage of deadzone
AxisKeysX := ["Left", "Right"]								; Array of keys to map to the X axis
AxisKeysY := ["Up", "Down"]									; Array of keys to map to the Y axis
AxisRepeat := 1												; Set to 1 to repeat down event for keys
HatKeysX := ["a","d"]										; Array of keys to map to the POV hat X axis
HatKeysY := ["w","s"]										; Array of keys to map to the POV hat Y axis
ButtonKeys := ["1","2","3","4","5","6","7","8"]				; Array of keys to map to the Joystick buttons

Axes := ["X", "Y"]											; Names of Axes to remap
AxisKeys := []
AxisKeys[1] := [{down: "{" AxisKeysX[1] " down}", up: "{" AxisKeysX[1] " up}"},{down: "{" AxisKeysX[2] " down}", up: "{" AxisKeysX[2] " up}"}]
AxisKeys[2] := [{down: "{" AxisKeysY[1] " down}", up: "{" AxisKeysY[1] " up}"},{down: "{" AxisKeysY[2] " down}", up: "{" AxisKeysY[2] " up}"}]

AxisStates := [0, 0]										; Current state of each input axis
DZ := DeadZone / 2											; Convert % to number of units (Range is 0 to +50)
if (HatKeysX.length() && HatKeysY.length()){
	HatKeys := []
	HatKeys[1] := [{down: "{" HatKeysX[1] " down}", up: "{" HatKeysX[1] " up}"},{down: "{" HatKeysX[2] " down}", up: "{" HatKeysX[2] " up}"}]
	HatKeys[2] := [{down: "{" HatKeysY[1] " down}", up: "{" HatKeysY[1] " up}"},{down: "{" HatKeysY[2] " down}", up: "{" HatKeysY[2] " up}"}]
	HatEnabled := 1
} else {
	HatEnabled := 0
}
HatState := [0,0]											; The current state of the hat X and Y axes
; Build an "Associative Array" map of hat angles to X and Y directions
HatMap := {-1: [0,0], 0: [0,1], 4500: [2,1], 9000: [2,0], 13500: [2,2], 18000: [0,2], 22500: [1,2], 27000: [1,0], 31500: [1,1]}
; Pre-assemble GetKeyState strings for performance optimization
JoyString := InputStick "Joy"
AxisStrings := []
HatString := JoyString "POV"
Loop 2 {
	AxisStrings.push(JoyString Axes[A_Index])
}
ButtonStrings := []
ButtonKeyStrings := []
Loop % ButtonKeys.length(){
	ButtonStrings[A_Index] := JoyString A_Index
	ButtonKeyStrings[A_Index] := {down: "{" ButtonKeys[A_Index] " down}", up: "{" ButtonKeys[A_Index] " up}"}
}

; Bind Buttons
Loop % ButtonKeys.length(){
	fn := Func("ButtonPressed").Bind(A_Index)
	hotkey, % JoyString A_Index, % fn
}
; Watch Axes and Hats
SetTimer, WatchStick, 5
return

WatchStick:
	; === Axes ===
	; Loop through axes. 1st loop is X, 2nd is Y
	Loop 2 {
		value := (GetKeyState(AxisStrings[A_Index])) - 50	; Range is now -50 to +50
		if (abs(value) < DZ)				; Enforce DeadZone setting
			value := 0
		; Work out current state of axis
		if (value = 0)
			state := 0
		else if (value < 0)
			state := 1
		else
			state := 2
		; Has the state changed?
		diff_state := ( AxisStates[A_Index] != state )
		; If the state changed, and wasn't "centered", then release the old key
		if (AxisStates[A_Index] && diff_state)
			Send % AxisKeys[A_Index, AxisStates[A_Index]].up
			
		; If the new state is not "centered", and is different to the old state (or AxisRepeat is 1), then press the new key
		if (state && (AxisRepeat || diff_state))
			Send % AxisKeys[A_Index, state].down
		
		; Update the States array
		AxisStates[A_Index] := state
	}
	
	; === Hat ===
	if (!HatEnabled)
		return
	state := GetKeyState(HatString)
	; Process Hat X axis then Y Axis
	Loop 2 {
		new_state := HatMap[state,A_Index]
		old_state := HatState[A_Index]
		if (old_state != new_state){
			if (old_state)
				Send % HatKeys[A_Index, old_state].up
			if (new_state)
				Send % HatKeys[A_Index, new_state].down
			HatState[A_Index] := new_state
		}
	}
	return

; Remap buttons, and make up event of buttons fire when button is actually released
ButtonPressed(btn){
	global ButtonKeys, InputStick, Buttonstrings, ButtonKeyStrings
	Send % ButtonKeyStrings[btn].down
	while(GetKeyState(Buttonstrings[btn])){
		Sleep 10
	}
	Send % ButtonKeyStrings[btn].up
}
Last edited by evilC on 21 Feb 2018, 10:56, edited 1 time in total.
nwalker83
Posts: 2
Joined: 25 Jan 2016, 19:11

Re: Improved Joystick to Keyboard remap script

25 Jan 2016, 19:20

Thank you for the post. I'd like to modify the script so that the remap only works with certain application being the foreground process. I tried to add an #IfWinActive line but that didn't seem to work (all the applications are always affected still). Ultimately I would like to apply different remaps for different applications...
User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: Improved Joystick to Keyboard remap script

26 Jan 2016, 04:04

AFAIK, #IfWinActive does not affect GetKeyState(), which is what this script uses. It can be done though.

At the start of WatchStick:, you could put:

Code: Select all

if (!WinActive("Title"))
   return
nwalker83
Posts: 2
Joined: 25 Jan 2016, 19:11

Re: Improved Joystick to Keyboard remap script

26 Jan 2016, 16:47

Thank you, evilC! I see from your GitHub page you're actually developing a URC program that will cover my use case also. Do you think it is ready for general consumption?
tetsuya_tsurugi

Re: Improved Joystick to Keyboard remap script

20 Feb 2018, 19:49

Can you write a scripts for the triggers (I have a xbox360 controller)?. I try to modify the script to include LT and RT but no success.

thanks
User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: Improved Joystick to Keyboard remap script

21 Feb 2018, 05:44

nwalker83 wrote:Thank you, evilC! I see from your GitHub page you're actually developing a URC program that will cover my use case also. Do you think it is ready for general consumption?
Both the old AHK UCR (See link in signature below) and the new C# UCR do joystick remappings.
tetsuya_tsurugi wrote:Can you write a scripts for the triggers (I have a xbox360 controller)?. I try to modify the script to include LT and RT but no success.thanks
Can you be more specific? What did not work? Bear in mind that if you want to read an xbox controller via AHK, when you read the triggers you can only read them as one axis. Using the built-in AHK commands, it is impossible to tell the difference between no triggers pressed and both triggers pressed.
If you want to write code to support triggers, you can use Lexikos' XInput Library.
If you just want to do straight remappings, look at my UCR-C# project linked above
tetsuya_tsurugi

Re: Improved Joystick to Keyboard remap script

21 Feb 2018, 08:13

Can you be more specific? What did not work? Bear in mind that if you want to read an xbox controller via AHK, when you read the triggers you can only read them as one axis. Using the built-in AHK commands, it is impossible to tell the difference between no triggers pressed and both triggers pressed.
If you want to write code to support triggers, you can use Lexikos' XInput Library.
If you just want to do straight remappings, look at my UCR-C# project linked above
Well, The triggers are axis Z. In your code, this axis don't exist. I tried with no success add this axis in the code, but I'm not a programmer, I try to do so just for logic and reading. One of my problem is in triggers, "null state" is 50, no 0.
User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: Improved Joystick to Keyboard remap script

21 Feb 2018, 10:58

Hmm, looking back at the code, if you want to use axes other than X and Y, you need to edit the section under the comment ; === Internal vars - do not edit ===, so I removed that comment.
tetsuya_tsurugi

Re: Improved Joystick to Keyboard remap script

21 Feb 2018, 19:00

Well... I did some modifications and finaly I can config triggers buttons.

This is my code:

Code: Select all

#SingleInstance force
#Persistent
; === Edit these vars to set up the script to your liking ===
InputStick := 1												; The ID of your input stick
DeadZone := 60												; Percentage of deadzone
AxisKeysX := ["Left", "Right"]								; Array of keys to map to the X axis
AxisKeysY := ["Up", "Down"]									; Array of keys to map to the Y axis
AxisKeysZ := ["RT", "LT"]										; Array of keys to map to the Z axis (triggers)
AxisRepeat := 1												; Set to 1 to repeat down event for keys
HatKeysX := ["a","d"]										; Array of keys to map to the POV hat X axis
HatKeysY := ["w","s"]										; Array of keys to map to the POV hat Y axis
ButtonKeys := ["1","2","3","4","5","6","7","8","9","10"]	; Array of keys to map to the Joystick buttons

; === Internal vars - do not edit ===
Axes := ["X", "Y", "Z"]											; Names of Axes to remap
AxisKeys := [AxisKeysX, AxisKeysY, AxisKeysZ]							; Build lookup for Axis Keys
AxisStates := [0, 0, 0]										; Current state of each input axis
DZ := DeadZone / 2											; Convert % to number of units (Range is 0 to +50)
if (HatKeysX.length() && HatKeysY.length()){
	HatKeys := [HatKeysX, HatKeysY]							; Build lookup for hat keys
	HatEnabled := 1
} else {
	HatEnabled := 0
}
HatState := [0,0]											; The current state of the hat X and Y axes
; Build an "Associative Array" map of hat angles to X and Y directions
HatMap := {-1: [0,0], 0: [0,1], 4500: [2,1], 9000: [2,0], 13500: [2,2], 18000: [0,2], 22500: [1,2], 27000: [1,0], 31500: [1,1]}
; Pre-assemble GetKeyState strings for performance optimization
JoyString := InputStick "Joy"
AxisStrings := []
Buttonstrings := []
HatString := JoyString "POV"
Loop 3 {
	AxisStrings.push(JoyString Axes[A_Index])
}

; Bind Buttons
Loop % ButtonKeys.length(){
	fn := Func("ButtonPressed").Bind(A_Index)
	hotkey, % JoyString A_Index, % fn
	Buttonstrings[A_Index] := JoyString A_Index				; Pre-assemble GetKeyState string for performance optimization
}
; Watch Axes and Hats
SetTimer, WatchStick, 5
return

WatchStick:
	; === Axes ===
	; Loop through axes. 1st loop is X, 2nd is Y
	Loop 3 {
		value := (GetKeyState(AxisStrings[A_Index])) - 50	; Range is now -50 to +50
		if (abs(value) < DZ)				; Enforce DeadZone setting
			value := 0
		; Work out current state of axis
		if (value = 0)
			state := 0
		else if (value < 0)
			state := 1
		else
			state := 2
		; Has the state changed?
		diff_state := ( AxisStates[A_Index] != state )
		; If the state changed, and wasn't "centered", then release the old key
		if (AxisStates[A_Index] && diff_state)
			Send % "{" AxisKeys[A_Index, AxisStates[A_Index]] " up}"
		
		; If the new state is not "centered", and is different to the old state (or AxisRepeat is 1), then press the new key
		if (state && (AxisRepeat || diff_state))
			Send % "{" AxisKeys[A_Index, state] " down}"
		
		; Update the States array
		AxisStates[A_Index] := state
	}
	
	; === Hat ===
	if (!HatEnabled)
		return
	state := GetKeyState(HatString)
	; Process Hat X axis then Y Axis
	Loop 2 {
		new_state := HatMap[state,A_Index]
		old_state := HatState[A_Index]
		if (old_state != new_state){
			if (old_state)
				Send % "{" HatKeys[A_Index, old_state] " up}"
			if (new_state)
				Send % "{" HatKeys[A_Index, new_state] " down}"
			HatState[A_Index] := new_state
		}
	}
	return

; Remap buttons, and make up event of buttons fire when button is actually released
ButtonPressed(btn){
	global ButtonKeys, InputStick, Buttonstrings
	Send % "{" ButtonKeys[btn] " down}"
	while(GetKeyState(Buttonstrings[btn])){
		Sleep 10
	}
	Send % "{" ButtonKeys[btn] " up}"
}
Thanks for your help and thanks for your amazing code :D
anti3d
Posts: 15
Joined: 31 Aug 2016, 05:48

Re: Improved Joystick to Keyboard remap script

18 Mar 2018, 10:18

tetsuya_tsurugi wrote:Well... I did some modifications and finaly I can config triggers buttons.
This works for me, thanks! Maybe you can help? I need analog turbo on gamepad. I don't understand how can i loop only one key?
tetsuya_tsurugi

Re: Improved Joystick to Keyboard remap script

11 Sep 2018, 09:42

Hello, it's me again :)

I recently bougth a 8bitdo SN pro and I want to play games using the improvemented script. It's works perfectly in wired conections, but it don't work in bluetooth mode. I ask you for help. How can I detect the pad in bluetooth mod to use ahk script?

once more thanks
tetsuya_tsurugi

Re: Improved Joystick to Keyboard remap script

11 Sep 2018, 15:14

Does appears in joy.cpl. I tried change ID from 1 to 16 searching correct one, but nothings happens

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: gwarble and 115 guests