How to create an abilities system using module scripts?

Hi! I’m currently experimenting with module scripts and OOP, but I don’t quite understand certain aspects.
I’m interested in creating a fighting game with abilities and during my researches I found this thread:

I read the code in the file attached in the answer I linked, but I didn’t quite get how it works.
For example, in ServerScriptService there a script with just one line:

require(script.Parent:WaitForChild("Magic"))

But what’s its purpose exactly? If someone could break down the logic behind this method and explain it in simple\more practical terms I’d really appreciate it.
Also, let’s say I wanted to let player select one magic type when they spawn and once selected, a GUI with the buttons binded to the abitilies appear, which are also keybinded to a keyboard key. How should I apply that method including UserInputService?

Thanks in advance to any person willing to help! And no, I’m not asking for someone to script it for me, but rather explain how I should approach it logic-wise. Pseudo-code also works!

EDIT: if the original poster, @DrKittyWaffles, could help it would be amazing :slight_smile:

5 Likes

It’s just gonna be extremely easy for you to setup new abilities each time. They are used mostly because you don’t have to add a lot of lines of code in a single script (or use multiple scripts which is worse than using only one). I use module scripts for skills too, and they are extremely useful. You can create a folder, maybe in ServerStorage, with all the abilities’ modules in there, then clone them and require them whenever a player wants to use a skill. The concept is pretty simple. It’s all about performance and organization.

1 Like

Hi, thank you for answering! Unfortunately I’m not sure if it helped :sweat_smile: I’m familiar with the concept of OOP and the purpose of module scripts, however I was interested in learning how the code in the thread i linked works specifically and maybe some practical examples to implement UIS in it.
In the file attached, this is the code in the main module script for example:

local magic = {}
for i, module in pairs(script:GetChildren()) do
	local magicType = module.Name
	local abilities = require(module)
	
	magic[magicType] = abilities
end

return magic

It returns a table with the abilities, but to what\where?

I looked at the file linked and I couldn’t find any line that resembled what you wrote. Could you either screenshot the hierarchy of this script or else upload your file?

Using the methods already described you could make the GUI you want appear using:

ScreenGUI.Enabled = true

When they first join and when they select an ability:

for _, button in pairs(ScreenGUI:GetChildren())
  if button:IsA("ImageButton") then
   button.MouseButton1Down:Connect(function()
    InventoryUI.Templates:FindFirstChild(Folder.Name).Parent = InventoryUI.Inventory

This will run through the possibilities that they can choose and then when they choose make the UI for that magic appear. I’ve used a fairly arbitrary hierarchy for that so it’s not fundamental you do it in that way. The folder in this case would include the different abilities for said magic. I would also maybe use a remote function here to send validation to the server where you could change values in a module or string value to store info of which magic is bound to which button (avoid exploitation and the such).

You could then use ContextActionService | Roblox Creator Documentation to bind the magic to the key by just coding the function for each key to something like (I would probably use a remote function following the CAS event being fired):

local MagicValid = require(Player.MagicValid)
local Magic = require(ServerScriptService.Magic)
function()
  Magic[MagicValid.MagicChosen][MagicBindedToKey]
end

edit:
The module you just linked works by looping through the modules childed to the magic module and then requiring a module childed to the module and making the childed module’s function a member of magic. So basically it does something like this (I’ve just read this explanation and it’s giving even me a migraine so if you have questions that’s fair enough):

local abilityName = "Water"
local abilityCallback = require(Magic.Water)
	
magicType[abilityName] = abilityCallback

-- is the same as

Magic.Water.TyphoonWall = function() end

This enables the person who wrote the module’s statement to be true:

4 Likes

I won’t go over how to create abilities system, but rather shortly explain how modules work.

In your example it returns this magic table to every script that uses require on it.

For instance:

local MagicMod = require(script.MagicModule); -- MagicMod is now your 'magic' table

-- You can print out the contents of it
for mType, ability in pairs(MagicMod) do
    print(mType, ability);
end

You can return anything. Functions, tables, numbers etc…

The main purpose of modules is OOP and functions. You do not want to copy and paste the same functions over and over to all scripts that will use them.
Always follow the golden rule of programming - DRY (Don’t Repeat Yourself)
Instead, you can make a single module that returns the function, require it in other scripts and then call the function.
This can save a lot of time, because if you wanted to change something in your main function you could do it once and it would automatically update for other scripts too.

5 Likes

Omg I’m so dumb, it was just some left-over code I forgot to remove after messing around. My bad, sorry for the confusion!

So basically, using :PlayerAdded or something like that I make the GUI with the ability selection appear

then after selecting it, the script will copy the GUI buttons binded to each skill inside the hotbar GUI, right?

I’m not sure about this part, what do you mean with CAS event? And what’s local MagicValid = require(Player.MagicValid) ?

Okay, I’m starting to understand. Basically, the module requires its own children multiple time until it reaches the individual skills’ modules, which creates a sort of table I assume. Does the script inside a module run automatically once require() is used in a script?
Following this logic, in the game memory we should have a huge table with all the functions like this:

local Magic = {
	Fire = {
		"Breath Fire" = function() end,
		"Fire Wave" = function() end
},
	Dirt= {
		"Rock Armor" = function() end,
		"Tree Whip" = function() end,
		"Boulder Throw" = function() end

},
	Water = {
		"Typhoon Wall" = function() end,
		"Waterfall Blast" = function() end
}
}

Which then can be referenced using this, as you said:

local Magic = require(MagicModuleHere)

Magic["Water"]["Typhoon Wall"](Player)

Still not 100% sure about the keys part, but thank you so much, you have been incredibly helpful so far:)

2 Likes

Yes, modulescript runs its code every time a script requires it. Hence a lot of developers have their game consisted of only modules because “it looks cool”, but I do not do that. I use modules when i have to.

2 Likes

Thank you so much, that explains a lot! LOL I always thought you could only use functions to call later in a local or server script later.

1 Like

Basically, a module script is nothing but a usual script with the only difference that:

  1. It has to be required in order to run.
  2. It must return at least one value back.

And that’s really it.
The only thing that you should keep in mind though, is that you can require another module in a module script, however if you require both of the same modules in 2 different module scripts you will get a infinite recursion error (2 modules infinitely requiring each other)
I think there is a work-around this, but you’d have to search for yourself cause I can’t remember the post.

1 Like

That’s pretty much correct. It’s definitely slightly convoluted but that’s pretty much how it works.

You could do that, yes.

Sort of. It would be more like there is a folder inside a separate screenGUI representing the hotbar which contains templates for each magic type/move, which are then cloned into a frame (the hotbar GUI itself).

The MagicValid module would be a module inside the player which the server can use to check which hotkey is bound to which move and what magic is currently equipped. This enables you to have server-sided checks and store data dynamically based on which magic is equipped.

The CAS event is just my abbreviation for a ContextActionService event. In the API reference article I linked it gives adequate detail on how you can bind keys/actions to functions, which in this case would be via the original magic module you linked.

1 Like

Okay, thank you once again! I’m still not 100% sure on how I should handle the CAS part nor the module for server checks, but I’ll try to put something together and if it doesn’t work I’ll let you know. Currently reading the wiki article more in-depth since I don’t use CAS often and always used UIS instead.

Also, I wrote this script (script inside ServerScriptService) which copies the buttons inside the hotbar everytime the character respawns:

local Players = game:GetService("Players")
local SS = game:GetService("ServerStorage")
local SkillsStorage = SS:WaitForChild("MagicHotBar")
local CAS = game:GetService("ContextActionService")

Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		
		local SelectionGui = player:WaitForChild("PlayerGui"):WaitForChild("Selection")
		SelectionGui.Enabled = true
		

		for _, button in pairs(SelectionGui:FindFirstChild("Selection"):GetChildren()) do
			
			if button:IsA("ImageButton") then
				
				button.MouseButton1Down:Connect(function()
					print(button.Name.." selected!")
					SelectionGui.Enabled = false
					for _,skillkey in pairs (SkillsStorage:FindFirstChild(button.Name):GetChildren()) do
						
						local copy = skillkey:Clone()
						copy.Parent = player.PlayerGui:WaitForChild("HotBar").HotBar
					end
					
				end)
			end
		end
	end)
end)

It works fine when the player first join, but for some reason SelectionGui.Enabled isn’t set to true when the player respawns after resetting\dying. Not sure what the cause could be, the code seems correct.

Hierarchy:
image

I really can’t explain that. I did a load of tests to try and find the issue and couldn’t locate the specific problem. It worked when I added a wait after the character added event but wouldn’t work otherwise. It also worked fine if ran from a local script (without the character added event).

1 Like

How odd, I tried adding a wait() too there yesterday, but it didn’t work. I tried again and it seems to work. Sometimes I feel like Studio just likes to mess with me :skull:

Anyway, I tried working on the system following your directions, and I think I’m almost getting there but I might as well be completely wrong since it doesn’t work yet.
AbilitiesSystemModules.rbxl (31.1 KB)
I added a LocalScript in PlayerScripts, with a module containing the keys, as you suggested. When the player selects their ability type, a remote is fired and the individual skills’ functions are binded using CAS. However, it gives me this error:
image
Apparently you can’t pass functions via a remote? I probably misunderstood your directions, so I’m literally stuck. I tried moving the Magic module in ReplicatedStorage (making the adequate changes ofc), but it errors saying “attept to index nil with nil”, which is even worse; besides, I’d rather keep the Magic module in the Server-side to prevent exploiters from decompiling and such. + CAS can only be used in LocalScripts.
I’m thinking using metamethods\metatables could be a solution, but I’m not familiar with those at all. Any idea on what I’m doing wrong?

Am I going crazy or I wrote it exactly how you said? o.o
EDIT: you said it wouldn’t be called if i put CharacterAdded inside PlayerAdded, how can I reference the player otherwise?

I think I wrote that wrong, my bad. Is this a localscript? If not, you will have to make an event for a localscript to communicate with the serverscript. And also, I meant you to put player.CharacterAdded somwhere else, not in the PlayerAdded function.

Edit: I think you can send over the Player from a local script with the remote event.

1 Like

Oh, no it’s a server script indeed.
I just realized that many people consider a bad practice handling GUI via server, right? So I think I’ll use a LocalScript like you said.

Making a remote event will do the job. Also, I think this is how you pass additional data in remote events(sorry, havent been scripting in a while)

Edit: I don’t think you can handle GUI’s very efficiently, if not at all in server side anyway.

1 Like

I think it would be better if the keys are server side accessible for game protection purposes, but also so that the server can more easily access said data.

I would suggest that you bind the key to a function that sends a remote to the server rather than the reverse. This would allow you to perform the function encapsulated in the magic module on the server (which you’d want to do if it’s multiplayer).

You can’t pass functions as parameters and I don’t think it’s necessary either. You could probably make it so that a remote from the CAS event passes the key info which (if you have the key info on the server) can be validated and the magic type confirmed.

edit: Handling GUI on the server isn’t AWFUL, it works, it just probably isn’t the most performant. It’s an easy fix either way.

I agree. Think of it as like this: The chef is in the server side, or backend, and the game is like the food. The internet is the waiter.

Also be sure to close this thread or checkmark someone’s reply as a solution, you don’t want people entering the thread when the problem is already solved.

Edit 2: Also roblox developer hub can help you too. Be sure to check what you’re working on in devhub so you can get a clear understanding of what it does and how.

So should I place the Keys module in ServerScriptService? I’m probably missing the point, this is the Keys module, is it really that important to protect it by putting it in the server?

local Keys = {
	Fire = {
		["Breath Fire"] = Enum.KeyCode.B,
		["Fire Wave"] = Enum.KeyCode.F
},
	Dirt= {
		["Rock Armor"] = Enum.KeyCode.B,
		["Tree Whip"] = Enum.KeyCode.F,
		["Boulder Throw"] = Enum.KeyCode.R

},
	Water = {
		["Typhoon Wall"] = Enum.KeyCode.B,
		["Waterfall Blast"] = Enum.KeyCode.F
}
}

return Keys

That makes sense, but how can I bind the function if CAS can only be used by the client? The key module is in the server and It’s impossible to use :BindAction there.

I literally feel so stupid right now lmao I’m sorry, I don’t understand, really. I pass the key info to the client, but I still have no access to the skills functions since they are server-sided?

1 Like