- Joined
- Feb 7, 2020
- Messages
- 398
edit: 10/05b - Added arcing missiles; got up to 1400 missiles with 45fps, 2000 with 20-30, then hit a sweet spot around 2100 where fps tanks to 10 - probably fighting with garbage collector at that point. Also, stress tested in local LAN and didn't get a desync.
edit: 10/05 - reworked this dumpster fire from the ground up into a class approach with methods.
edit: 10/05 - reworked this dumpster fire from the ground up into a class approach with methods.
Lua:
missile = {}
function missile:init()
self.__index = self
--[[
missile instance configurables:
--]]
self.height = 96 -- default z height.
self.radius = 60 -- how wide to search to collide.
self.vel = 14 -- default velocity.
self.pitch = 0 --
self.offset = 0 -- origin distance to project from x and y.
self.locku = nil -- set this to a unit to enable homing.
self.effect = nil -- special effect for missile.
self.model = nil -- special effect model.
self.angle = nil -- missile travel angle.
self.dist = nil -- distance to travel.
self.trav = nil -- distance traveled so far.
self.x = nil -- missile current x coord.
self.y = nil -- `` y coord.
self.z = nil -- `` z coord.
self.ox = nil -- origin coord.
self.oy = nil -- ``
self.oz = nil -- ``
self.func = nil -- function to run on every update.
self.time = nil -- use a travel time instead of travel distance.
self.elapsed = nil -- self.time converted to timer ticks required.
self.arc = false -- should missile arc?
self.collide = true -- destroy on collision.
self.colfunc = nil -- function to run on collision.
self.expl = false -- deal area-effect damage/healing.
self.explr = 300 -- explosion radius.
self.heal = false -- heal allies on collision instead.
self.coleff = nil -- play this effect on struck units.
self.maxdist = 3000 -- default max missile travel range.
--[[
missile class settings:
--]]
self.stack = {}
self.tmr = NewTimer()
self.cad = 0.04
self.acc = 3 -- how accurate are updates? (bigger = performant; scales with missile total).
self.total = 0 -- total missile instances.
self.tick = 0 -- the current timer tick loop (for collision accuracy).
self.buffer = 11 -- on missile destroy, run table clean if total > buffer.
self.sacc = self.acc -- cached accuracy; don't edit.
self.sradius = self.radius -- cached radius; don't edit.
--[[
toggles:
--]]
self.debug = false -- print debug counts, etc.
self.printc = true -- simple debug (print instance count).
end
--[[
basic missile creation functions:
--]]
function missile:create_timedxy(tarx, tary, dur, unit, model, dmg)
-- determine angle by @tarx,@tary target coord, lasts @dur seconds.
local mis = missile:create_targetxy(tarx, tary, unit, model, dmg)
mis.time = time
mis:initduration()
return mis
end
function missile:create_targetxy(tarx, tary, unit, model, dmg)
-- determine angle by @tarx,@tary target coord.
local ux, uy = utils.unitxy(unit)
local angle = utils.anglexy(ux, uy, tarx, tary)
local mis = missile:create_angle(angle, utils.distxy(ux, uy, tarx, tary), unit, model, dmg)
return mis
end
function missile:create_angle(angle, dist, unit, model, dmg)
-- send a missile towards a specified angle.
local mis = missile:new(unit)
mis.x, mis.y = utils.unitxy(unit)
mis.dist = dist
mis.dmg, mis.angle, mis.model = dmg, angle, model
mis:launch()
return mis
end
--[[
missile class functions:
--]]
function missile:new(owner)
local o = {}
setmetatable(o, self)
self.stack[o] = o
o.owner = owner
return o
end
function missile:launch()
-- optional properties:
if self.arc then
self.pitch = 0
end
if self.offset > 0 then
self.ox, self.ox = utils.projectxy(self.ox, self.oz, self.offset, self.angle)
end
-- initialize missile:
self.sradius = self.radius
self.trav = 0
self.p = utils.powner(self.owner)
self.effect = AddSpecialEffect(self.model, self.x, self.y)
self.oz = self:getheight()
self.ox, self.oy = self.x, self.y
-- check if timer should be rebooted:
if missile.total == 0 then
TimerStart(self.tmr, self.cad, true, function()
if missile.total <= 0 then
missile.total = 0
PauseTimer(self.tmr)
else
for _,mis in pairs(self.stack) do
mis:update()
end
end
end)
end
-- increment totals, run performance check:
missile.total = missile.total + 1
if self.total > 100 then
self.acc = self.acc + math.floor(self.total/100)
-- make radius scale up to help with reduced accuracy:
self.radius = math.floor(self.radius*(1+(self.acc-self.sacc)*0.1))
else
self.acc = self.sacc
end
if missile.printc then self:debugprint() end
self:update() -- (keep this last)
end
function missile:initduration()
if self.time then
self.elapsed = math.floor(self.time/self.cad)
self.dist = self.maxdist
end
end
function missile:update()
-- optional properties:
if self.func then
self.func(self)
end
if self.locku then
self.angle = utils.anglexy(self.x, self.y, utils.unitxy(self.locku))
end
-- missile positioning:
self.trav = self.trav + self.vel
self.x, self.y = utils.projectxy(self.x, self.y, self.vel, self.angle)
if self.tick == 0 then
self.z = self:getheight()
end
-- reposition missile effect:
BlzSetSpecialEffectYaw(self.effect, self.angle*bj_DEGTORAD)
BlzSetSpecialEffectX(self.effect, self.x)
BlzSetSpecialEffectY(self.effect, self.y)
BlzSetSpecialEffectZ(self.effect, self.z)
-- check for update accuracy:
if self.tick == self.acc then
self.tick = 0
else
self.tick = self.tick + 1
end
-- initiate destroy check:
if self.elapsed then
self.elapsed = self.elapsed - 1
end
if self:foundcollision() or self.trav >= self.dist then
self:destroy()
else
if (self.elapsed and self.elapsed <= 0) or self.trav >= self.dist then
self:destroy()
end
end
end
function missile:getheight()
return GetTerrainCliffLevel(self.x, self.y)*128 - 256 + self.height
end
function missile:foundcollision()
if self.tick == 0 then
local grp = g:newbyxy(self.p, g_type_dmg, self.x, self.y, self.radius)
if grp:getsize() > 0 then
self.hitu = grp:indexunit(0)
grp:destroy()
if self.colfunc then
self.colfunc(self)
end
if missile.debug then print("found collision for "..tostring(self)) end
return true
end
grp:destroy()
end
return false
end
function missile:explodemissile()
local grp = g:newbyxy(self.p, g_type_dmg, self.x, self.y, self.explr)
grp:action(function()
self:dealdmg(grp.unit)
end)
grp:destroy()
end
function missile:dealdmg(target)
if self.heal then
UnitDamageTarget(self.owner, target, -self.dmg, true, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
else
UnitDamageTarget(self.owner, target, self.dmg, true, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
end
if self.coleff then
utils.speffect(self.coleff, utils.unitxy(target))
end
end
function missile:destroy()
if self.hitu and not self.expl then
self:dealdmg(self.hitu)
elseif self.expl then
self:explodemissile()
end
if self.total > 500 then
-- if we're in bananas count territory, render death effect off screen.
BlzSetSpecialEffectZ(self.effect, 3500.0)
end
DestroyEffect(self.effect)
missile.total = missile.total - 1
self.stack[self] = nil
if missile.total == 0 or missile.total > self.buffer then
missile:recycle()
end
if missile.printc then self:debugprint() end
end
function missile:recycle()
utils.tablecollapse(self.stack)
if missile.debug then print("recycle buffer met, recycling") end
end
function missile:debugprint()
ClearTextMessages()
print("Instances: "..missile.total)
end
local oldglobals = InitGlobals
function InitGlobals()
utils.debugfunc(function()
oldglobals()
g:init()
missile:init()
print("Issue attack order = basic missile.")
print("Issue patrol order = arcing missile.")
print("Issue move order = 50 spiraling missiles (stress test).")
end, "InitGlobals")
end
Lua:
LuaMissileTimer = NewTimer() -- global missile Timer.
LuaMissileCadence = .03 -- how fast the missile updates and checks for collision (higher = less accurate).
LuaMissileStack = {} -- where instances of missiles are stored.
LuaMissile = {} -- initialize class table.
--[[
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
[required]
@ownerUnit = unit = unit that creates the missile and who deals damage to targets on collision.
@landingX = float = X of coord for missile to move towards.
@landingY = float = Y of coord for missile to move towards.
[optional] - see LuaMissile.create for defaults.
@distance = float = how far should the missile travel.
@duration = float = how long should the missile last (seconds).
@damage = float = how much damage should the missile do.
@hitRadius = float = how far should the missile check to count as a collision.
@collides = bool = does the missile destroy itself on collision.
@heroOnly = bool = does the missile only collide with heroes.
@allies = bool = does the missile collide with allies instead of enemies.
@excludeUnit = unit = should the missile not hit a target (e.g. the caster).
@dmgFunc = function = should there be an extra effect on the unit when hit (callback); include unit as param: myDmgFunc(u).
@effect = effect = the model of the missile.
@effectDmg = effect = the special effect on collision.
@height = float = offset from the ground (Z).
@scale = float = scale of @effect.
@timerFunc = function = should there be extra calculations on timer update; include object as param: myTimerFunc(o).
@aType = attacktype = pass attack type for more advanced use cases.
@dType = damagetype = pass damage type for more advanced use cases.
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--]]
function LuaMissile.create(ownerUnit, landingX, landingY, distance, duration, damage, hitRadius, collides, heroOnly, allies,
excludeUnit,dmgFunc,effect,effectDmg,height,scale,timerFunc,aType,dType)
local o = {}
o.ownerUnit = ownerUnit
o.owner = GetOwningPlayer(ownerUnit)
o.landingX = landingX
o.landingY = landingY
o.missileX = GetUnitX(ownerUnit)
o.missileY = GetUnitY(ownerUnit)
o.missileZ = 0.0
o.traveled = 0.0
-- set values or defaults:
o.effect = effect or 'Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl'
o.effectDmg = effectDmg or 'Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl'
o.height = height or 60.0
o.scale = scale or 1.0
o.duration = duration or 1.5
o.distance = distance or 600.0
o.damage = damage or 100.0
o.hitRadius = hitRadius or 65.0
o.collides = collides or false
o.heroOnly = heroOnly or false
o.allies = allies or false
o.dmgFunc = dmgFunc or nil
o.timerFunc = timerFunc or nil
o.excludeUnit = excludeUnit or nil
o.aType = aType or ATTACK_TYPE_NORMAL
o.dType = dType or DAMAGE_TYPE_NORMAL
--
o.velocity = distance / ( duration / LuaMissileCadence );
o.angle = AngleBetweenPointsXY(o.missileX, o.missileY, landingX, landingY)
o.effect = AddSpecialEffect(effect, o.missileX, o.missileY)
o.searchGroup = CreateGroup()
o.damageGroup = CreateGroup()
if allies then
o.filter = LuaMissile.filterAllies
else
o.filter = LuaMissile.filterEnemies
end
BlzSetSpecialEffectScale(o.effect, scale)
LuaMissile.addToStack(o)
return o
end
-- add the missile to the stack:
-- @o = missile object key
function LuaMissile.addToStack(o)
table.insert(LuaMissileStack, o)
LuaMissile.startTimer()
end
-- start or stop the Timer based on active missiles:
function LuaMissile.startTimer()
if tablelength(LuaMissileStack) == 1 and TimerGetRemaining(LuaMissileTimer) == 0.0 then
TimerStart(LuaMissileTimer, LuaMissileCadence, true, LuaMissile.updateStack)
end
end
-- start or stop the Timer based on active missiles:
function LuaMissile.timerCheck()
if tablelength(LuaMissileStack) < 1 then
PauseTimer(LuaMissileTimer)
end
end
-- run missile update for the stack:
function LuaMissile.updateStack()
for o in pairs(LuaMissileStack) do
if LuaMissileStack[o].traveled > LuaMissileStack[o].distance then
LuaMissile.destroy(o)
elseif LuaMissileStack[o] then
LuaMissile.update(o)
end
end
end
-- move the missile:
-- @o = missile object key
function LuaMissile.update(o)
LuaMissileStack[o].traveled = LuaMissileStack[o].traveled + LuaMissileStack[o].velocity
LuaMissileStack[o].missileX,LuaMissileStack[o].missileY = PolarProjectionXY(LuaMissileStack[o].missileX, LuaMissileStack[o].missileY,
LuaMissileStack[o].velocity, LuaMissileStack[o].angle)
LuaMissileStack[o].missileZ = GetTerrainCliffLevel(LuaMissileStack[o].missileX,LuaMissileStack[o].missileY)*128 - 256 + LuaMissileStack[o].height
BlzSetSpecialEffectYaw(LuaMissileStack[o].effect, LuaMissileStack[o].angle*bj_DEGTORAD) -- update facing angle of missile
BlzSetSpecialEffectX(LuaMissileStack[o].effect, LuaMissileStack[o].missileX)
BlzSetSpecialEffectY(LuaMissileStack[o].effect, LuaMissileStack[o].missileY)
BlzSetSpecialEffectZ(LuaMissileStack[o].effect, LuaMissileStack[o].missileZ)
if LuaMissileStack[o].timerFunc ~= nil then
LuaMissileStack[o].timerFunc(o)
end
LuaMissile.collisionCheck(o)
end
-- check if missile should do damage:
-- @o = missile object key
function LuaMissile.collisionCheck(o)
GroupEnumUnitsInRange(LuaMissileStack[o].searchGroup, LuaMissileStack[o].missileX, LuaMissileStack[o].missileY, LuaMissileStack[o].hitRadius,
Condition( function() return LuaMissileStack[o].filter(o, GetFilterUnit()) and GetFilterUnit() ~= LuaMissileStack[o].excludeUnit end ) )
if BlzGroupGetSize(LuaMissileStack[o].searchGroup) > 0 then
GroupUtilsAction(LuaMissileStack[o].searchGroup, function()
if not IsUnitInGroup(LUA_FILTERUNIT,LuaMissileStack[o].damageGroup) then
LuaMissile.collision(o, LUA_FILTERUNIT) end
end)
end
end
-- deal damage to the unit:
-- @o = missile object key
-- @u = unit to damage
function LuaMissile.collision(o, u)
UnitDamageTarget(LuaMissileStack[o].ownerUnit, u, LuaMissileStack[o].damage, true, false, LuaMissileStack[o].aType, LuaMissileStack[o].dType, WEAPON_TYPE_WHOKNOWS)
if LuaMissileStack[o].dmgFunc ~= nil then
LuaMissileStack[o].dmgFunc(u, o)
end
if LuaMissileStack[o].effectDmg then
DestroyEffect(AddSpecialEffect(LuaMissileStack[o].effectDmg,GetUnitX(u),GetUnitY(u)))
end
if LuaMissileStack[o].collides then
LuaMissile.destroy(o)
else
GroupUtilsAction(LuaMissileStack[o].searchGroup, function()
GroupAddUnit(LuaMissileStack[o].damageGroup,LUA_FILTERUNIT)
GroupRemoveUnit(LuaMissileStack[o].searchGroup,LUA_FILTERUNIT)
end)
end
end
-- destroy the missile instance:
-- @o = missile object key
function LuaMissile.destroy(o)
DestroyEffect(LuaMissileStack[o].effect)
DestroyGroup(LuaMissileStack[o].damageGroup)
DestroyGroup(LuaMissileStack[o].searchGroup)
LuaMissileStack[o] = nil
LuaMissile.timerCheck()
end
-- @o = missile object key
-- @u = unit to check
function LuaMissile.filterEnemies(o, u)
return IsUnitEnemy(u,LuaMissileStack[o].owner)
and not IsUnitDeadBJ(u)
and not IsUnitType(u,UNIT_TYPE_STRUCTURE)
and not IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE)
and not IsUnitInGroup(u, LuaMissileStack[o].damageGroup)
end
-- @o = missile object key
-- @u = unit to check
function LuaMissile.filterAllies(o, u)
return IsUnitAlly(u,LuaMissileStack[o].owner)
and not IsUnitDeadBJ(u)
and not IsUnitType(u,UNIT_TYPE_STRUCTURE)
and not IsUnitInGroup(u, LuaMissileStack[o].damageGroup)
end
Attachments
Last edited: