• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Simple Missile

Status
Not open for further replies.
Level 14
Joined
Feb 7, 2020
Messages
387
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.

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

  • WC3ScrnShot_100520_162812_001.png
    WC3ScrnShot_100520_162812_001.png
    3.6 MB · Views: 50
  • WC3ScrnShot_100520_163402_001.png
    WC3ScrnShot_100520_163402_001.png
    3.7 MB · Views: 61
  • lua_simple_missile_1.0.w3m
    32.6 KB · Views: 41
Last edited:
Level 14
Joined
Feb 7, 2020
Messages
387
Why not track each missile instance as its own Table with members (like an object) instead of using a set of tables? This way nilling members might not be required as one could leave it to the Lua garbage collector to remove.

This is something I've been considering doing in a refactor once I get around to needing missiles again. I would have done this originally but I didn't understand global Lua variable context or table syntax very well at the time. Now, I make everything in tables :)

edit - this suggestion was added in v5
 
Last edited:
Level 14
Joined
Feb 7, 2020
Messages
387
I ended up reworking this unholy abomination with a lightweight starting point for a missile generator class and got into the weeds of performance; I can get up to 1,000 basic missiles on my own machine (laptop version of 1070 and i7) before seeing 30 frames or less. Granted, there's not a whole lot of calculations going on. Any comments on if that's a crap starting point?

Not sure where you'd need this kind of missile madness but I noticed BPower's Missile library uses units. I'm curious if there would be a significant performance gain by converting from units to special effects, and if Lua vs. JASS would make any difference.

I speak from a position of both curiosity and ignorance.

:ugly:
 
Level 1
Joined
Apr 8, 2020
Messages
110
You could compare BPower's Missile library to AGD's update for units to special effects comparison. However, I would not entirely written off units even in a special effects version. AGD still maintains the ability to use units as missiles in his version.

Edit: AGD's latest edition has written off units and delegated them to a knockback system or something similar.
 
Last edited:
Status
Not open for further replies.
Top