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
-- 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