Ask and ye shall receive!
Download Mouse2Move.luaLanguage: lua
-- The sign of a scalar.
local function sign(x)
return (x>0) and 1 or ((x<0) and -1 or 0)
end
-- Sums over a vector.
local function sum(v)
local total=0
for i=1,#v do
total=total+v[i]
end
return total
end
-- Did we just press this key?
local function justpressed(keys, last, button)
return keys[button] and not(last[button])
end
-- Inner product of two vectors. If not the same length, just uses the shorter of the two. To capture the most recent values, multiplies only the last elements.
local function inner(v1, v2)
local prod = {}
for i=0,math.min(#v1, #v2)-1 do
table.insert(prod, v1[#v1-i]*v2[#v2-i])
end
return sum(prod)
end
-- Updates the mouse's history.
local function updatemouse(history)
local curr=input.getmouse()
table.remove(history, 1)
table.insert(history, curr.X)
return history
end
-- Returns the difference between adjacent x values, effectively the mouse velocity.
local function getdeltas(history)
local delta_x = {}
for i=1,#history-1 do
table.insert(delta_x, history[i+1]-history[i])
end
return delta_x
end
-- Returns a sort of weighted average of the mouse's movement based on the window function.
local function windowavg(v, window)
local denom = sum(window)
local num = inner(v, window)
return num/denom
end
-- Makes a window function. Exponential with time.
local function makewindow(length, lambda)
local window={}
for i=1,length do
table.insert(window, math.exp(lambda*(i-length)))
end
return window
end
-- Updates the joypad input based on the threshold and its history. Also updates and returns the history.
local function gameinput(threshold, mouseavg, joyhist, joyavg)
local curr=false
if math.abs(mouseavg)>threshold*(1+math.abs(joyavg)) then -- "1+math.abs(joyavg)" is just a guess. It maybe needs to be refined.
curr=true
end
local lorr = sign(mouseavg)
local buttons = {"Right", [-1]="Left"}
if curr then
joypad.set({[buttons[lorr]]=curr})
end
table.remove(joyhist, 1)
table.insert(joyhist, curr and lorr or 0)
return joyhist
end
-- Adjusts a single parameter according to whether we press some keys.
local function adjustparam(param, keys, last, downkey, upkey, mini, maxi, interval, eps)
eps=eps or 0.0001
if justpressed(keys, last, upkey) then
param = (param>=maxi-eps) and maxi or param+interval
elseif justpressed(keys, last, downkey) then
param = (param<=mini+eps) and mini or param-interval
end
return param
end
local function updatevalues(last, mouselambda, joylambda, length, threshold, debugmode)
local keys = input.get()
mouselambda = adjustparam(mouselambda, keys, last, "Number6", "Number7", 0, 1, 0.1)
joylambda = adjustparam(joylambda, keys, last, "Y", "U", 0, 1, 0.1)
length = adjustparam(length, keys, last, "H", "J", 2, 10, 1)
threshold = adjustparam(threshold, keys, last, "N", "M", 10, 50, 2)
if justpressed(keys, last, "B") then
gui.clearGraphics()
debugmode = not(debugmode)
end
return keys, mouselambda, joylambda, length, threshold, debugmode
end
-- Initialization
local last = input.get()
local mouselambda = 0.2
local joylambda = 0.2
local length = 5
local threshold = 20
local mousestart = input.getmouse()
mousestart=mousestart.X
local mousehist, joyhist = {}, {}
local debugmode = false
for i=1,10 do -- Keep a history of the last 10 mouse locations and joypad buttons. Changing length only changes the window.
table.insert(mousehist, mousestart)
table.insert(joyhist, 0)
end
while true do
mousehist = updatemouse(mousehist)
delta_x = getdeltas(mousehist)
mousewindow = makewindow(length-1, mouselambda)
mouseavg = windowavg(delta_x, mousewindow)
joywindow = makewindow(length, joylambda) -- Note that the joypad window and mouse window are different lengths. I assume this either doesn't matter or makes very little difference.
joyavg = windowavg(joyhist, joywindow)
joyhist = gameinput(threshold, mouseavg, joyhist, joyavg)
emu.frameadvance()
keys, mouselambda, joylambda, length, threshold, debugmode = updatevalues(keys, mouselambda, joylambda, length, threshold, debugmode)
-- Print all the parameters and history for troubleshooting purposes.
if debugmode then
gui.drawRectangle(0,0,240,160,"gray","gray")
gui.pixelText(180,20,mouselambda)
gui.pixelText(180,28,joylambda)
gui.pixelText(180,36,length)
gui.pixelText(180,44,threshold)
for i=1,length-1 do
gui.pixelText(180,44+8*i,delta_x[i])
gui.pixelText(200,44+8*i,joyhist[i])
gui.pixelText(220,44+8*i,mousehist[i])
end
gui.pixelText(200,44+8*length,joyhist[length])
gui.pixelText(220,44+8*length,mousehist[length])
end
end
I've briefly run this in debug mode to verify that it is at least
mostly working, but you'll want to tweak the parameters yourself.
There are four main parameters: mouselambda, joylambda, length, and threshold.
- mouselambda affects how much we weight old mouse movements. Its default is 0.2. Set it to 0 and all mouse movements are equal. Its maximum value is 1, making it heavily weighted toward new movements. (Behind the scenes, each change in x is weighted by e^(-lambda) times the more recent change in x, so if lambda=1, the most recent delta-x gets a weight of 1 and the second most recent gets a weight of just 0.368, quickly going to zero.) To raise or lower its value, press 7 or 6, respectively.
- joylambda is much like mouselambda, deciding how much we weight toward recent joypad inputs. Its default is 0.2. Set it to 0 and all joypad presses are equal. Its maximum value is 1, producing the same weighting strongly toward new joypad movements in exactly the same way as the mouselambda parameter. To raise or lower its value, press u or y, respectively.
- length is how far back into the history we look. I keep the last ten values of each but truncate the window to this length. Its default is 5. Set it to 2 and only the most recent mouse movement matters (and if it's working correctly, the two lambda parameters are then irrelevant). Its maximum value is 10, which might cause a noticeable time lag, especially if your lambdas are small. To raise or lower its value, press j or h, respectively.
- threshold is basically how far you have to move the mouse over the course of one frame in order for the script to register a left or right button press. This is the parameter I'm least certain about. I've allowed it to range from 10 to 50, with 10 being very sensitive to mouse movements and 50 being very insensitive. Its default is 20 and you can raise or lower its value in increments of 2 by pressing m or n, respectively. I also made a guess as to how it might be implemented, with the mouse movement needing to exceed the threshold plus the weighted average of previous joypad presses.
- Additionally, I've included a "debug mode", which can be accessed by pressing b. This displays all parameters and recent mouse movements, joypad buttons, and mouse positions.
Very basically, setting mouselambda=0, joylambda=0, length=10, and threshold=50 makes input
weak and
laggy. Setting mouselambda=1, joylambda=1, length=2, and threshold=10 makes input
strong and
twitchy.
Having said all that, I have an assignment for you: Run the script with the game of your choice (Revolution X?) and freely change the parameters by pressing 7, 6, u, y, j, h, m, and n until it's working to your satisfaction. Then switch to debug mode and tell me what the parameters are so I can change the defaults, ranges, and intervals.
If there are any basic issues with the program, such as wanting more or less maximum sensitivity, more fine tuning, or a longer history, I can make those changes easily so don't be afraid to tell me about them! If there are more complex issues, please bring them to my attention anyway and maybe I'll discover there's something simple wrong with my assumptions or implementation. But in general, no promises.
If we're lucky, the game accepts input frame-by-frame, rather than in pairs or with acceleration or something else that might affect how mouse movements translate to joypad movements. If that ends up being the case, troubleshooting my script will be rather difficult.
Hope this works right out of the box!
Edit: I've updated the threshold parameter to range from 10 to 50 in intervals of 2 because I perceived it wasn't sensitive enough. I also overhauled the updatevalues function, consolidating all the if statements into a new adjustparam function.
Edit 2: I probably should have looked up a video of the game before starting this. For some reason, I assumed Revolution X was a top-down shoot-'em-up with the ship fixed at the bottom of the screen. As such, I only programmed in motion for the x axis. Oops. Adding in y axis functionality probably won't take all that long, but I kind of want to take a break so please be patient.