How to manage/design abilities for client-server communication?

I created a simple base class for creating a character that can have different abilities and I think the structure is pretty good there are 3 functions that must/should be implemented by derived classes and this is where the bulk of the work goes and then the base class provides some utility functions for some important things like playing sound and spawning particles.

function BaseCharacter.new()
	local self = setmetatable({},BaseCharacter);
   
	return self;
end

function BaseCharacter:PlaySound()	
-- Use some AudioManager to play sound
end

function BaseCharacter:SpawnParticles()	
-- Use some ParticleManager to spawn particles
end

function BaseCharacter:AbilityOne() 	
	warn("BaseCharacter:AbilityOne() must be implemented!")
end

-- AbilityTwo(), AbilityThree(), ...

The problem is figuring out how to fit this into a client-server architecture I’d like to have good responsiveness (i.e when player makes an input something immediately happens) so the client would have to have some info about how the run the abilities, but then how would that get communicated to the server to do important things like replicating, damage, etc. Would the client and server need their own copies of the same class? Or is there a more efficient solution?

2 Likes

Usually how I handle this is that I just have a shared dictionary (in the form of a module) that both the client and server can reference for their own needs.

Say you have an ability that should play a sound when you do something, and deals some amount of damage when it hits a humanoid, you COULD send over damage dealt and the sound to play over a remote, but that could easily be tampered with and break your game.
But with a shared dictionary, you could just send over which ability you are going to use, and have the server lookup everything else.
Any tampering done inside of the module will not be replicated to the server, and is pretty much impervious to such.
This also solves the responsiveness issue, since you can just use whatever values you need in the table, and just update whatever you need on the client.

Heres an example:

The shared dictonary:

local AbilityDictionary = {
  Ability1 = {
    AnimationId = (…)
    SoundId = (…)
    Damage = 35
  }
}

Client:

local AbilityDict = require(…)

function Ability1()
     PlaySound(AbilityDict.Ability1.SoundId) -- youll have to make sure to send a replication signal to other clients seperately, since if you play the sound on the server it will sound like it plays twice for the client who uses the ability
     PlayAnimation(AbilityDict.Ability1.AnimationId)
     FireAbilityRemote(AbilityName)
end

Server:

local AbilityDict = require(…)
function OnAbility(Player, AbilityName)
     local AbilityInUse = AbilityDict[AbilityName]
     -- the rest of your logic
end

Obviously this doesn’t omit you from using other sanity checks (i.e. remote spam), but it does make it pretty much impossible for exploiters to just change values and send over bad data

If I may ask, what’s the reasoning for having an ability dictionary instead of having the corresponding values (damage, soundId, etc) in the script that uses them as variables?

Mostly so that you don’t have to copy paste values for sanity checks on the server, but it also makes organization and scalability much easier.

Basically, why not just reuse the same values that you are going to have to update anyways

Thanks this looks like a great solution!

I don’t have too much experience with this but I’ve always split up an ability up into two modules. One for the server, and one for the client. The server has a ā€œuseā€ function and the client has two ā€œuseā€ functions (one for the user, and one for when used by another player/character).
Splitting up the modules gives me good scalability (sorta easy to remove and add abilities) and it also allows me to do ā€˜client-side only’ abilities.
For example, if there is an ESP ability then other clients don’t need to replicate it (only the user will have ESP after all), but I can also do ā€˜server-side’ abilities (ones that I want sanity checks for i.e creating a projectile that will do damage).

Also, not advising this, but (using my structure) you could have abilities stored in one ModuleScript, and whenever you require the module it’ll return the version of the ability based on what context the script is running. I don’t do this but you technically could.
For example:

local RunService = game:GetService("RunService")

local AbilityServer =  {}
local AbilityClient = {}

function AbilityServer.Use()
   blahblah
end

function AbilityClient.Use()
   blahblah
end

if RunService:IsClient() then
   return AbilityClient
else
   return AbilityServer
end
2 Likes

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