Trouble with Mouse Delta angle rounding

Howdy,

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.

CurrentLayout

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 :slight_smile:

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)

Thanks in advance.

1 Like

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.

1 Like

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. :slightly_smiling_face:

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.

Let me know if this works for you

1 Like

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!

1 Like

Sorry if it looks like I’m begging, I’m not I’m just asking if you can help me with my issue or not.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.