Something I have been working on a little here and there the past few days.
The main issue I have with the extended knockback system is that you can't have multiple slides on a unit - I alleviate this issue by calculating the vector components of the current slide and the updated slide, which allows me to implement concurrent slides all into one instance.
Also, a few of the things in the previous are a little archaic (ex. no static ifs), that along with its heavy system requirements makes for a less then desirable system.
I chose a unit group enum just to try something different (initially) - after proving to myself that it was tormentingly slow, I switched to an array stack.
Examples of direct struct method use:
The slide struct should be treated as a unique object, and used as such.
Wrappers for these methods are also included for GUI users.
Critiques and other thoughts welcome.
The main issue I have with the extended knockback system is that you can't have multiple slides on a unit - I alleviate this issue by calculating the vector components of the current slide and the updated slide, which allows me to implement concurrent slides all into one instance.
Also, a few of the things in the previous are a little archaic (ex. no static ifs), that along with its heavy system requirements makes for a less then desirable system.
I chose a unit group enum just to try something different (initially) - after proving to myself that it was tormentingly slow, I switched to an array stack.
Examples of direct struct method use:
JASS:
function Actions takes nothing returns nothing
local slide d=slide.create(Hero,GetRandomReal(0.,360.),375.,0.)
call d.destroy()
endfunction
function Blurg takes nothing returns boolean
local slide s
if GetEventPlayerChatString()=="sliding" then
if slide.isunitsliding(Hero) then
call BJDebugMsg("yes")
else
call BJDebugMsg("no")
endif
return false
elseif GetEventPlayerChatString()=="stop" then
call slide.unitstopslide(Hero)
return false
elseif GetEventPlayerChatString()=="get" then
set s=slide.unitgetslide(Hero)
return false
endif
return true
endfunction
//===========================================================================
function InitTrig_Knock takes nothing returns nothing
local trigger t = CreateTrigger( )
call TriggerAddAction( t, function Actions )
call TriggerAddCondition( t, Condition(function Blurg) )
call TriggerRegisterPlayerChatEvent( t, Player(0), "knock", true )
call TriggerRegisterPlayerChatEvent( t, Player(0), "sliding", true )
call TriggerRegisterPlayerChatEvent( t, Player(0), "stop", true )
call TriggerRegisterPlayerChatEvent( t, Player(0), "get", true )
endfunction
The slide struct should be treated as a unique object, and used as such.
Wrappers for these methods are also included for GUI users.
Critiques and other thoughts welcome.
JASS:
library SlideSystem needs optional GroupUtils, optional TimerUtils, optional DestructableLib, optional IsTerrainWalkable
globals
//==Configurables==\\
private constant boolean ALLOWMOVE = false // Allow movement while units slide
private constant boolean COLLIDE = true // Unit/Structure collision detection
private constant boolean CLIFF = true // Cliff detection, stops unit movement
private constant boolean REHIT = true // Can collide with the same target more then once during a single slide event
private constant boolean SINCELAST = false // When tracking total slide time and distance, will reset value for each newly created slide
private constant boolean TREES = true // Destroy trees in sliding path
private constant integer OFFSET = 0x100000 // Id of the first handle in your map, if unsure don't change
private constant real BREAK = 10. // Velocity below which units stop sliding
private constant real CLIFFBREAK = 75. // Break point for a terrian height change to be considered a cliff, if not using IsTerrainWalkable library
private constant real DECEL = .95 // In the abscence of a linear deceleration set point, DECEL/TIMEOUT
private constant real FACTORA = .66 // Percentage of slide velocity transfered to colliding units
private constant real FACTORB = .66 // Percentage of slide velocity retained after a collision
private constant real FLYBREAK = 150. // Units above this fly height will not be struck by collisions or destroy trees
private constant real RADIUS = 180. // Collision detection radius
private constant real REHITTIME = .4 // Elapsed time required before a unit can be collided with again
private constant real TIMEOUT = .05 // Periodic timer interval
private constant string AIR = "sfx\\AirSlide.mdx" // Flying units slide effect
private constant string ATTACH = "origin" // Slide effects attachment point
private constant string HIT = "Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl" // Collision effect
private constant string LAND = "sfx\\LandSlide.mdx" // Land slide effect
private constant string SEA = "sfx\\SeaSlide.mdx" // Water slide effect
endglobals
// Establish custom unit collision conditions
private function CollisionFilter takes unit slider, unit target returns boolean
return true
endfunction
//==NO TOUCHING PAST THIS POINT==\\
globals
private boolexpr TREEBOOL
private boolexpr UNITBOOL
private group COLLISION = CreateGroup()
private hashtable HT // Only needed if no TimerUtils
private integer COUNT = 0
private slide array DATA
private slide array DATA2 // Remove O(n) searches
private slide TEMPDATA
private location LOC = Location(0.,0.)
private constant real FRAMERATE = 1./TIMEOUT
private constant real HALFRADIUS = RADIUS/2.
private constant real REALBREAK = BREAK/FRAMERATE
private rect RECT
private unit T
private unit TREECHECK
// Incase either of these need to be accessed
public group GROUP = CreateGroup()
public timer TIMER = CreateTimer()
endglobals
private struct rehit
timer t = null
unit target = null
unit slider = null
group g = null
method destroy takes nothing returns nothing
// Clean up
static if LIBRARY_TimerUtils then
call ReleaseTimer(this.t)
else
call FlushChildHashtable(HT,GetHandleId(this.t))
call PauseTimer(this.t)
call DestroyTimer(this.t)
endif
call this.deallocate()
endmethod
static method timeout takes nothing returns nothing
local rehit r=GetTimerData(GetExpiredTimer())
// Unit is still sliding
if IsUnitInGroup(r.slider,GROUP) then
call GroupRemoveUnit(r.g,r.target)
endif
call r.destroy()
endmethod
static method create takes unit target, unit slider, group g returns rehit
local rehit r=rehit.allocate()
static if LIBRARY_TimerUtils then
set r.t=NewTimer()
call SetTimerData(r.t,r)
else
set r.t=CreateTimer()
call SaveInteger(HT,GetHandleId(r.t),0,r)
endif
set r.target=target
set r.slider=slider
set r.g=g
// Remove target from sliders hit group after a duration
call TimerStart(r.t,REHITTIME,false,function rehit.timeout)
return r
endmethod
endstruct
private interface face
method onPeriodic takes nothing returns nothing defaults nothing
method onEnd takes nothing returns nothing defaults nothing
method onHit takes nothing returns nothing defaults nothing
endinterface
struct slide extends face
private effect sfx = null
private integer marker = 0 // mark spot in movement loop stack
private real cos = 0.
private real decel = 0.
private real sin = 0.
public group g = null
public real angle = 0.
public real velocity = 0.
readonly boolean customsfx = false
readonly boolean fly = false
readonly boolean land = true
readonly boolean trees = true
readonly real totaldist = 0.
readonly real totaltime = 0.
readonly unit slider = null
method destroy takes nothing returns nothing
// Clean up
if .onEnd.exists then
call .onEnd.execute()
endif
call DestroyEffect(.sfx)
static if LIBRARY_GroupUtils then
call ReleaseGroup(.g)
else
call DestroyGroup(.g)
endif
call GroupRemoveUnit(GROUP,.slider)
set DATA[.marker]=DATA[COUNT]
set DATA[.marker].marker=.marker
set COUNT=COUNT-1
call .deallocate()
endmethod
private static method unitfilter takes nothing returns boolean
local slide d=TEMPDATA
local slide dd
local rehit r
local real xT
local real yT
local real x
local real y
local timer t
set T=GetFilterUnit()
// Make sure target is alive and not invulnerable, is not the slider and has not already been hit, and isn't flying too high, along with
// conditions from the custom user edited filter
if CollisionFilter(d.slider,T) and GetUnitFlyHeight(T)<=FLYBREAK and GetUnitAbilityLevel(T,039;Avul039;)==0 and UnitAlive(T) and not IsUnitInGroup(T,d.g) and T!=d.slider then
if d.onHit.exists then
call d.onHit.execute()
endif
set xT=GetUnitX(T)
set yT=GetUnitY(T)
set x=GetUnitX(d.slider)
set y=GetUnitY(d.slider)
// Only begin a slide on the target if its not a structure
if IsUnitType(T,UNIT_TYPE_STRUCTURE)==false then
// Start collision slide
set dd=slide.create(T,bj_RADTODEG*Atan2(yT-y,xT-x),(d.velocity*FRAMERATE)*FACTORA,d.decel)
call GroupAddUnit(dd.g,d.slider)
call DestroyEffect(AddSpecialEffectTarget(HIT,T,ATTACH))
// If allowed, start re-hit effects
static if REHIT then
set r=rehit.create(T,d.slider,d.g)
endif
endif
// Fancy maths for deflection, see my GetDeflectionAngle script
set d.angle=d.angle+180.+(2.*((bj_RADTODEG*Atan2(yT-y,xT-x))-d.angle))
set d.cos=Cos(d.angle*bj_DEGTORAD)
set d.sin=Sin(d.angle*bj_DEGTORAD)
set d.velocity=d.velocity*FACTORB
call DestroyEffect(AddSpecialEffectTarget(HIT,d.slider,ATTACH))
call GroupAddUnit(d.g,T)
endif
return false
endmethod
private static method killtrees takes nothing returns boolean
// Make sure the destructable is actually a tree
static if not LIBRARY_DestructableLib then
if IssueTargetOrder(TREECHECK,"harvest",GetFilterDestructable()) then
call KillDestructable(GetFilterDestructable())
endif
else
if IsDestructableTree(GetFilterDestructable()) then
call KillDestructable(GetFilterDestructable())
endif
endif
return false
endmethod
private static method movement takes nothing returns nothing
local slide d
local integer i=1
local real x
local real xc
local real y
local real yc
local real z
loop
exitwhen i>COUNT
set d=DATA<i>
// Slide is over
if d.velocity<=REALBREAK or not UnitAlive(d.slider) then
// Clean up
call d.destroy()
set i=i-1
// Slide is not over
else
// Caculate positioning
set x=GetUnitX(d.slider)+d.velocity*d.cos
set y=GetUnitY(d.slider)+d.velocity*d.sin
if d.onPeriodic.exists then
call d.onPeriodic.execute()
endif
// Destroy trees
static if TREES then
if d.trees then
call MoveRectTo(RECT,x,y)
static if not LIBRARY_DestructableLib then
call PauseUnit(TREECHECK,false)
endif
call EnumDestructablesInRect(RECT,TREEBOOL,null)
static if not LIBRARY_DestructableLib then
call PauseUnit(TREECHECK,true)
endif
endif
endif
// Cliff detection
static if CLIFF then
// Check out in front of the slider a bit in the direction it's traveling
set xc=x+HALFRADIUS*d.cos
set yc=y+HALFRADIUS*d.sin
static if LIBRARY_IsTerrainWalkable then
// Cliff found
if not IsTerrainWalkable(xc,yc) then
call d.destroy()
endif
else
call MoveLocation(LOC,x,y)
set z=GetLocationZ(LOC)
call MoveLocation(LOC,xc,yc)
// Cliff found
if GetLocationZ(LOC)>z+CLIFFBREAK or GetLocationZ(LOC)<z-CLIFFBREAK then
call d.destroy()
endif
endif
endif
// Update position and slide speed
static if ALLOWMOVE then
call SetUnitX(d.slider,x)
call SetUnitY(d.slider,y)
else
call SetUnitPosition(d.slider,x,y)
endif
// Update total time and distance slid
set d.totaldist=d.totaldist+d.velocity
set d.totaltime=d.totaltime+TIMEOUT
// If a deceleration has been set, use it
if d.decel!=0. then
set d.velocity=d.velocity-d.decel
else
set d.velocity=d.velocity*DECEL
endif
// Update slide effects
if not d.fly or d.customsfx then
// Ground
if IsTerrainPathable(x,y,PATHING_TYPE_FLOATABILITY) or not IsTerrainPathable(x,y,PATHING_TYPE_ANY) then
if not d.land then
set d.land=true
call DestroyEffect(d.sfx)
set d.sfx=AddSpecialEffectTarget(LAND,d.slider,ATTACH)
endif
// Water
elseif not IsTerrainPathable(x,y,PATHING_TYPE_WALKABILITY) then
if d.land then
set d.land=not d.land
call DestroyEffect(d.sfx)
set d.sfx=AddSpecialEffectTarget(SEA,d.slider,ATTACH)
endif
endif
endif
// Collision detection
static if COLLIDE then
set TEMPDATA=d
call GroupEnumUnitsInRange(COLLISION,x,y,RADIUS,UNITBOOL)
endif
endif
set i=i+1
endloop
if COUNT==0 then
call PauseTimer(TIMER)
endif
endmethod
//==User Methods==\\
// Create a new instance for a unit, deceleration=0 to use the DECEL global
static method create takes unit slider, real angle, real velocity, real deceleration returns slide
local slide this
local integer id=GetHandleId(slider)-OFFSET
local real x
local real y
// Unit wasn't previously sliding
if not IsUnitInGroup(slider,GROUP) then
// Initial slide struct setup
set this=slide.allocate()
set .slider=slider
set .angle=angle
set .velocity=velocity/FRAMERATE
set .cos=Cos(.angle*bj_DEGTORAD)
set .sin=Sin(.angle*bj_DEGTORAD)
if deceleration!=0 then
set .decel=deceleration/FRAMERATE
endif
static if LIBRARY_GroupUtils then
set .g=NewGroup()
else
set .g=CreateGroup()
endif
set x=GetUnitX(.slider)
set y=GetUnitY(.slider)
call GroupAddUnit(GROUP,slider)
// Unit is a flyer
if IsUnitType(slider,UNIT_TYPE_FLYING)==true then
set .fly=not .fly
set .sfx=AddSpecialEffectTarget(AIR,.slider,ATTACH)
// Too high??
if GetUnitFlyHeight(slider)>FLYBREAK then
set .trees=not .trees
endif
else
// Ground Stairs don't have any pathing...
if IsTerrainPathable(x,y,PATHING_TYPE_FLOATABILITY) or not IsTerrainPathable(x,y,PATHING_TYPE_ANY) then
set .sfx=AddSpecialEffectTarget(LAND,.slider,ATTACH)
// Water
elseif not IsTerrainPathable(x,y,PATHING_TYPE_WALKABILITY) then
set .sfx=AddSpecialEffectTarget(SEA,.slider,ATTACH)
set .land=false
endif
endif
// Store struct data
set DATA2[id]=this
set COUNT=COUNT+1
set DATA[COUNT]=this
set .marker=COUNT
// First sliding unit
if COUNT==1 then
call TimerStart(TIMER,TIMEOUT,true,function slide.movement)
endif
// Unit was already sliding
else
// Calculate vector components of slide velocity
set this=DATA2[id]
set velocity=velocity/FRAMERATE
set x=.velocity*.cos+(velocity)*Cos(angle*bj_DEGTORAD)
set y=.velocity*.sin+(velocity)*Sin(angle*bj_DEGTORAD)
// Update slide direction and velocity
set .velocity=SquareRoot(x*x+y*y)
set .angle=Atan2(y,x)
set .cos=Cos(.angle)
set .sin=Sin(.angle)
set .angle=.angle*bj_RADTODEG
if deceleration!=0 then
set .decel=deceleration/FRAMERATE
endif
// Reset total slide time and distance
static if SINCELAST then
set .slidedist=0.
set .totaltime=0.
endif
endif
return this
endmethod
// Retrieve the units current slide information
static method unitgetslide takes unit slider returns slide
debug if IsUnitInGroup(slider,GROUP) then
return DATA2[GetHandleId(slider)-OFFSET]
debug else
debug call BJDebugMsg(SCOPE_PREFIX+" UnitGetSlide Error: Unit is not sliding.")
debug return 0
debug endif
endmethod
// Is the unit currently sliding?
static method isunitsliding takes unit slider returns boolean
return IsUnitInGroup(slider,GROUP)
endmethod
// Stop the unit from sliding immediately
static method unitstopslide takes unit slider returns boolean
if IsUnitInGroup(slider,GROUP) then
call DATA2[GetHandleId(slider)-OFFSET].destroy()
return true
else
debug call BJDebugMsg(SCOPE_PREFIX+" UnitStopSlide Error: Unit is not sliding.")
return false
endif
endmethod
// Incase you need to set a custom slide effect mid slide
// Use "" to restore automatic sfx updates
static method unitsetslideeffect takes unit slider, string sfx returns boolean
local slide this
if IsUnitInGroup(slider,GROUP) then
set this=DATA2[GetHandleId(slider)-OFFSET]
call DestroyEffect(.sfx)
if sfx!="" then
set .customsfx=true
set .sfx=AddSpecialEffectTarget(sfx,slider,ATTACH)
else
set .customsfx=false
if .fly then
set .sfx=AddSpecialEffectTarget(AIR,slider,ATTACH)
endif
endif
return true
else
debug call BJDebugMsg(SCOPE_PREFIX+" UnitSetEffect Error: Unit is not sliding.")
return false
endif
endmethod
// Kinematics
// Slide a unit for a specific duration, given the initial velocity
static method unitslidetimed takes unit slider, real angle, real velocity, real time returns slide
// a=Vi-Vf/t
return slide.create(slider,angle,velocity,(velocity-BREAK)/(time*FRAMERATE))
endmethod
// Slide a unit over a specific distance, given the initial velocity
static method unitslidedistance takes unit slider, real angle, real velocity, real distance returns slide
// a=(Vf^2-Vi^2)/(2*d)
return slide.create(slider,angle,velocity,FRAMERATE*(REALBREAK*REALBREAK-velocity*velocity*TIMEOUT)/(2.*distance)))
endmethod
//==Initialize System==\\
private static method onInit takes nothing returns nothing
set UNITBOOL=Condition(function slide.unitfilter)
// Destroy trees in path of sliding units setup
static if TREES then
set RECT=Rect(-RADIUS,-RADIUS,RADIUS,RADIUS)
set TREEBOOL=Condition(function slide.killtrees)
// Don't have DestructableUtils
static if not LIBRARY_DestructableLib then
set TREECHECK=CreateUnit(Player(15),039;hfoo039;, 0.0, 0.0, 0.0)
call PauseUnit(TREECHECK,true)
call ShowUnit(TREECHECK,false)
call UnitAddAbility(TREECHECK,039;Ahrl039;)
call UnitAddAbility(TREECHECK,039;Aloc039;)
endif
endif
// If you don't have TimerUtils
static if not LIBRARY_TimerUtils then
set HT=InitHashtable()
endif
endmethod
endstruct
//==Wrapper Functions==\\
// Create a new instance for a unit, deceleration=0 to use the DECEL global
function UnitStartSlide takes unit slider, real angle, real velocity, real deceleration returns slide
debug if slider==null or not UnitAlive(slider) or IsUnitType(slider,UNIT_TYPE_DEAD)==true then
debug call BJDebugMsg(SCOPE_PREFIX+" Create Error: Invalid argument inputs.")
debug return 0
debug endif
return slide.create(slider,angle,velocity,deceleration)
endfunction
// Retrieve the units current slide information
function UnitGetSlide takes unit slider returns slide
return slide.unitgetslide(slider)
endfunction
// Is the unit currently sliding?
function IsUnitSliding takes unit slider returns boolean
return slide.isunitsliding(slider)
endfunction
// Stop the unit from sliding immediately
function UnitStopSlide takes unit slider returns boolean
return slide.unitstopslide(slider)
endfunction
// Incase you need to set a custom slide effect mid slide
// Use "" to restore automatic sfx updates
function UnitSetSlideEffect takes unit slider, string sfx returns boolean
return slide.unitsetslideeffect(slider,sfx)
endfunction
// Kinematics
// Slide a unit for a specific duration, given the initial velocity
function UnitSlideTimed takes unit slider, real angle, real velocity, real time returns slide
return slide.unitslidetimed(slider,angle,velocity,time)
endfunction
// Slide a unit over a specific distance, given the initial velocity
function UnitSlideDistance takes unit slider, real angle, real velocity, real time returns slide
return slide.unitslidedistance(slider,angle,velocity,time)
endfunction
endlibrary</i>