8-Way Directional Movement Module

What this script does

The purpose is simple, but the usual solution is quite an ouroboros of functions and if statements if you don't know how to structure it based on logic or how modules work.

This ModuleScript returns a function which sets up 2 listeners: InputBegan and InputEnded. These listeners are connected once when the function is required, and they listen only for your specified movement keys (W, A, S, and D, defined in the keyMap table).

When those keys are pressed or released, the listeners write to a proxy table, which uses a __newindex metamethod to change values in the real MovementKeys table, defined outside of the returned function scope. Based on logical conditions, the metamethod updates currentDirection accordingly.

If you’re pressing opposite keys (e.g., A and D or W and S), the corresponding axis is considered inactive.

Finally, the function returns the currentDirection string, which you can access in any LocalScript. I don’t think I need to explain the use of this script—if you’re here, you probably already know why you need it.

local UIS = game:GetService("UserInputService")

local MovementKeys = {
	W = false;
	S = false;
	A = false;
	D = false;
}

local listenersCalled
local currentDirection = "None"

return function()
	
	local proxy = setmetatable({},{
		__newindex = function(_, k, v)

			MovementKeys[k] = v
			
			local InactiveMovementAxis = {
				Y = MovementKeys.W == MovementKeys.S;
				X = MovementKeys.A == MovementKeys.D;
			}

			if InactiveMovementAxis.Y and not InactiveMovementAxis.X then
				currentDirection = MovementKeys.A and "Left" or currentDirection
				currentDirection = MovementKeys.D and "Right" or currentDirection
			end
			
			if InactiveMovementAxis.X and not InactiveMovementAxis.Y then
				currentDirection = MovementKeys.W and "Up" or currentDirection
				currentDirection = MovementKeys.S and "Down" or currentDirection
			end
			
			if not InactiveMovementAxis.X and not InactiveMovementAxis.Y then
				currentDirection = MovementKeys.W and MovementKeys.D and "UpRight" or currentDirection
				currentDirection = MovementKeys.W and MovementKeys.A and "UpLeft" or currentDirection
				currentDirection = MovementKeys.S and MovementKeys.D and "DownRight" or currentDirection
				currentDirection = MovementKeys.S and MovementKeys.A and "DownLeft" or currentDirection
			elseif InactiveMovementAxis.X and InactiveMovementAxis.Y then
				currentDirection = "None"
			end
		end,
	})
	
	local keyMap = {
		[Enum.KeyCode.W] = "W";
		[Enum.KeyCode.A] = "A";
		[Enum.KeyCode.S] = "S";
		[Enum.KeyCode.D] = "D";
	}
	
	if listenersCalled then return currentDirection end
	
	UIS.InputBegan:Connect(function(input)
		if not keyMap[input.KeyCode] then return end
		proxy[keyMap[input.KeyCode]] = true
	end)
	
	UIS.InputEnded:Connect(function(input)
		if not keyMap[input.KeyCode] then return end
		proxy[keyMap[input.KeyCode]] = false
	end)
	
	listenersCalled = true
	
	return currentDirection
end

Organisation

Incase you don't know how module scripts work, here's where to put it and how to require it:

Create a module script inside an accessible folder (most commonly ReplicatedStorage). Create a local script, which you put in either the player folders or StarterGui, then you require them like this:

local keyHandler = require(game:GetService("ReplicatedStorage").KeyHandler)

In order to use it, you can index it, but keep in mind that it's a FUNCTION, so you need to put brackets (parentheses), otherwise it's only gonna return the function ID

print(keyHandler()) --Prints the currentDirection string value

Let me know if you have any improvements and use this script with grace

1 Like

No idea where you got that idea, how do you expect people to know what this is without explaining it.

2 Likes

Ok, firstly sorry, this is my first post I’m not fully familiar with how the forum works. Secondly, I explained what the script does. It returns a string value with the direction of where the player is supposed to be going. It’s pretty self explanatory, the idea is to prevent having to make value instances and look for them in every script where you’ll need them. You just require this module and index the function to get the returned value. If you want an example of a use case, imagine that you have attached a velocity or vectorforce to your character and you wanna change its direction based on the keys you’re pressing. I’m personally using this for a 2D game project but you can use it for anything involving 8-way movement. I hope that clears it up

1 Like

Hi, that’s an interesting module, but I have some questions:

Wouldn’t that connect to input events on every function call? (Forget it, I just saw it has a protection for that, my mistake for missing it at first).

Why not return a signal to connect to, or maybe have a callback be passed to the function instead?

Also, I don’t mean to say that your approach is wrong, it’s just that since the title was very broad (which can be a bit misleading), so I expected it would do a bit more.

Thank you very much for pointing this out! I worked on this yesterday all day and I was really tired so I completely forgot that I’ll need a callback as well. I decided to use a bindable event,
here’s the updated version:

local UIS = game:GetService("UserInputService")

local MovementKeys = {
	W = false;
	S = false;
	A = false;
	D = false;
}

local callback = Instance.new("BindableEvent")
local listenersCalled
local currentDirection = "None"

return function(cb)
	
	local proxy = setmetatable({},{
		__newindex = function(_, k, v)

			MovementKeys[k] = v
			
			local InactiveMovementAxis = {
				Y = MovementKeys.W == MovementKeys.S;
				X = MovementKeys.A == MovementKeys.D;
			}

			if InactiveMovementAxis.Y and not InactiveMovementAxis.X then
				currentDirection = MovementKeys.A and "Left" or currentDirection
				currentDirection = MovementKeys.D and "Right" or currentDirection
			end
			
			if InactiveMovementAxis.X and not InactiveMovementAxis.Y then
				currentDirection = MovementKeys.W and "Up" or currentDirection
				currentDirection = MovementKeys.S and "Down" or currentDirection
			end
			
			if not InactiveMovementAxis.X and not InactiveMovementAxis.Y then
				currentDirection = MovementKeys.W and MovementKeys.D and "UpRight" or currentDirection
				currentDirection = MovementKeys.W and MovementKeys.A and "UpLeft" or currentDirection
				
				currentDirection = MovementKeys.S and MovementKeys.D and "DownRight" or currentDirection
				currentDirection = MovementKeys.S and MovementKeys.A and "DownLeft" or currentDirection
			elseif InactiveMovementAxis.X and InactiveMovementAxis.Y then	
				currentDirection = "None"		
			end
			
			callback:Fire(currentDirection)
		end,
	})
	
	local keyMap = {
		[Enum.KeyCode.W] = "W";
		[Enum.KeyCode.A] = "A";
		[Enum.KeyCode.S] = "S";
		[Enum.KeyCode.D] = "D";
	}
	
	if cb then return callback end
	if listenersCalled then return currentDirection end
	
	UIS.InputBegan:Connect(function(input)
		
		if not keyMap[input.KeyCode] then return end
		proxy[keyMap[input.KeyCode]] = true
		
	end)
	
	UIS.InputEnded:Connect(function(input)
		
		if not keyMap[input.KeyCode] then return end	
		proxy[keyMap[input.KeyCode]] = false
		
	end)
	
	listenersCalled = true
	
	return currentDirection
	
end

The functionality now works a bit differently. If you write keysHandler() like this with just the parentheses, you’ll get the currentDirection value. (Note that this needs an initialization, but I’ve worked on pretty much no other modules than this one as it is my first proper one, so I didn’t think about that when I made it. Definitely make an initialization function in another module to run the function first and start the listeners). Then if you write keysHandler(true) you’ll get the BindableEvent. Here a simple example:

local keyshandler = require(game:GetService("ReplicatedStorage").Modules.Client.KeysHandler)
keyshandler() --The initialization
keyshandler(true).Event:Connect(function() -- the BindableEvent
	print(keyshandler()) -- the Direction string
end)

As for the title, I know what you mean, but I wasn’t sure how to name it. I’m still learning and I’m not that familiar with terminology. I’ll update the post if that’s all