I genuinely cannot find the source of the memory

I tried searching the dev forum for how to fix but its all about connections and task.spawn, things im not even using, or using them in ways that could cause memory leaks. I am so stumped I even tried chat gtp and it gave me the same result. The server has like 3000 memory usage when I test the game in studio.

Main module

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Modules = ReplicatedStorage:WaitForChild("Modules")

local PlayerStates = require(Modules:WaitForChild("FighterStates"))
local Objects = workspace:WaitForChild("Game"):WaitForChild("Objects")

local FighterModule = {}
FighterModule.StageLength = 50

function FighterModule.New(Model:Model,Config:{})
	local RootPart = Model.PrimaryPart
	local LifeTick = 0
	Model:SetAttribute("state",PlayerStates.idle)
	Model:SetAttribute("state_timer",0)
	Model:SetAttribute("max_health",Config.max_health)
	Model:SetAttribute("health",Config.max_health)
	Model:SetAttribute("dir",1)
	Model:SetAttribute("free",true)
	Model:SetAttribute("custom_gravity",1)
	Model:SetAttribute("custom_friction",1)
	Model:SetAttribute("custom_speed",1)
	Model:SetAttribute("custom_jumppower",1)
	Model:SetAttribute("attack_type",2)
	Model:SetAttribute("attack","")
	Model:SetAttribute("cancel_type",0)
	Model:SetAttribute("dir_point",0)
	Model:SetAttribute("djumps",1)
	Model:SetAttribute("djump_stored_hspeed",0)
	Model:SetAttribute("air_dash_used",false)
	Model:SetAttribute("air_dash_back_used",false)
	
	local Inputs = {
		Move = Vector2.zero,
		A = false,
		B = false,
		C = false,
		D = false,
		Evasive = false,
	}
	
	local TechBuffer = 0
	local InputBuffer = {0,Vector2.zero,0}
	
	local Server = Config.InitializeServer(Model)
	local Attacks = Server.Attacks
	
	local function GetState()
		return Model:GetAttribute("state"),Model:GetAttribute("state_timer")
	end
	
	local function GetFacingDir()
		return Model:GetAttribute("dir")
	end
	
	local function SetState(state:number)
		Model:SetAttribute("state",state)
		Model:SetAttribute("state_timer",0)
		if state ~= PlayerStates.attack and state ~= PlayerStates.attack_air and state ~= PlayerStates.attack_ground then
			Model:SetAttribute("custom_gravity",1)
			Model:SetAttribute("custom_friction",1)
			Model:SetAttribute("attack_type",2)
		end
		TechBuffer = 0
	end
	
	local oldInputs = Inputs
	local function BufferHandler()
		if Inputs.A == true and oldInputs.A ~= true then
			InputBuffer = {LifeTick,Inputs.Move,1}
		elseif Inputs.B == true and oldInputs.B ~= true then
			InputBuffer = {LifeTick,Inputs.Move,2}
		elseif Inputs.C == true and oldInputs.C ~= true then
			InputBuffer = {LifeTick,Inputs.Move,3}
		elseif Inputs.D == true and oldInputs.D ~= true then
			InputBuffer = {LifeTick,Inputs.Move,4}
		elseif Inputs.Evasive == true and oldInputs.Evasive ~= true then
			InputBuffer = {LifeTick,Inputs.Move,5}
		end
	end
	
	local function ClearBuffer()
		InputBuffer = {0,Inputs.Move,0}
	end
	
	local function DoAttack(attack)
		Model:SetAttribute("attack_type",(typeof(attack.type) == "number" and attack.type) or 2)
		Model:SetAttribute("attack",attack.Name)
		Model:SetAttribute("cancel_type",0)
		SetState(PlayerStates.attack)
		attack.hit = false
		attack.hit_other = false
		attack.Use(LifeTick)
	end
	
	local function AirTech()
		SetState(PlayerStates.air_tech)
		RootPart.Velocity = Vector3.new(-Model:GetAttribute("dir"),1.25,0).Unit*Config.speeds.air_tech
	end
	
	local function Evasive()
		local state,state_timer = GetState()
		if state_timer >= 20 and TechBuffer <= 0 and state == PlayerStates.hurt_air then
			TechBuffer = 6
		elseif Model:GetAttribute("free") and ((state ~= PlayerStates.air_dash and state ~= PlayerStates.air_dash_back) or state_timer >= 15) then
			if Model:GetAttribute("air_dash_back_used") ~= true and ((Model:GetAttribute("dir") == 1 and Inputs.Move.x < 0) or (Model:GetAttribute("dir") == -1 and Inputs.Move.x > 0)) then
				Model:SetAttribute("air_dash_back_used",true)
				SetState(PlayerStates.air_dash_back)
				Model.PrimaryPart.Velocity = Vector3.new(Model:GetAttribute("dir")*-Config.speeds.air_dash_hspeed,Config.speeds.air_dash_vspeed,0)
			elseif Model:GetAttribute("air_dash_used") ~= true then
				Model:SetAttribute("air_dash_used",true)
				SetState(PlayerStates.air_dash)
				Model.PrimaryPart.Velocity = Vector3.new(Model:GetAttribute("dir")*Config.speeds.air_dash_hspeed,Config.speeds.air_dash_vspeed,0)
			end
		end
	end
	
	local function AirInputHandler(state:number,state_timer:number)
		-- action inputs
		if Inputs.Move.Y > 0.5 and oldInputs.Move.Y <= 0.5 and Model:GetAttribute("djumps") > 0 then
			Model:SetAttribute("djumps",Model:GetAttribute("djumps")-1)
			SetState(PlayerStates.djump_start)
			Model:SetAttribute("djump_stored_hspeed",RootPart.Velocity.x)
		elseif LifeTick - InputBuffer[1] < 6 then
			local button = InputBuffer[3]
			if Config.AttackHandler then
				Config.AttackHandler(button,LifeTick)
			else
				local attack
				if button == 1 then
					attack = Attacks.Air_A
				elseif button == 2 then
					attack = Attacks.Air_B
				elseif button == 3 then
					attack = Attacks.Air_C
				elseif button == 4 then
					attack = Attacks.Air_D
				elseif button == 5 then
					Evasive()
				end
				if attack and typeof(attack.Use) == "function" and attack.CanBeUsed() then
					DoAttack(attack)
				end
			end
			ClearBuffer()
		end
	end
	
	local function GroundedInputHandler(state:number,state_timer:number)
		-- action inputs
		if Inputs.Move.Y > 0.5 then -- no buffer required.
			SetState(PlayerStates.jumpsquat)
			if InputBuffer[3] ~= 0 then
				InputBuffer[1] += Config.base_frames.jumpsquat
			end
		elseif LifeTick - InputBuffer[1] < 6 then
			local button = InputBuffer[3]
			if Config.AttackHandler then
				Config.AttackHandler(button,LifeTick)
			else
				local attack
				if button == 1 then
					attack = Attacks.A
				elseif button == 2 then
					attack = Attacks.B
				elseif button == 3 then
					attack = Attacks.C
				elseif button == 4 then
					attack = Attacks.D
				elseif button == 5 then
					Evasive()
				end
				if attack and typeof(attack.Use) == "function" and attack.CanBeUsed() then
					DoAttack(attack)
				end
			end
			ClearBuffer()
		end
		
		-- non-action inputs
		
		if (state == PlayerStates.idle or state == PlayerStates.crouch_start or state == PlayerStates.walk or state == PlayerStates.walk_back) and Inputs.Move.Magnitude > 0.5 then
			local dir = GetFacingDir()
			local new = state
			if Inputs.Move.y < -0.5 and state ~= PlayerStates.crouch and new ~= PlayerStates.crouch_end then
				new = PlayerStates.crouch_start
			else--if state ~= PlayerStates.crouch and state ~= PlayerStates.crouch_start then
				if (Inputs.Move.x > 0 and dir == 1) or (Inputs.Move.x < 0 and dir == -1) then
					new = PlayerStates.walk
				else
					new = PlayerStates.walk_back
				end
			end
			if new ~= state then
				SetState(new)
			end
		elseif (state == PlayerStates.walk or state == PlayerStates.walk_back) and math.abs(Inputs.Move.x) <= 0.5 then
			SetState(PlayerStates.idle)
		elseif state == PlayerStates.crouch and Inputs.Move.y > -0.5 then
			SetState(PlayerStates.crouch_end)
		end
		
	end
	
	local function GroundedBehavior()
		local begin_state,begin_timer = GetState()
		if begin_state == PlayerStates.idle or begin_state == PlayerStates.crouch or begin_state == PlayerStates.crouch_start or begin_state == PlayerStates.crouch_end or begin_state == PlayerStates.crouch then
			if begin_state == PlayerStates.crouch_end and begin_timer >= Config.base_frames.crouch_end then
				SetState(PlayerStates.idle)
			elseif (begin_state == PlayerStates.crouch_start and begin_timer >= Config.base_frames.crouch_start) then
				SetState(PlayerStates.crouch)
			end
			RootPart.Velocity = Vector3.zero
		elseif begin_state == PlayerStates.attack or begin_state == PlayerStates.attack_air then
			SetState(PlayerStates.attack_ground)
		elseif begin_state == PlayerStates.attack_end then
			SetState(PlayerStates.idle)
		elseif begin_state == PlayerStates.walk then
			RootPart.Velocity = Vector3.new(math.sign(Model:GetAttribute("dir"))*Config.speeds.walk*Model:GetAttribute("custom_speed"),0,0)
		elseif begin_state == PlayerStates.walk_back then
			RootPart.Velocity = Vector3.new(-math.sign(Model:GetAttribute("dir"))*Config.speeds.walk_back*Model:GetAttribute("custom_speed"),0,0)
		elseif begin_state == PlayerStates.jumpsquat then
			RootPart.Velocity = Vector3.zero
			if begin_timer >= Config.base_frames.jumpsquat then
				SetState(PlayerStates.jump)
				local influence = (Inputs.Move.x > 0.5 and 1) or (Inputs.Move.x < -0.5 and -1) or 0
				RootPart.Velocity = Vector3.new(influence*Config.speeds.jump_influence,Config.speeds.jump*Model:GetAttribute("custom_jumppower"),0)
			end
		elseif begin_state == PlayerStates.hurt_ground then
			
		elseif begin_state == PlayerStates.hurt_air then
			if TechBuffer > 0 then
				AirTech()
			else
				SetState(PlayerStates.hurt_land)
			end
		elseif begin_state == PlayerStates.hurt_land then
			if begin_timer >= 40 or Inputs.Move.y > 0.5 then
				SetState(PlayerStates.getup)
			end
		elseif begin_state == PlayerStates.getup then
			RootPart.Velocity = Vector3.zero
			if begin_timer >= Config.base_frames.getup then
				SetState(PlayerStates.idle)
			end
		elseif begin_state == PlayerStates.land and begin_timer >= Config.base_frames.landing then
			SetState(PlayerStates.idle)
		elseif begin_state ~= PlayerStates.land and (begin_state ~= PlayerStates.air_tech or begin_timer > 2) and begin_state ~= PlayerStates.attack_ground then -- this line should be at the end. this prevent weird bugging when in an aerial state
			RootPart.Velocity = Vector3.zero
			SetState(PlayerStates.land)
		end
		local state,state_timer = GetState()
		if state == PlayerStates.idle or state == PlayerStates.walk or state == PlayerStates.walk_back or state == PlayerStates.crouch or state == PlayerStates.crouch_end or state == PlayerStates.crouch_start then
			GroundedInputHandler(state,state_timer)
		end
	end
	
	local function AirborneBehavior()
		local begin_state,begin_timer = GetState()
		local can_influence = false
		if begin_state == PlayerStates.jump then
			can_influence = true
			if begin_timer >= Config.base_frames.jump_end then
				SetState(PlayerStates.fall)
			end
		elseif begin_state == PlayerStates.hurt_air then
			if Inputs.Evasive then
				Evasive()
			end
		elseif begin_state == PlayerStates.grasped then
			RootPart.Velocity = Vector3.zero
		elseif (begin_state == PlayerStates.air_dash or begin_state == PlayerStates.air_dash_back) then
			if begin_timer >= Config.base_frames.air_dash_frames then
				SetState(PlayerStates.fall)
			end
		elseif begin_state == PlayerStates.attack_end then
			SetState(PlayerStates.idle)
		elseif begin_state == PlayerStates.djump_start then
			RootPart.Velocity = Vector3.zero
			if begin_timer >= Config.base_frames.djump then
				SetState(PlayerStates.djump)
				local influence = (Inputs.Move.x > 0.5 and 1) or (Inputs.Move.x < -0.5 and -1) or 0
				RootPart.Velocity = Vector3.new((influence*Config.speeds.jump_influence)+Model:GetAttribute("djump_stored_hspeed"),Config.speeds.djump*Model:GetAttribute("custom_jumppower"),0)
				-- i forgor
			end
		elseif begin_state == PlayerStates.djump then
			can_influence = true
			if begin_timer >= Config.base_frames.djump_end then
				SetState(PlayerStates.fall)
			end
		elseif begin_state == PlayerStates.attack_air then
			can_influence = true
		elseif begin_state ~= PlayerStates.fall and (begin_state == PlayerStates.air_tech and begin_timer < Config.base_frames.air_tech) == false then -- prevents weird behaviour from happening with grounded states
			can_influence = true
			SetState(PlayerStates.fall)
		end
		
		if can_influence and math.abs(Inputs.Move.x) > 0.25 then
			local add = Inputs.Move.x*Config.speeds.air_accel/60
			if (add > 0 and RootPart.Velocity.x + add < Config.speeds.air_speed) or (add < 0 and RootPart.Velocity.x + add > -Config.speeds.air_speed) then
				RootPart.Velocity = Vector3.new(math.clamp(RootPart.Velocity.x+add,-Config.speeds.air_speed,Config.speeds.air_speed),RootPart.Velocity.y,0)
			end
		end
		local state,state_timer = GetState()
		if state == PlayerStates.fall or state == PlayerStates.jump or state == PlayerStates.djump or state == PlayerStates.air_dash or state == PlayerStates.air_dash_back then
			AirInputHandler(state,state_timer)
		end
	end
	
	local function UpdateAttack()
		local state = GetState()
		if state == PlayerStates.attack or state == PlayerStates.attack_ground or state == PlayerStates.attack_air then
			local attack = Attacks[Model:GetAttribute("attack")]
			if attack then
				attack.Update(LifeTick)
			end
		end
	end
	
	local function Friction(friction:number,value:number)
		if value > 0 then
			value = math.clamp(value-friction,0,value)
		elseif value < 0 then
			value = math.clamp(value+friction,value,0)
		end
		return value
	end
	
	local function HandleCancelling()
		if Model:GetAttribute("cancel_type") == 1 then
			
		elseif Model:GetAttribute("cancel_type") == 2 then
			if Inputs.Move.Y > 0.5 or (LifeTick - InputBuffer[1] < 6 and InputBuffer[2].y > 0.5) then
				SetState(PlayerStates.jumpsquat)
			end
		end
	end
	
	local function Update(ip)
		Inputs = ip
		Server.Update(Inputs,InputBuffer,LifeTick)
		BufferHandler()
		if Model:GetAttribute("cancel_type") ~= 0 then Model:SetAttribute("cancel_type",0) end
		local x = Vector3.new(math.sign(RootPart.Velocity.x)*Config.collision_width,0,0)
		local y = (RootPart.Velocity.Y > 0 and Vector3.new(0,Config.hip_height,0)) or Vector3.new(0,-Config.hip_height,0)
		if Model:GetAttribute("state") == PlayerStates.grasped then x = Vector3.zero y = Vector3.zero end
		RootPart.CFrame += RootPart.Velocity/60
		local collisionCheck_X = RootPart.Position + x
		local collisionCheck_Y = RootPart.Position + y
		
		local prev_free = Model:GetAttribute("free")
		
		if collisionCheck_Y.y > 0 then
			if not prev_free then
				Model:SetAttribute("free",true)
				Model:SetAttribute("djumps",Config.max_djumps)
				Model:SetAttribute("air_dash_used",false)
				Model:SetAttribute("air_dash_back_used",false)
			end
			local gravity = Config.gravity * Model:GetAttribute("custom_gravity") / 60
			RootPart.Velocity += Vector3.new(0,gravity,0)
			local f = Config.frictions.air_friction
			local state,state_timer = GetState()
			if state == PlayerStates.hurt_air then
				f = Config.frictions.air_stun_friction
			elseif state == PlayerStates.air_dash or state == PlayerStates.air_dash_back then
				f = Config.frictions.air_dash_friction
			end
			RootPart.Velocity = Vector3.new(Friction(f*Model:GetAttribute("custom_friction"),RootPart.Velocity.x),RootPart.Velocity.y,0)
			AirborneBehavior()
		else
			if prev_free then
				Model:SetAttribute("free",false)
			end
			RootPart.CFrame = RootPart.CFrame.Rotation + Vector3.new(RootPart.Position.x,Config.hip_height,0)
			RootPart.Velocity = Vector3.new(Friction(Config.frictions.ground_state_friction*Model:GetAttribute("custom_friction"),RootPart.Velocity.x),0,0)
			GroundedBehavior()
		end
		
		UpdateAttack()
		HandleCancelling()
		
		if math.abs(collisionCheck_X.x*2) > FighterModule.StageLength then
			RootPart.CFrame = RootPart.CFrame.Rotation + Vector3.new(math.clamp(RootPart.Position.x,-FighterModule.StageLength/2+Config.collision_width,FighterModule.StageLength/2-Config.collision_width),RootPart.Position.y,0)
		end
		
		local state,timer = GetState()
		if state == PlayerStates.idle or state == PlayerStates.walk or state == PlayerStates.walk_back or state == PlayerStates.crouch or state == PlayerStates.fall or state == PlayerStates.jump or state == PlayerStates.crouch_end or state == PlayerStates.jumpsquat then
			if RootPart.Position.x < Model:GetAttribute("dir_point") and Model:GetAttribute("dir") ~= 1 then
				Model:SetAttribute("dir",1)
			elseif RootPart.Position.x > Model:GetAttribute("dir_point") and Model:GetAttribute("dir") ~= -1 then
				Model:SetAttribute("dir",-1)
			end
		end
		
		if TechBuffer > 0 then
			TechBuffer -= 1
			if TechBuffer <= 0 and state == PlayerStates.hurt_air and state_timer >= 20 then
				AirTech()
			end
		end
		
		-- very end
		if Server.PostUpdate then
			Server.PostUpdate(Inputs,InputBuffer,LifeTick)
		end
		LifeTick += 1
		oldInputs = Inputs
		Model:SetAttribute("state_timer",Model:GetAttribute("state_timer")+1)
	end
	
	return Update,Model
end

return FighterModule

the server script

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

local FighterModule = require(ReplicatedStorage:WaitForChild("Modules"):WaitForChild("FighterModule"))

local FighterObjects = {}

function GetPlayerInputs(Player:Player?)
	local Inputs = {
		Move = Vector2.zero,
		A = false,
		B = false,
		C = false,
		D = false,
		Evasive = false,
	}
	if not Player then return Inputs end -- no player
	local folder = ServerStorage:FindFirstChild("PlayerInputStorage"):FindFirstChild(Player.Name)
	if folder then
		Inputs.Move = folder:GetAttribute("Move") or false
		Inputs.A = folder:GetAttribute("A") or false
		Inputs.B = folder:GetAttribute("B") or false
		Inputs.C = folder:GetAttribute("C") or false
		Inputs.D = folder:GetAttribute("D") or false
		Inputs.Evasive = folder:GetAttribute("Evasive") or false
		if Inputs.Move.Magnitude > 1 then -- prevents exploiters from having a really high input for movement
			Inputs.Move = Inputs.Move.Unit
		end
	end
	return Inputs
end

function LoadCharactersOnClient()
	for i, v in ipairs(FighterObjects) do
		ReplicatedStorage.Remotes.ObjectAdded:FireAllClients(v[2],0)
	end
end

function AddFighterObject(name:string)
	local object = ReplicatedStorage.Assets.Fighters:FindFirstChild(name)
	if object then
		object = object:Clone()
		object:SetPrimaryPartCFrame(CFrame.new(0,10,0)) -- rotation doesn't matter
		object.Parent = workspace.Game.Objects
		local Config = require(object:WaitForChild("Config"))
		local Update = FighterModule.New(object,Config)
		table.insert(FighterObjects,{Update,object})
	else
		warn("could not find anything.")
	end
	return object
end

ReplicatedStorage.Remotes.InputRemote.OnServerEvent:Connect(function(Player,inputs)
	local folder = ServerStorage.PlayerInputStorage:FindFirstChild(Player.Name)
	if folder then
		folder:SetAttribute("A",inputs.A==true)
		folder:SetAttribute("B",inputs.B==true)
		folder:SetAttribute("C",inputs.C==true)
		folder:SetAttribute("D",inputs.D==true)
		folder:SetAttribute("Evasive",inputs.Evasive==true)
		folder:SetAttribute("Move",inputs.Move or Vector2.zero)
	end
end)

Players.PlayerAdded:Connect(function(Player)
	local InputFolder = Instance.new("Folder")
	InputFolder.Name = Player.Name
	InputFolder:SetAttribute("Move",Vector2.zero)
	InputFolder:SetAttribute("A",false)
	InputFolder:SetAttribute("B",false)
	InputFolder:SetAttribute("C",false)
	InputFolder:SetAttribute("D",false)
	InputFolder:SetAttribute("Evasive",false)
	InputFolder.Parent = ServerStorage:FindFirstChild("PlayerInputStorage")
	local object = AddFighterObject("TestGuy")
	local owner = Instance.new("ObjectValue")
	owner.Name = "Player"
	owner.Value = Player
	owner.Parent = object
end)

--AddFighterObject("TestGuy")

task.spawn(function()
	while task.wait() do
		for i, v in ipairs(FighterObjects) do
			local update = v[1]
			local object = v[2]
			if update and object ~= nil and object.Parent ~= nil then
				local Player = object:FindFirstChild("Player") and object:FindFirstChild("Player").Value
				update(GetPlayerInputs(Player))
			else
				table.remove(FighterObjects,i)
			end
		end
	end
end)

task.spawn(function()
	task.wait(4)
	LoadCharactersOnClient()
end)

send hlep.rbxl (107.2 KB)

4 Likes

Not tested anything but I’m assuming it’s to do with FigherObjects or cloned models that are not being cleaned up correctly. For example is “object” ever destroyed from AddFighterObject - I know you’re using table.remove but are you actually destroying the object and any events linked to it?

Also consider avoiding setting attributes every single frame if it’s unnecessary.

2 Likes

Thank you for taking your time to respond and help. I have not actually tested clearing the characters yet, as that isn’t my main issue. I’ll try replacing the attributes with number and string instances if that’ll help. I also used getAttribute() to check every attribute before setting it. It might have to do with something in the loading, cause when I run the game it’s already at 2000-2500 memory, and it gradually goes up slowly sometimes.

1 Like

I’d like to add that roblox studio has tons of bloat and memory leaks built-in, so the overall memory counters in studio are not accurate to the usage of the game itself.
I’m unsure of any solutions to see the server memory usage on a live server though.

2 Likes

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