Heartbeat, Stepped Or? to avoid potential lag issues

I am currently in the process of foundation work for a combat game I am wondering if some developers with more completed game experience could give me some feedback on if my system will be too laggy in its current form. Let me layout some details.

Currently I am look at having 10 “Heroes” on the field together that will simulator combat in a TFT style auto combat. I am using Heartbeat to run the combat loop and It doesn’t lag in testing but thats with only 1 player.

If I am going to run

  • 12 Player lobbies, so potentially 12 combats going at 1 time
  • 10 Heroes each, all targetting, move towards targets, running auto attacks with upto 3 additonal abilities
  • Calculating Attack Vs Defense and other stats for damage
  • Tracking HP & Stamina for other abilities and ultimates
  • Animated characters
  • Attack animations

and probably even more, will something like Heartbeat or Stepped cause way too much lag? How does a system like a Tower Defense game handle much more Units on screen all with some form of AI run without too much lag

Here is very simple version of the combat handler I am using. I tried to add as many notes as possible for anyone curious about how things are running. This is a difficult questions to give enough information for, which I understand makes answering also difficult. Thankyou in advance :pray:

-- Start combat for the given player
function CombatHandler:StartCombat(player, arenaData)
	local playerArenas = FloorHandler.GetActiveArenas()

	if not playerArenas or not playerArenas[player.UserId] then
		warn("No arena data found for player: " .. player.Name)
		return
	end

	local playerArena = playerArenas[player.UserId]
	local playerHeroes = playerArena.heroes
	local npcEnemies = playerArena.npcEnemies

	if not playerHeroes or not npcEnemies then
		warn("Missing heroes or NPC enemies for player: " .. player.Name)
		return
	end

	-- Timer to control auto-attack frequency
	local attackTimers = {}
	local energyTracker = {} -- Track energy for each hero during combat
	local moveUpdateTimers = {} -- Track movement update timers
	local combatInterval = 0.2 -- 0.2 seconds interval for combat checks (adjustable)

	-- Initialize attack timers and movement timers for heroes and NPCs
	for _, heroData in pairs(playerHeroes) do
		attackTimers[heroData.Model] = 0
		energyTracker[heroData.Model] = { Energy = 0 }
		moveUpdateTimers[heroData.Model] = 0 -- Track how often the hero checks for movement
	end
	for _, npcData in pairs(npcEnemies) do
		attackTimers[npcData.Model] = 0
		energyTracker[npcData.Model] = { Energy = 0 }
		moveUpdateTimers[npcData.Model] = 0 -- Track NPC movement timers
	end

	-- Helper function to load hero abilities
	local function loadHeroAbilities(heroModel)
		local heroName = heroModel.Name
		local heroAbilitiesModule = HeroAbilities[heroName]

		if not heroAbilitiesModule then
			warn("No ability module found for hero: " .. heroName)
			return nil
		end

		if not heroAbilitiesModule.AutoAttackStats then
			warn("AutoAttackStats not defined properly for hero: " .. heroName)
			return nil
		end

		return heroAbilitiesModule
	end

	-- Function to handle attacks
	local function handleAttacks(attacker, target, abilityModule, stats, deltaTime)
		if attackTimers[attacker] <= 0 and abilityModule:IsInRange(target.PrimaryPart.Position, attacker.PrimaryPart.Position) then
			local targetStats = {
				Attack = target:GetAttribute("Attack"),
				Defense = target:GetAttribute("Defense"),
				CurrentHP = target:GetAttribute("CurrentHP"),
				Speed = target:GetAttribute("Speed"),
				MaxHP = target:GetAttribute("MaxHP")
			}

			local damageDealt = abilityModule:AutoAttack(target, stats)

			-- Log the damage dealt for debugging
			print(attacker.Name .. " dealt " .. damageDealt .. " damage to " .. target.Name)

			-- Update the target's health bar
			FloorHandler.updateHealthBar(target, damageDealt)

			-- Check if the target is defeated
			if target:GetAttribute("CurrentHP") <= 0 then
				print(target.Name .. " has been defeated!")
				self:HandleDefeat(target)
			end

			attackTimers[attacker] = abilityModule.AutoAttackStats.AttackSpeed
		else
			attackTimers[attacker] = math.max(attackTimers[attacker] - deltaTime, 0) -- Reduce cooldown
		end
	end

	-- Combat loop function (called at regular intervals, not every frame)
	local function runCombatLoop(deltaTime)
		-- Handle Player Heroes
		for _, heroData in pairs(playerHeroes) do
			local heroModel = heroData.Model
			local heroAbilitiesModule = loadHeroAbilities(heroModel)

			if heroModel and heroModel.Parent and heroAbilitiesModule then
				local heroStats = {
					Attack = heroData.Attack,
					Defense = heroData.Defense,
					CurrentHP = heroData.CurrentHP,
					Speed = heroData.Speed,
					MaxHP = heroData.MaxHP,
					Energy = energyTracker[heroModel].Energy, -- Track energy during combat
					Model = heroModel
				}

				-- Find nearest NPC
				local targetNPC = self:GetNearestNPC(heroModel, npcEnemies)
				if targetNPC then
					-- Only check movement at a reduced frequency (not every frame)
					moveUpdateTimers[heroModel] = moveUpdateTimers[heroModel] + deltaTime
					if moveUpdateTimers[heroModel] >= combatInterval then
						self:MoveTowardsTarget(heroModel, targetNPC, heroAbilitiesModule)
						moveUpdateTimers[heroModel] = 0 -- Reset movement timer
					end

					-- Handle attacks
					handleAttacks(heroModel, targetNPC, heroAbilitiesModule, heroStats, deltaTime)
				end
			end
		end

		-- Handle NPC Heroes
		for _, npcData in pairs(npcEnemies) do
			local npcModel = npcData.Model
			local npcAbilitiesModule = loadHeroAbilities(npcModel)

			if npcModel and npcModel.Parent and npcAbilitiesModule then
				local npcStats = {
					Attack = npcData.Attack,
					Defense = npcData.Defense,
					CurrentHP = npcData.CurrentHP,
					Speed = npcData.Speed,
					MaxHP = npcData.MaxHP,
					Energy = energyTracker[npcModel].Energy -- Track energy for NPCs
				}

				-- Find nearest Player Hero
				local targetHero = self:GetNearestHero(npcModel, playerHeroes)
				if targetHero then
					-- Only check movement at a reduced frequency (not every frame)
					moveUpdateTimers[npcModel] = moveUpdateTimers[npcModel] + deltaTime
					if moveUpdateTimers[npcModel] >= combatInterval then
						self:MoveTowardsTarget(npcModel, targetHero, npcAbilitiesModule)
						moveUpdateTimers[npcModel] = 0 -- Reset movement timer
					end

					-- Handle attacks
					handleAttacks(npcModel, targetHero, npcAbilitiesModule, npcStats, deltaTime)
				end
			end
		end
	end

	-- Main loop to manage combat flow
	game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
		runCombatLoop(deltaTime)
	end)

	print("Combat started for player: " .. player.Name)
end

Ask yourself this: Does it really all need to be updated every frame, or could you use an event based approach? What needs to be updated/rendered every frame, what doesn’t?

1 Like

This is kind of exactly what I needed to hear. Ill transition to event based for my core stuff and then if I want to I can probably use Heartbeat for movement and target tracking since its significantly smoother with Heartbeat then anything else I’ve devised

1 Like