Ability system : Oop/Classes/Composition

I’m currently learning Oop/Classes/Composition/ however I’m uncertain I’ve coded it in an optimal way, or if it is even considered composition in the way I have coded it;

Essentially I am making a “Operator/Hero/Agent” system, similar of that to Valorant/R6S/Overwatch,
In a quick brief overview; I have created a base character class which creates each character from a data template then I adds the correct ability modules.

Are there better ways to do it? if so what do you suggest?

  • Note there is no server side logic yet it’s all just setup, I simply would like to know what should be changed, is it optimal or whatever I need to know before scripting any logic, all scripts are related to server side of things, I have not coded any client side stuff yet.

BASE CLASS MODULE

local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--//Varaibles
local Default = ReplicatedStorage.Resources.Default
--//Requires
local CharacterData = require(ServerStorage.Data.CharacterData)
local Abilities = require(ServerStorage.Classes.Abilities)

local BaseCharacter = {}
BaseCharacter.__index = BaseCharacter

-- New class
function BaseCharacter.new(player, characterType)
	local data = CharacterData[characterType]
	assert(data, "Character type not found:", characterType)
	
	local self = setmetatable({}, BaseCharacter)
	
	-- Character attributes
	self.Health = data.Health
	self.Walkspeed = data.Walkspeed
	self.Abilities = {}
	self.Cooldowns = {}
	
	-- Adding abilities
	for slot,abilityInfo in pairs(data.Abilities) do
		local module = Abilities[abilityInfo.Name]
		self.Abilities[slot] = module.new(abilityInfo.Config)
	end
	
	return self
end

-- Checking if on cooldown
function BaseCharacter:IsOnCooldown(slot, duruation)
	return self.Cooldowns[slot] and tick() < self.Cooldowns[slot]
end

-- Setting cooldown
function BaseCharacter:SetCooldown(slot, duration)
	duration = duration or 10
	self.Cooldowns[slot] = tick() + duration
	print("Set cooldown for:", duration, "seconds", slot)
end

-- Use ability/m1 (m1 is also coded like an ability unsure how else to do it
function BaseCharacter:UseAbility(slot)
	local ability = self.Abilities[slot]
	
	if ability and ability.Activate then
		if self:IsOnCooldown(slot) then print("on cooldown") return end
		self:SetCooldown(slot, ability.Cooldown)
		ability:Activate()
	else
		warn("Ability configured incorrectly; or ability not found")
	end
end

return BaseCharacter

DATA TEMPLATE EXAMPLE

return {
	["Piracy"] = {
		Health = 75,
		Walkspeed = 20,
		Abilities = {
			Primary = {Name = "Shotgun", Config = {Cooldown = 10, Damage = 20}};
			Secondary = {Name = "Shoot", Config = {Cooldown = 7, Damage = 15}};
			Alternative = {Name = "Kegbarrel", Config = {Cooldown = 15, Damage = 25}};
			Dash = {Name = "Dash", Config = {Cooldown = 5, Distance = 20}};
		}
	}
}

ABILITY EXAMPLE MODULE (There’s multiple)

local Dash = {}
Dash.__index = Dash

function Dash.new(config)
	config = config or {}
	local self = setmetatable({}, Dash)
	
	self.Cooldown = config.Cooldown or 3
	self.Distance = config.Distance or 10
	
	return self
end

function Dash:Activate()
	print("Dash activated, Cooldown:", self.Cooldown, "Distance:", self.Distance)
end

return Dash

Bumpity bump bump.

3 Likes

This isnt bad, but I would recomend you not use configs but rather just plain parameters and branchless programming. This makes for easier to read code.
So something like this:

function Char.new(name, age, height)
	local self = {}
	self.Name = name or "Nameless"
	self.Age = age or 0
	self.Height = height or 0
	
	return setmetatable(self, Char)
end

Also, for your config, Its really easy to get lost when the game becomes bigger. Its fine as it is now, but if that data doesnt change, having constants in the module that controls the operator/agent/whatever would be good. Because if someone were to come onto your project and didn’t get an introduction, it may take a minute for them to find that every operators config is in one file. Again, not a horrible practice, but for readability and general understanding, having those be constants and defaults would be good. If I were to apply what I just said to the snippet earlier i would do this:

local DEFAULT_NAME = "Nameless"
local DEFAULT_AGE = 0
local DEFAULT_HEIGHT = 0

function Char.new(name, age, height)
	local self = {}
	self.Name = name or DEFAULT_NAME
	self.Age = age or DEFAULT_AGE 
	self.Height = height or DEFAULT_HEIGHT 
	
	return setmetatable(self, Char)
end

This way, if I were to have never seen a line of code ever, I would know that if there isnt a name, go with the default, etc..

2 Likes

Final few questions

1: Also you showed your example using char.new, I dont use char.new everything is created inside base character and I manually add abilities though a loop, is this okay?

2: The reason I used a config table was because as you can see this is how im setting it, however each config table can vary slightly, perhaps its cooldown and distance or for another ability perhaps its cooldown and damage, how would I pass those arguments in as is?

3: One of my concerns was how M1s work, I had to treat it like an ability as I was unsure how else to do it, is this alright would you say or should I do it any other way?

4: This is for the server side handling validation logic for each ability, how would you suggest I make the client side so it all works together correctly

2 Likes

1: I use char.new as an example, not anything related to your code.

2: You can use table.unpack to return a tuple which will automatically be passed into the function

3: I would seperate M1’s completely as its not an ability, but rather a different mechanic as a whole. The name implies its mouse button 1’s, so an object wouldnt work too well with this. You could have it so that the object handles when the input is active or not. But, I would do something like marvel rivals where you have your main left and right click stuff, then you also have your abilities that you can do.

4: Make your game to appeal to players, not hackers. So that means handling visuals and movement on the client side. But for gameplay, handle that on the server(e.g.: round starting, damaging, etc.). Essentially have it so the client requests things for the server to do. The server always holds control.

1 Like

As of OOP, you are using BaseCharacter.new(), which isn’t the standard way for OOP. But it’s primarily preference. The code below prevents a BaseCharacter object to have the constructor.

local Static = {}

local BaseCharacter = {}
BaseCharacter.__index = BaseCharacter

function Static.new()
    local thing = setmetatable({}, BaseCharacter)
    -- hidden code
end
3 Likes

When you say seperate M1s, would that be making an M1 class? Then having ranged M1, melee M1, aoe M1? Or some other way?

2 Likes

I said in the post your replying to that M1’s are a different mechanic overall. They aren’t a onetime thing, they the action of pressing the mouse button 1, or left click. So I would use a separate system for that kind of binded action. With your system it could work, but you would need a controller to properly manage input for the M1’s.

2 Likes

Ah okay, just trying to fogire out how it would work as I am new to oop/combat systems, especially considering the different type of M1s possible

2 Likes

Your approach is what I assume to be more oriented around learning Oop and higher level game development. The only caveat with that is that Oop and combat games aren’t the bestest of friends, considering you’ll never need two objects representing a dash. It would be better to look into more dynamic module frameworks that can be evolved, one script at a time.

Here is a video of a developer with a game that gets around 3k average ccu and represents what I mean when I say dynamic module framework: https://www.youtube.com/watch?v=LFV_PktjVuQ.

This video is very insightful to how bigger games are produced. While the game itself isnt combat related. The concepts that are shown(not discussed) can be applied to a combat game.

Edit: For combat games, especially with hero/operator selection, Oop can still be a good option. However making an object for every ability may not be the method. The video I linked earlier is a decent insight to how games can be structured. Obviously you can do whatever you want, like literally anything. But, that video demonstrates a decent approach to the larger problem of scalability. They focus on individual systems and grouping rather than oop for everything.

Edit 2: PLEASE PLEASE PLEASE look into the singleton pattern. It is what makes up a good dynamic framework, allows for better communication between scripts, and is easier to work with in bigger projects.

3 Likes

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