Recently I’ve been interested in refactoring my game from static attack animation cycles to a dynamic cursor influenced system. When you move your cursor, it takes your mouse delta X & Y and adds it to a Vector2 until it reaches a specific magnitude value. In practice, easy to replicate angles such as left and right (-90, 90 respectively) are incredibly easy to replicate and will rarely “jitter,” while other angles such as the cleave in the diagram below are extremely hard to hit. The current system I have works generally, but sometimes the angle solver doesn’t get a precise enough angle, and will snap to an incorrect position.
My current code takes the dot product of the total delta, checks if it’s negative or positive, converts it to an angle and then tweens the cursor. Is there a more efficient, precise way to calculate this sort of angle? Any ideas or examples would be appreciated
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
-- this stuff will be in item modules, accessible to client
local attacks = {
[90] = "MiddleSweepRight";
[-90] = "MiddleSweepLeft";
[180] = "Cleave";
[45] = "45Test"
}
local attackAngles = {
90,-90,180,45
}
-- local player stuff
UserInputService.MouseIconEnabled = false
UserInputService.MouseDeltaSensitivity = .25
local previousPosition = Vector2.new(0, 0)
local angleTween = nil
local function roundNearestTable(inputTable, inputNumber:number)
table.sort(inputTable, function(Left, Right) return math.abs(inputNumber - Left) < math.abs(inputNumber - Right) end)
return inputTable[1]
end
local function averageOfNumbers(inputTable)
local total = 0
for _, value in pairs(inputTable) do total += value end return total/#inputTable
end
local function averageOfVectors(inputTable)
local total = Vector2.new(0,0)
for _, value in pairs(inputTable) do total += value end return total/#inputTable
end
local function totalOfVectors(inputTable)
local total = Vector2.new(0,0)
for _, value in pairs(inputTable) do total += value end return total
end
local function round(num, mult)
return math.floor(num / mult + 0.5) * mult
end
local MINIMUM_DISTANCE = 1
local MAXIMUM_DISTANCE = 10
local ROUND_ANGLE_DEGREE = 45
local MINIMUM_TIME_CHANGE = .25
local storeDelta = Vector2.new(0,0)
local lastChangedTime = 0
local lastAngle = 0
UserInputService.InputChanged:Connect(function(input:InputObject)
if input.UserInputType == Enum.UserInputType.MouseMovement then
local absSize = script.Parent.AbsoluteSize
local xAxisRatio = absSize.Y/absSize.X
local yAxisRatio = absSize.X/absSize.Y
-- Get delta change position
local changePosition = Vector2.new(input.Delta.X, input.Delta.Y)
storeDelta += changePosition
if storeDelta.Magnitude >= MAXIMUM_DISTANCE then
-- Calculate dot product and angle
local verticalDot = storeDelta.Unit:Dot(Vector2.new(0,-1))
local mouseAngle = round(math.deg(math.acos(verticalDot)),ROUND_ANGLE_DEGREE)
if storeDelta.X < 0 then mouseAngle *= -1 end
storeDelta = Vector2.new(0,0)
local foundIndex = roundNearestTable(attackAngles, mouseAngle)
if angleTween and angleTween.PlaybackState == Enum.PlaybackState.Playing then
angleTween:Pause()
end
if attacks[foundIndex] then
local angleTween = TweenService:Create(script.Parent.ImageLabel,TweenInfo.new(0.1,Enum.EasingStyle.Sine,Enum.EasingDirection.Out),{Rotation = foundIndex})
angleTween:Play()
end
end
end
end)
I’m wondering if it might be easier to just use the dot product and specify directional vectors instead of trying to convert the mouse movement to an angle. Really all you would need to do is perform :Dot on the mouse delta.Unit to each of your attack vectors, whichever one has the highest dot product is your best match. No rounding is needed either.
Here’s something quick I was able to come up with:
local userInputService = game:GetService('UserInputService')
local MIN_DISTANCE = 10 -- avoid accidental mouse movements; in pixels
local attacks = {
[Vector2.new(1, 0)] = 'MiddleSweepRight';
[Vector2.new(-1, 0)] = 'MiddleSweepLeft';
[Vector2.new(0, -1)] = 'Cleave';
[Vector2.new(1, 1).Unit] = '45Test';
}
userInputService.InputBegan:Connect(function(input)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
return
end
local originalPosition = userInputService:GetMouseLocation() -- use :GetMouseLocation because we need to use it later, it doesn't take into account the gui inset whereas input.Position does
local ended, didDoEnd
local function handleEnd()
didDoEnd = true
ended:Disconnect()
local mouseDirection = (userInputService:GetMouseLocation() - originalPosition)
if mouseDirection.Magnitude < MIN_DISTANCE then
return
end
mouseDirection = mouseDirection.Unit
local highestDot, highestAttackName = -math.huge, nil -- find the best match (aka the vector that's the closest in direction to mouseDirection)
for direction, attackName in attacks do
local dot = direction:Dot(mouseDirection)
if dot > highestDot then
highestDot = dot
highestAttackName = attackName
end
end
print('The best match was', highestAttackName)
end
ended = userInputService.InputEnded:Connect(function(input: InputObject)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
return
end
if not didDoEnd then
handleEnd()
end
end)
task.wait(0.5)
if didDoEnd then
return
end
handleEnd()
end)
Let me know if this suits your needs or if you still would like to go through with the angle method.
This system works well in third person, however it doesn’t work when in first person because the mouse is locked in the center (this is why I had to track mouse delta changes.) I should have included more details of my previous implementation; my system takes the mouse travel path, assigns it an angle, checks if it exists in the attack table, and then tweens the User Interface to show the user what attack they are going to perform if they click. (see video below)
The problem with this approach is that the smallest variation in the delta path (usually if your mouse travels one pixel in one frame) the angle value shoots from a smooth, believable angle to an angle on one of the major axis’s (90’s)
I’m going to see if there’s any adjustments I can make to your code in addition to looking over the documentation for UserInputService and see if there’s a better way to track the mouse direction change while in first person. If you think of any other solutions to this problem, I’m all ears.
Got you, I didn’t test it in first person so that’s an oversight on my end. For detecting the direction in first person I’d just add up the mouse deltas for a direction, and then just use that for the “compare” vector.
Just to clarify, The reason I’m using input.Delta only when the mouse is locked to the centre, (ie in first person), and not just sticking to one method (using delta for everything) is because input.Delta will only be non-zero when the mouse is locked.
Ex:
local userInputService = game:GetService('UserInputService')
local MIN_DISTANCE = 10 -- avoid accidental mouse movements; in pixels
local attacks = {
[Vector2.new(1, 0)] = 'MiddleSweepRight';
[Vector2.new(-1, 0)] = 'MiddleSweepLeft';
[Vector2.new(0, -1)] = 'Cleave';
[Vector2.new(1, 1).Unit] = '45Test';
}
userInputService.InputBegan:Connect(function(input)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
return
end
local position = input.Position
local totalDelta = Vector2.zero
local originalPosition = userInputService:GetMouseLocation() -- use :GetMouseLocation because we need to use it later, it doesn't take into account the gui inset whereas input.Position does
local ended, changed, didDoEnd
local isLocked = userInputService.MouseBehavior == Enum.MouseBehavior.LockCenter
local function handleEnd()
didDoEnd = true
ended:Disconnect()
if changed then
changed:Disconnect()
end
local mouseDirection
if isLocked then
mouseDirection = totalDelta
else
mouseDirection = (userInputService:GetMouseLocation() - originalPosition)
end
if mouseDirection.Magnitude < MIN_DISTANCE then
return
end
mouseDirection = mouseDirection.Unit
local highestDot, highestAttackName = -math.huge, nil
for direction, attackName in attacks do
local dot = direction:Dot(mouseDirection)
if dot > highestDot then
highestDot = dot
highestAttackName = attackName
end
end
print('The best match was', highestAttackName)
end
if isLocked then
changed = userInputService.InputChanged:Connect(function(input)
if input.UserInputType ~= Enum.UserInputType.MouseMovement then
return
end
local delta = input.Delta
totalDelta += Vector2.new(delta.X, delta.Y)
end)
end
ended = userInputService.InputEnded:Connect(function(input: InputObject)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
return
end
if not didDoEnd then
handleEnd()
end
end)
task.wait(0.5)
if didDoEnd then
return
end
handleEnd()
end)
Doing this (adding up the deltas) should also solve the issue you mentioned with the smallest variation in mouse movement, but you may need to ramp up MIN_DISTANCE as well.
Yeah, adding up the deltas and THEN checking the MIN_DISTANCE variable seems to be the best solution with the lowest amount of computations per frame. I tried something similar, doing a poll of the average angle by adding a certain amount of Vectors to a table, and once the table reached a certain amount of polls, calculate the angle and then check the average. I’ll continue to finick with this and tweak the values until it’s suitable for my use case. Thanks for the solution!