[HELP] Client and Server run for multiple players

Hi,

I am working on a combat game and I tried using modules to handle server and client but I am having problems with the server and client running for all the players… for example, if player2 clicks to attack, it does the animation for player2 only but everyones hitbox will be on…

Here’s how the modules look like:
Screen Shot 2024-02-19 at 5.34.42 AM

Client:

local Client = {}
Client.__index = Client

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService = game:GetService("SoundService")
local Debris = game:GetService("Debris")
local Players = game:GetService("Players")

local Util = ReplicatedStorage.Source.Util
local Modules = ReplicatedStorage.Source.Modules

local ContextActionService = require(Modules.ContextActionUtility)
local BridgeNet2 = require(Modules.BridgeNet2)
local bind = require(Util.bind)

local ComboBridge = BridgeNet2.ReferenceBridge("Combo")
local HitBridge = BridgeNet2.ReferenceBridge("Hit")

local PlaceHolderDebounce = false

function Client.new(Character: Model, Animations)
	local self = setmetatable({},Client)

	self.Character = Character
	self.Humanoid = Character.Humanoid:: Humanoid
	self.Root = Character.HumanoidRootPart

	self.LoadedAnimations = {}
	self.Settings = require(script.Parent.Settings)
	self.GlobalSettings = require(script.Parent.Parent.GLOBAL_SETTINGS)
	self.Sounds = SoundService[script.Parent.Name]
	self.Animations = Animations
	-- constants
	self._combo = 1
	self._lastClicked = tick()
	
	HitBridge:Connect(function(EnemyHumanoid)
		print("Hit", EnemyHumanoid.Name)
	end)
	
	self:Equip()
	
	return self
end

function Client:Equip()
	-- Load the animations
	for _, anim in pairs(self.Animations:GetChildren()) do 
		self.LoadedAnimations[anim.Name] =  self.Humanoid.Animator:LoadAnimation(anim)
		self.LoadedAnimations[anim.Name].Priority = Enum.AnimationPriority.Action3 -- second highest...
	end
	
	-- Make sure Idle is looped
	self.LoadedAnimations.Idle.Looped = true
	
	-- Play the animations on equip
	self.LoadedAnimations.Equip:Play()
	self.LoadedAnimations.Idle:Play()
	
	-- Bind action for mobile and pc
	local function Attack(Action, InputState, InputType)
		self:M1(Action,InputState,InputType)
	end
	
	bind(true,
		self.GlobalSettings.M1_ACTION_NAME, 
		Attack,
		self.GlobalSettings.M1_BUTTON_IMAGE,
		self.GlobalSettings.M1_SLOT, 
		self.GlobalSettings.M1_ENUM
	);
end

function Client:M1(Action, InputState, InputType)
	if InputState ~= Enum.UserInputState.Begin then
		return
	end
	
	if self.Humanoid.Health <= 0 then
		return
	end

	if not PlaceHolderDebounce then
		PlaceHolderDebounce = true

		if self._lastAnimation and self._lastAnimation.IsPlaying then
			-- stops the last animation
			self._lastAnimation:Stop()
		end
		
		local comboString = "Combo"..tostring(self._combo)

		if tick() - self._lastClicked >= self.GlobalSettings["COMBO_RESET"] then
			-- resets the combo under a certain time
			self._combo = 1
		end
		
		coroutine.wrap(function(...)  -- enables the hitbox without delaying the rest of the script
			task.wait(self.Settings["START_HTIBOX_TIME"])
			ComboBridge:Fire(comboString) -- hitbox is done on server
		end)()
		
		self._lastAnimation = self.LoadedAnimations[comboString]
		self._lastClicked = tick() 

		self.LoadedAnimations[comboString]:Play() -- play the next combo animation
		
		self.Humanoid.WalkSpeed = self.GlobalSettings.M1_WALKSPEED -- set the walkspeed to hitting walkspeed
		
		task.wait(self.Settings["M1_COOLDOWN"]) -- cooldown
		
		if self._combo == 3 then -- last animation = stop moving
			self.Humanoid.WalkSpeed = 0
			task.wait(self.GlobalSettings.LAST_COMBO_COOLDOWN)
		end
		
		self.Humanoid.WalkSpeed = 16 -- change to normal speed
		
		if self._combo < self.Settings.COMBOS then -- adds 1 to combo so it actually changes anims
			self._combo += 1
		else
			self._combo = 1
		end
	
		PlaceHolderDebounce = false
	end
end

return Client

Server

--[[

TODO: 
- Handle SFX on server-side
- Handle Hitbox on server-side (better connection)
- Make the sword look cooler.
]]

local SoundService = game:GetService("SoundService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Debris = game:GetService("Debris")

local Util = ReplicatedStorage.Source.Util
local Modules = ReplicatedStorage.Source.Modules

local BridgeNet2 = require(Modules.BridgeNet2)
local RaycastHitboxV4 = require(Modules.RaycastHitboxV4)
local playSound = require(Util.playSound)

local ComboBridge = BridgeNet2.ReferenceBridge("Combo")
local HitBridge = BridgeNet2.ReferenceBridge("Hit")

local Server = {}
Server.__index = Server

function Server.new(Character: Model, Tool: Tool)
	local self = setmetatable({},Server)
	
	self.Character = Character
	self.Humanoid = Character:WaitForChild("Humanoid"):: Humanoid
	self.Root = Character:WaitForChild("HumanoidRootPart")
	self.Tool = Tool:Clone()
	
	self.Player = Players:GetPlayerFromCharacter(Character)
	self.Settings = require(script.Parent.Settings)
	self.Sounds = SoundService[script.Parent.Name]
	self.Particles = ReplicatedStorage.Instances.Characters.Reaper.Particles
	
	self.Speed = 10
	self.Force = 80000
	
	self.Tool.Parent = self.Player.Backpack
	self.Humanoid:EquipTool(self.Tool)
	
	playSound(self.Sounds["Equip"],self.Root)
	
	self.Hitbox = RaycastHitboxV4.new(self.Tool["Handle"])
	self.Hitbox.Visualizer = true
	
	ComboBridge:Connect(function(Player,Combo: string)
		print("Hitstart from"..Player.Name)
		local HitboxTime = self.Settings[string.upper(Combo)]
		self.Hitbox:HitStart(HitboxTime)
		playSound(self.Sounds[Combo],self.Root)
	end)
	
	self.Hitbox.OnHit:Connect(function(hit, humanoid)
		self:Hit(hit,humanoid)
	end)

	return self
end

function Server:M1(Combo: string)
	
end

function Server:Hit(hit,humanoid)
	local enemyRoot = hit.Parent:FindFirstChild("HumanoidRootPart")
	
	if humanoid == self.Humanoid then
		return
	end
	
	if not enemyRoot then
		return
	end

	local TotalForce = self.Force

	local Knockback1 = Instance.new("LinearVelocity")
	Knockback1.VectorVelocity = self.Root.CFrame.LookVector * self.Speed
	Knockback1.MaxForce = TotalForce
	Knockback1.Attachment0 = self.Root.RootAttachment

	local Knockback2 = Instance.new("LinearVelocity")
	Knockback2.VectorVelocity = self.Root.CFrame.LookVector * self.Speed
	Knockback2.MaxForce = TotalForce
	Knockback2.Attachment0 = enemyRoot:FindFirstChild("RootAttachment")

	Knockback1.Parent = self.Root
	Knockback2.Parent = enemyRoot
	
	local HitParticle = self.Particles["Hit"].Attachment:Clone()
	HitParticle.Parent = enemyRoot
	HitParticle:FindFirstChild("ParticleEmitter"):Emit(1)

	Debris:AddItem(Knockback1, 0.1)
	Debris:AddItem(Knockback2, 0.1)
	Debris:AddItem(HitParticle, 0.3)

	playSound(self.Sounds["Hit"], hit, math.random(10,20)/10)
	humanoid:TakeDamage(self.Settings.M1_DAMAGE)
end

return Server

How it is being tested:

Goes into server first:

local ChangeClass = BridgeNet2.ReferenceBridge("ChangeClass")
local Directories = {
	ServerScriptService.Source.Services
}

Knit.Start():andThen(function()
	print("Server: Knit")
end)

function onPlayerAdded(player: Player)
	player.CharacterAdded:Connect(function(Character: Model)
		ChangeClass:Fire(player, "Reaper")
		local Module = require(ReplicatedStorage.Source.Classes.Characters.Reaper).new(Character)
		Module:Init()
	end)
end

From a remote it goes into client:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Modules = ReplicatedStorage.Source.Modules

local BridgeNet2 = require(Modules.BridgeNet2)

local ChangeClass = BridgeNet2.ReferenceBridge("ChangeClass")

ChangeClass:Connect(function(class)
	local Module = require(ReplicatedStorage.Source.Classes.Characters.Reaper).new(game.Players.LocalPlayer.Character)
	Module:Init()
end)

make sure to utilize the “player” variable so it will only work for THAT player

2 Likes

I debugged a little and I found out that when I do:

Player.Name (Player from remote from client to server), I get Player1 printed 2x.
But when I do self.Player inside the connect function, it does Player1 and Player2 printed.

1 Like

You can clearly see that the server is trying to handle 2 players inside of the script, and if there was mmore than 2 players it would be worse. This is what im trying to solve

I had this similar problem back when I was making a reward system. I forgot how to fix it but my hitbox script (using RaycastHitboxV4) on a ServerScript worked perfectly fine.

1 Like

Client detects when to start hitbox by the way. Then fires an event to enable the hitbox from server

Instead of solving this, as I can see no one is really able to help me, what different can I do instead of using this method. Like what other choices do I have

1 Like

Alrighty, lets start here. So whenever a play joins you setup their “reaper” class. Lets say we have player1 and player2 in the server both using reaper class. Player1 activates their hitbox from the client module:

The ComboBridge remote gets fired to back to the server. However in your server module every player is connected to the same ComboBridge event:

Inside your connection instead of using the “Player” variables you are instead activating the self.Hitbox for every player connected to said remote. That’s why when a player activates their hitbox everyone else’s is activated too.

The solution? You could quite literally do:

ComboBridge:Connect(function(Player,Combo: string)
        if Player ~= self.Player then return; end; -- The player that fired the remote isn't us
		print("Hitstart from"..Player.Name)
		local HitboxTime = self.Settings[string.upper(Combo)]
		self.Hitbox:HitStart(HitboxTime)
		playSound(self.Sounds[Combo],self.Root)
	end)

I also recommend that instead of having every player listen for the same remote on the server → Just have 1 script that listens for an event → references the player that fired the remotes info → activate their hitbox.

1 Like

Thank you so much and I really appreciate giving me advice on how you think this should be handled while also giving me the solution. I will try this when I get on

Does the one script handle all the hitboxes? Or is it seperate?

It really depends how you want things to be setup. You don’t need to listen to the same event like 20 times (e.g if you had 20 players in game) and then check if they were the one that fired said event.

So you could have the ComboBridge connection on the server as a standalone script/module that handles every class hitbox activation (of course you can have different handlers for special weapons/skills that don’t use the traditional hitbox method).

So you would remove it completely from the “Server.new()” function and have it as a standalone module/script. You would then need to store the players info so it can access it for the hitbox activation.

e.g

-- It's own module. E.g name "Combo Handler"
local BridgeNet2 = require(Modules.BridgeNet2);
local ComboBridge = BridgeNet2.ReferenceBridge("Combo");

ComboBridge:Connect(function(Player,Combo: string)
	print(Player.Name.." activated hitbox");
	local GetPlayerInfo = FuncToGetClass(); -- basically whatever method to get the players info/settings. The way you store/cache this data is up to u
	GetPlayerInfo.StartHitbox(); -- e.g of having a function for each class to activate the players hitbox 
end);

Hopefully this makes sense. Again the way you structure everything is up to you. And if you just want the easiest method just refer to my previous post and add the if statement check.

1 Like

One last question, do you think the way I am structuring this is inefficient?

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