How to handle keys in module scripts?

Hi, I am trying to make abilities for certain classes and to use the abilities you can press a key to activate them. I am trying to do all this inside a module script and each class will have a different module script that handles the abilities.

Problem is I don’t know how to detect if a key is pressed in a module script.

MainModule

local UserInputService = game:GetService("UserInputService")

local Archer = {}
Archer.__index = Archer

function Archer.new(Player) 
	local self = setmetatable({},Archer)
	self.Player = Player
	
	return self
end

function Archer:Initilize()
	print("INITILIZED: Archer Module")
	local InfoFolder = self.Player:FindFirstChild("Information")
	local PlayerMouse = self.Player:GetMouse()
	
	Archer:Inputs(self.Player)
	
end


function Archer:Inputs(Player)
	local InfoFolder = Player.Information
	UserInputService.InputBegan:Connect(function(Input,GameProcessed)
		if GameProcessed then return end
		if Input.KeyCode == Enum.KeyCode.Q then
			print("Move 1")
		end

		if Input.KeyCode == Enum.KeyCode.E then
			print("Move 2")
		end

		if Input.KeyCode == Enum.KeyCode.Z then
			print("Move 3")
		end

		if Input.KeyCode == Enum.KeyCode.F then
			print("Ultimate")
		end
	end)
end



function Archer:Marksman()
	
end

function Archer:HomingArrow()
	
end

function Archer:ArrowBarrage()
	
end

return Archer

Hi!

You’ve mentioned you don’t know how to detect keys being pressed, so I am going to give you a portion of my code, and hopefully it will give you an idea of how to do it.

Hopefully it helps you out and you’ll know what to do next.


local UserInput = game:GetService("UserInputService") ---This is the user input service
local ActionController = {} ---This is where I store all my functions, I am using rojo + knit to manage my code.

---I have this function that handles actions binding.
function ActionController:BindAction(keyCode, stateName)
	UserInput.InputBegan:Connect(function(input, gameProcessed)
		if gameProcessed then
			return
		end
		
		if input.KeyCode ~= keyCode then
			return
		end
		
		if not (self.IsAlive and self.Humanoid) then ---Checks whether the player is alive, otherwise there's no point in playing actions.
			return
		end
		
		local canPlay = true
		
		if self.State ~= "None" then ---This is where I check if the player is already playing an action, if so, I undo it.
			canPlay = self.State ~= stateName
			self:UndoState()
		end
		
		if canPlay then ---If the player is not playing an action, I set the state and play the action.
			self.State = stateName
			self:PlayState(stateName) ---This is where you define your handler function
			---In my case, it looks up for the "stateName" inside a table and executes the function
		end
	end)
end

---This function essentially binds all actions at once, this is where I organize my "actions".
function ActionController:BindActions()
	local states = { ---This is how I map all keys and state names, these are essentially strings that are referenced for my handler function.
		Enum.KeyCode.Z, "At Ease",
		Enum.KeyCode.X, "Prone",
		Enum.KeyCode.C, "Crouch",
		Enum.KeyCode.V, "Salute",
		Enum.KeyCode.B, "Cross Arms"
	}
	
	for index = 1, #states, 2 do ---This is how I iterate through the table, I am using a for loop because I am lazy lol.
		self:BindAction(states[index], states[index + 1])
	end
end
1 Like

I think I’m doing this completely wrong nothing is printing for me and I’m not sure why

local UserInputService = game:GetService("UserInputService")

local ActionController = {}
local Archer = {}
Archer.__index = Archer

function Archer.new(Player) 
	local self = setmetatable({},Archer)
	self.Player = Player
	
	return self
end

function Archer:Initilize()
	print("INITILIZED: Archer Module")
	local InfoFolder = self.Player:FindFirstChild("Information")
	local PlayerMouse = self.Player:GetMouse()
	ActionController:BindActions()
end

function ActionController:BindAction(keyCode, stateName)
	UserInputService.InputBegan:Connect(function(input, gameProcessed)
		if gameProcessed then
			return
		end
		print("Test")
		if input.KeyCode ~= keyCode then
			return
		end

		if not (self.IsAlive and self.Humanoid) then ---Checks whether the player is alive, otherwise there's no point in playing actions.
			return
		end

		local canPlay = true

		if self.State ~= "None" then ---This is where I check if the player is already playing an action, if so, I undo it.
			canPlay = self.State ~= stateName
			self:UndoState()
		end

		if canPlay then ---If the player is not playing an action, I set the state and play the action.
			self.State = stateName
			self:PlayState(stateName) ---This is where you define your handler function
			---In my case, it looks up for the "stateName" inside a table and executes the function
		end
	end)
end

function ActionController:BindActions()
	local states = { ---This is how I map all keys and state names, these are essentially strings that are referenced for my handler function.
		Enum.KeyCode.Q, "Marksman",
		Enum.KeyCode.E, "Homing Arrow",
		Enum.KeyCode.Z, "Arrow Barrage",
		Enum.KeyCode.F, "Ultimate",
	}

	for index = 1, #states, 2 do ---This is how I iterate through the table, I am using a for loop because I am lazy lol.
		self:BindAction(states[index], states[index + 1])
	end
end

function Archer:PlayState(stateName)
	if stateName == "Marksman" then
		print("Marksman")
	end
	if stateName == "Homing Arrow" then
		print("Homing Arrow")
	end
	if stateName == "Arrow Barrage" then
		print("Arrow Barrage")
	end
	if stateName == "Ultimate" then
		print("Ultimate")
	end
end

function Archer:UndoState(stateName)
	print("Undoing state")
end

return Archer

Hi!

I’ve simplified your code and commented off some parts for testing, but hopefully you’ll know what to do next:

local UserInputService = game:GetService("UserInputService")

---Creates the action controller object
local Archer = {}
Archer.__index = Archer

function Archer.new(Player) 
	local self = setmetatable({}, Archer)
	self.Player = Player
	self.State = "None"

	return self
end

function Archer:Initilize()
	print("INITILIZED: Archer Module")
	---local InfoFolder = self.Player:FindFirstChild("Information")
	---local PlayerMouse = self.Player:GetMouse()
	local ArcherObj = Archer.new() ---Creates a new object
	ArcherObj:BindActions() ---Binds the actions
end

function Archer:BindAction(keyCode, stateName)
	UserInputService.InputBegan:Connect(function(input, gameProcessed)
		if gameProcessed then
			return
		end

		if input.KeyCode ~= keyCode then
			return
		end
		print("Input is NOT a game processed event and IS the bound key, continuing...")

		--[[
		if not (self.IsAlive and self.Humanoid) then ---Checks whether the player is alive, otherwise there's no point in playing actions.
			return
		end
		print("Player IS alive, continuing...")
		--]]

		---local canPlay = true
		
		if self.State ~= "None" then ---This is where I check if the player is already playing an action, if so, I undo it.
			self:UndoState()
		else
			self.State = stateName
			self:PlayState(stateName) ---This is where you define your handler function
		end
	end)
end

function Archer:BindActions()
	local states = { ---This is how I map all keys and state names, these are essentially strings that are referenced for my handler function.
		Enum.KeyCode.Q, "Marksman",
		Enum.KeyCode.E, "Homing Arrow",
		Enum.KeyCode.Z, "Arrow Barrage",
		Enum.KeyCode.F, "Ultimate",
	}

	for index = 1, #states, 2 do ---This is how I iterate through the table, I am using a for loop because I am lazy lol.
		self:BindAction(states[index], states[index + 1])
	end
end

function Archer:PlayState(stateName)
	if stateName == "Marksman" then
		print("Marksman")
	end
	if stateName == "Homing Arrow" then
		print("Homing Arrow")
	end
	if stateName == "Arrow Barrage" then
		print("Arrow Barrage")
	end
	if stateName == "Ultimate" then
		print("Ultimate")
	end
end

function Archer:UndoState(stateName)
	print("Undoing state")
	self.State = "None"
end

Archer:Initilize()
1 Like

Everything seems to be working except

UserInputService.InputBegan:Connect(function(input, gameProcessed)

I’m not really sure if it’s just studio but I tested it and it didn’t work for some reason. I checked if it was being initilized and everything but UserInputService.InputBegan:Connect(function(input, gameProcessed) didn’t run.

Here’s the code I runned

local UserInputService = game:GetService("UserInputService")

---Creates the action controller object
local Archer = {}
Archer.__index = Archer

function Archer.new(Player) 
	local self = setmetatable({}, Archer)
	self.Player = Player
	self.State = "None"

	return self
end

function Archer:Initilize()
	print("INITILIZED: Archer Module")
	---local InfoFolder = self.Player:FindFirstChild("Information")
	---local PlayerMouse = self.Player:GetMouse()
	local ArcherObj = Archer.new() ---Creates a new object
	ArcherObj:BindActions() ---Binds the actions
end

function Archer:BindAction(keyCode, stateName)
	print("Binding action: ".. stateName)
	UserInputService.InputBegan:Connect(function(input, gameProcessed)
		if gameProcessed then
			return
		end

		if input.KeyCode ~= keyCode then
			return
		end
		print("Input is NOT a game processed event and IS the bound key, continuing...")

		--[[
		if not (self.IsAlive and self.Humanoid) then ---Checks whether the player is alive, otherwise there's no point in playing actions.
			return
		end
		print("Player IS alive, continuing...")
		--]]

		---local canPlay = true

		if self.State ~= "None" then ---This is where I check if the player is already playing an action, if so, I undo it.
			self:UndoState()
		else
			self.State = stateName
			self:PlayState(stateName) ---This is where you define your handler function
		end
	end)
end

function Archer:BindActions()
	local states = { ---This is how I map all keys and state names, these are essentially strings that are referenced for my handler function.
		Enum.KeyCode.Q, "Marksman",
		Enum.KeyCode.E, "Homing Arrow",
		Enum.KeyCode.Z, "Arrow Barrage",
		Enum.KeyCode.F, "Ultimate",
	}

	for index = 1, #states, 2 do ---This is how I iterate through the table, I am using a for loop because I am lazy lol.
		self:BindAction(states[index], states[index + 1])
	end
end

function Archer:PlayState(stateName)
	if stateName == "Marksman" then
		print("Marksman")
	end
	if stateName == "Homing Arrow" then
		print("Homing Arrow")
	end
	if stateName == "Arrow Barrage" then
		print("Arrow Barrage")
	end
	if stateName == "Ultimate" then
		print("Ultimate")
	end
end

function Archer:UndoState(stateName)
	print("Undoing state")
	self.State = "None"
end
-- I removed the Archer:Initilize since it was already being initilized
return Archer

Output:

Studio
  19:06:02.145  INITILIZED: Main Menu  -  Server - MenuModule:13
  19:06:08.691  INITILIZED: Archer Module  -  Server - MainModule:16
  19:06:08.691  Binding action: Marksman  -  Server - MainModule:24
  19:06:08.691  Binding action: Homing Arrow  -  Server - MainModule:24
  19:06:08.691  Binding action: Arrow Barrage  -  Server - MainModule:24
  19:06:08.691  Binding action: Ultimate  -  Server - MainModule:24

Boosting this since I couldn’t find a answer

My personal preferred method of handling keys is to have a generic input controller that can watch for certain input triggers that the client gives. It can then call the relevant action in an activated class/character controller.

Instead of an inputs function, I would independently have skill functions in the class module. The controller class would listen on different inputs and accordingly call certain functions to my liking. If I wanted to build on additional inputs (e.g. one class double taps to perform ultimate, another only needs a single tap), the class controller receiving the inputs could track them so it can act accordingly. It’s also maintainable since it divides input handling from class/character handling.

You may want to consider structuring your code that way instead.

2 Likes

So if I am right, you’re saying I should have a client script that listen for when a input is triggered and call an action from the characters module.

But what’s wrong with using module scripts? Does it just not work for listening for keys?

It doesn’t have to be a LocalScript, it can be another ModuleScript. The main thing I’m talking about is the code itself, handling inputs should be divorced from any specific character module. You can create a generic handler that listens to inputs and gives it to the character modules to work with.

In short though yes, you want separate code that will listen for triggered input and then call actions from the active character module. If you have a permanent kit for all characters (all are expected to have 3 skills and an ultimate), for example, any press of the ultimate key will always call the ultimate function in the active character module.

2 Likes

I don’t have a permanent kit for all characters so I have a seperate module for the classes.

In that case, I am thinking of adding a component called Abilities and the main module is will be handling the inputs.

Screen Shot 2023-02-05 at 9.18.11 PM

Also should I be using UserInputService.InputBegan? Thanks for the help :smile:

That’s alright, it can work even without a set kit for each character.

Regarding your inputs: I’m thinking more along the line of a generic controller, meaning you only have one that handles all input from the client and delivers it to class modules. I have an example that I personally use with my own class-based RPG experience.

This is how I have mine set up:

image

CharacterMaps would be equivalent to your Classes folder. init.lua is the input controller. It might be a bit hard to see but init.lua and CharacterMaps are both children of one folder but init.lua is not a child of CharacterMaps, they’re on the same level.

The input controller is where we set up our UserInputService connections. As we have controller and mobile support, it allows us to make actions equivalent (for example: pressing left mouse, left trigger and tapping a specific button would all do the same thing). It also allows us to set an active “action map” which determines which character’s controls we want to call actions from.

In the characters maps, that’s where we set up the actions that should happen when actions are called. That’d be equivalent to you making an ultimate function in your class module.

Key is pressed → Input controller tells a character map what input was sent and what the state of it is (began or ended) → ActionMap receives this directive, finds what action is linked to that input if there is one, then calls that function

2 Likes

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