The goal here was to make an all encompassing, end all be all Slide/Knockback System. I think I have created that, minus any glaring slights on my behalf.
The main issue I had with the extended knockback system is that you can't have multiple concurrent slides on a single unit - I alleviate this issue by calculating the vector components of the current slide and the updated slide, which allows me to implement all slides created into a single instance (which also increases system performance). Also, a few of the scripting techniques in the previous are a little archaic (ex. no static ifs), that, along with its heavy external system requirements, makes for a less then desirable system.
A newer system is Bribe's well received Knockback 2D, which, being written in GUI, is inherently slower, and doesn't/cannot allow for the same amount of functionality found here.
We will need to implement the slideStruct module where we want to use it (see Demo).
Wrappers for the struct's methods are inbound, in the not to distant future (GUI friendly).
Test map in route also....a Demo trigger is included in the interim.
Shared methods include: OnLoop (on every timer interval), OnCollision (on unit collision), OnCliff (on cliff collision), OnRest (on slide end). All methods take "slide s", except for OnCollision, which takes "slide s, unit t", and return nothing.
The syntax may need to be tweaked a bit, but I think it fits pretty good.
Critiques and other thoughts welcome. I humbly submit for consideration.
Requirements:
UnitAlive native
A Dummy caster
A Dummy tree destruction ability (as seen in Knockback 2D)
Script:
Jass:
//=========================\\ //==System made by emjlr3==\\ //==Version 1.c, 02/04/12==\\ //=========================\\ library SlideSystem
//== CONFIGURATION ==\\ globals // bounce off terrain? privateconstantboolean BOUNCE =true // collide with other units? privateconstantboolean COLL =true // allow movement during slides? privateconstantboolean MOVE =true // destroy trees in slide path? privateconstantboolean TREES =true // tree destruction ability rawcode privateconstantinteger ABIL ='tree' // rawcode of your map's caster dummy privateconstantinteger DUMMY ='dumy' // conserved slide velocity/TIMEOUT privateconstantreal FRICT =.96 // momentum conserved upon collision, what's loss is transfered to the colliding unit, if it exists privateconstantreal LOSS =.75 // collision radius privateconstantreal RADIUS =128. // minimum slide velocity privateconstantreal STOP =3.0 // periodic timer interval privateconstantreal TIMEOUT =.031250000
// we will configure this global below privatestringarray SFX endglobals privatefunction setupSlideSFX takesnothingreturnsnothing // the system assumes the array placements are unchanged // i speculate as to terrain types using known means; this isn't fool proof // if you are interested in snow/ice/lava slide sfx, let me know
// ground slide sfx set SFX[1]="war3mapImported\\LandSlide.mdx" // water slide sfx set SFX[2]="war3mapImported\\SeaSlide.mdx" // air slide sfx set SFX[3]="war3mapImported\\AirSlide.mdx" // slide collision sfx set SFX[4]="Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl" endfunction
//== NO TOUCHING PAST THIS POINT ==\\ native UnitAlive takesunit u returnsboolean globals publicgroup SLIDING =CreateGroup() privategroup ENUM =CreateGroup() privatehashtable HT =InitHashtable() privateconstantinteger LVL =IMaxBJ(4,R2I(RADIUS/32.)) privatelocation LOC =Location(0.,0.) privatelocation LOC2 =Location(0.,0.) privatereal RSTOP = STOP*TIMEOUT privatetimer T =CreateTimer() privateunit DUM =null endglobals
struct slide thistype next thistype prev
unit u player owner real xP real yP real xV real yV real ang real dist=0.// total distance slid real time=0.// total duration slid effect sfx group g integer gt=0// ground type (numbers correspond to SFX type) boolean remove=false
method destroy takesnothingreturnsnothing // update linked list set.prev.next=.next set.next.prev=.prev // clean up callGroupRemoveUnit(SLIDING,.u) callDestroyEffect(.sfx) callSetUnitPathing(.u,true) callDestroyGroup(.g) call.deallocate() endmethod
method getDeflection takesunit t returnsboolean localthistype s localreal a localreal d localreal l localreal r localreal x localreal dx localreal y localreal dy localreal z
callMoveLocation(LOC,.xP,.yP) set z=GetLocationZ(LOC)+GetUnitFlyHeight(.u)// slider zP set x=GetUnitX(t) set y=GetUnitY(t) callMoveLocation(LOC2,x,y) // collision detected ifRAbsBJ(z-(GetLocationZ(LOC2)+GetUnitFlyHeight(t)))<=RADIUS*2.then // currently we can only collide once/target callGroupAddUnit(.g,t)
// account for momentum loss set l=SquareRoot(.xV*.xV+.yV*.yV) set r=l*LOSS set l=(l-r)/TIMEOUT
//calculate angular relationship set dx=x-.xP set dy=y-.yP set a=Atan2(dy,dx)
// create collision effect between two units set r=SquareRoot(dx*dx+dy*dy)/2. callDestroyEffect(AddSpecialEffect(SFX[4],.xP+r*Cos(a),.yP+r*Sin(a)))
// slide target ifIsUnitType(t,UNIT_TYPE_STRUCTURE)==falsethen set s=thistype.createSlide(t,l*Cos(a),l*Sin(a)) callGroupAddUnit(s.g,.u) returntrue endif endif
returnfalse endmethod method findCliffCollision takesnothingreturnsboolean localrealarray r
callMoveLocation(LOC,.xP,.yP) // height at or above which the slider will collide with the given terrain loc set r[1]=GetLocationZ(LOC)+GetUnitFlyHeight(.u)-(RADIUS/2.) set r[2]=.ang-.7854// bj_PI/4. set r[3]=.ang+.7854 // loop in a quarter circle loop exitwhen r[2]>r[3]// stop after the quarter circle set r[4]=.xP+(RADIUS/2.)*Cos(r[2]) set r[5]=.yP+(RADIUS/2.)*Sin(r[2]) callMoveLocation(LOC,r[4],r[5]) set r[6]=GetLocationZ(LOC) // collision found, and better than the last if r[6]>=r[1]and r[6]>r[9]then // update dynamic array members set r[7]=r[4]// x set r[8]=r[5]// y set r[9]=r[6]// z endif // continue to loop, bj_PI/16. set r[2]=r[2]+.1963 endloop callMoveLocation(LOC,.xP,.yP) // we have a cliff collision if r[9]>0.and r[9]-GetLocationZ(LOC)>=15.then // create collision effect where we strike callDestroyEffect(AddSpecialEffect(SFX[4],r[7],r[8])) // update velocity vectors staticif BOUNCE then set.ang=2.*Atan2(r[8]-.yP,r[7]-.xP)+bj_PI-.ang set r[1]=SquareRoot(.xV*.xV+.yV*.yV)*LOSS set.xV=r[1]*Cos(.ang) set.yV=r[1]*Sin(.ang) else set.xV=0. set.yV=0. endif
returntrue endif
returnfalse endmethod staticmethod getSlideSFX takesthistypethis,real x,real y returnsnothing localstring s=""
ifIsUnitType(.u,UNIT_TYPE_FLYING)==trueandGetUnitFlyHeight(.u)>0.0then // air if.gt!=3then set.gt=3 set s=SFX[3] endif else // land stairs don't have any pathing... ifIsTerrainPathable(x,y,PATHING_TYPE_FLOATABILITY)ornotIsTerrainPathable(x,y,PATHING_TYPE_ANY)then if.gt!=1then set.gt=1 set s=SFX[1] endif // sea elseifnotIsTerrainPathable(x,y,PATHING_TYPE_WALKABILITY)then if.gt!=2then set.gt=2 set s=SFX[2] endif endif endif // new sfx to be added if s!=""then // old sfx removal if.sfx!=nullthen callDestroyEffect(.sfx) endif // new sfx creation set.sfx=AddSpecialEffectTarget(s,.u,"origin") endif endmethod method movement takesnothingreturnsnothing localreal d localreal fh
// update positioning set fh=GetUnitFlyHeight(.u) set.xP=GetUnitX(.u)+.xV set.yP=GetUnitY(.u)+.yV // allow movement staticif MOVE then callSetUnitX(.u,.xP) callSetUnitY(.u,.yP) else // don't allow movement callSetUnitPosition(.u,.xP,.yP) endif
// various updateable values set.time=.time+TIMEOUT set d=SquareRoot(.xV*.xV+.yV*.yV)// distance traveled set.dist=.dist+d
// update velocity if fh<=0.then set.xV=.xV*FRICT set.yV=.yV*FRICT endif
// tree destruction // kudos to Bribe for this neat trick staticif TREES then if fh<=200.then callSetUnitAbilityLevel(DUM,ABIL,LVL) callIssuePointOrder(DUM,"flamestrike",.xP,.yP) endif endif
// check for slide end based on velocity if d<=RSTOP then set.remove=true endif endmethod
implement slideStruct
//== FUNCTIONALITY METHODS ==\\ // retrieve the units current slide information staticmethod unitGetSlide takesunit u returnsthistype ifIsUnitInGroup(u,SLIDING)then returnLoadInteger(HT,GetHandleId(u),0) else debugcallBJDebugMsg(SCOPE_PREFIX+" unitGetSlide Error: "+GetUnitName(u)+" is not sliding.") return0 endif endmethod // is the unit currently sliding? staticmethod isUnitSliding takesunit u returnsboolean returnIsUnitInGroup(u,SLIDING) endmethod // stop the unit from sliding staticmethod unitStopSlide takesunit u returnsboolean localthistypethis ifIsUnitInGroup(u,SLIDING)then setthis=LoadInteger(HT,GetHandleId(u),0) set.remove=true returntrue else debugcallBJDebugMsg(SCOPE_PREFIX+" unitStopSlide Error: "+GetUnitName(u)+" is not sliding.") returnfalse endif endmethod // set custom slide velocity staticmethod setVelocity takesthistypethis,real x,real y returnsnothing set.xV=x*TIMEOUT set.yV=y*TIMEOUT endmethod // add to current slide velocity staticmethod addVelocity takesthistypethis,real x,real y returnsnothing set.xV=x*TIMEOUT+.xV set.yV=y*TIMEOUT+.yV endmethod
//== INITIALIZATION ==\\ staticmethod onInit takesnothingreturnsnothing // this is the dummy unit used for tree destruction set DUM=CreateUnit(Player(15),DUMMY,0.,0.,0.) callUnitAddAbility(DUM,ABIL) callUnitAddAbility(DUM,'Aloc') callSetUnitPathing(DUM,false) callSetUnitInvulnerable(DUM,true) callPauseUnit(DUM,false)
// stores our user defined SFX strings call setupSlideSFX()
module slideStruct staticmethod iterate takesnothingreturnsnothing local slide s=slide(0).next localunit t
loop exitwhen s==0
// check for slide end ifnot UnitAlive(s.u)or s.removethen staticifthistype.onRest.existsthen callthistype.onRest(s) endif // destroy the slide struct call s.destroy() else // update positioning call s.movement() // cliff detection if s.findCliffCollision()then staticifthistype.onCliff.existsthen callthistype.onCliff(s) endif endif // unit collision detection staticif COLL then callGroupEnumUnitsInRange(ENUM,s.xP,s.yP,RADIUS,null) callGroupRemoveUnit(ENUM,s.u) loop set t=FirstOfGroup(ENUM) exitwhen t==null callGroupRemoveUnit(ENUM,t)
ifnotIsUnitInGroup(t,s.g)and UnitAlive(t)then if s.getDeflection(t)then // unit t was collided with callGroupAddUnit(s.g,t) staticifthistype.onCollision.existsthen callthistype.onCollision(s,t) endif endif endif endloop endif
// struct list empty ifFirstOfGroup(SLIDING)==nullthen callPauseTimer(T) endif endmethod
//== CREATION ==\\ staticmethod createSlide takesunit u,real x,real y returns slide local slide s
// struct list empty ifFirstOfGroup(SLIDING)==nullthen callTimerStart(T,TIMEOUT,true,functionthistype.iterate) endif
ifIsUnitInGroup(u,SLIDING)then // had previously been sliding set s=LoadInteger(HT,GetHandleId(u),0) else // hadn't been previously sliding set s=slide.create() // we can access our slide struct from the sliding unit callSaveInteger(HT,GetHandleId(u),0,s)
set s.u=u // removes pathing glitches callSetUnitPathing(u,false) set s.owner=GetOwningPlayer(u) // so we know this unit is sliding callGroupAddUnit(SLIDING,u) // establish our initial slide sfx call slide.getSlideSFX(s,x,y) // collision group set s.g=CreateGroup()
// update linked list set slide(0).next.prev=s set s.next=slide(0).next set slide(0).next=s set s.prev=slide(0) endif
// update position and velocities set s.xV=s.xV+x*TIMEOUT set s.yV=s.yV+y*TIMEOUT set s.xP=GetUnitX(u) set s.yP=GetUnitY(u) set s.ang=Atan2((s.yP+s.yV)-s.yP,(s.xP+s.xV)-s.xP)
set face=face*bj_DEGTORAD set r=GetRandomReal(-750.,750.) callthistype.createSlide(dum,r*Cos(face),r*Sin(face))
set dum=null endmethod
privatestaticmethod onEsc takesnothingreturnsnothing if b then callPauseTimer(t) else callTimerStart(t,0.25,true,functionthistype.onExpire) endif set b=not b endmethod
v1.c - Removed GroupUtils and AutoIndex requirements, now uses a Hashtable for data storage to units
v1.b - Updated to use modules (removes bloat code), added collision effects
v1.a - Rewrote from scratch, changed system requirements, updated cliff detection algorithm, now uses straight vector movement calcs., changed tree destruction method (Thnx Bribe), now can only collide once/unit/slidevent, allows momentum conservation on collision, still uses interfaces :(
So that we can keep everyone updated on both forums ;D. Changed a little bit of the post to make you read it again ; p (velocity based on unit facing rather than set unit.x.velocity).
Quote:
Good idea, but this can undergo a lot of optimization. Whenever you deal with periodic stuff, you need to optimize the hell out of it ; ).
TimerQueue for rehit
This should have like 0 groups, be better on lists.
A unit's velocity should just be set (set unit.x.velocity = -5; set unit.y.velocity = -3). Actually, velocity should just be based on facing, so it'd just be set unit.velocity = -5 or w/e.
This way people can control exactly how they want to do it. You should split this into a myriad of resources... all of it in one resource is just a huge jumbled mess. The most basic should be the setting unit velocity. From there, users can make it so that a unit's velocity gets set when they go on a certain terrain or when a knock back occurs or w/e.
From there, you can go up a level with things like rehit and all of these other options you have. Having them all together is just a terrible idea. It just limits the usability of the resource and makes people have to write identical code to do nearly identical things >.>. This is why modularity is just so important.
It looks like you put a lot of work into this lib, but you should just scrap it and start over.
One interesting thing is that if you just have a general sliding lib, you could run like anything off of it from projectiles to sliding on terrain in maze maps to knockback spells to w/e.
I suggest adding list of the wrappers (maybe at the top of the code or on a separate trigger), and add spaces in between to make them easier to read...
so that user's won't really need to look at the code, and to prevent accidental errors, especially since those wrappers are aimed at GUI users...
something like this
Jass:
/*
==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
Retrieve the units current slide information
->function UnitGetSlide takes unit slider returns slide
Is the unit currently sliding?
->function IsUnitSliding takes unit slider returns boolean
Stop the unit from sliding immediately
->function UnitStopSlide takes unit slider returns boolean
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
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
Slide a unit over a specific distance, given the initial velocity
->function UnitSlideDistance takes unit slider, real angle, real velocity, real distance returns slide
Remove a unit from sliders collision group instantly
->function RemoveTargetFromSlide takes slide d, unit target returns boolean
Remove a unit from sliders collision group after REHITTIME duration
->function RemoveTargetFromSlideTimed takes slide d, unit target returns boolean
*/
Just one more thing, not sure if some of those returns are really needed...
You don't really need those static ifs in the onInit.
onInit runs only once, so static ifs are redundant.
If you use normal ifs, you'd decrease the file size :)
Using Static-if's will actually decrease map file size because it crops out the unwanted blocks during compilation instead of leaving everything in the war3map.j file.
__________________
How to post your triggers on the Hive Workshop. JPAG - Bettering the cause of readable source code.
Using Static-if's will actually decrease map file size because it crops out the unwanted blocks during compilation instead of leaving everything in the war3map.j file.
Hmm, last time i checked the code which is not used, because of a false evaluation of a static if is kept as comment lines instead of code lines. (for jasshelper at least, it won't probably the case with the luck parser :p )
EDIT : Not sure and not tested but i think i've read Vexorian said that Zinc doesn't keep comments (maybe in Zinc documentation)
This system is actually VERY useful ^^
I have this hero call "Cosmic Netherbeast" (coming soon in TDA v1.08)
He's also known as "The King of All Knockbacks"
His spells kept bugging (units moving like crazy when knocked twice with different angles)
This system was the solution!
Thanks man!
Btw, can I edit this system a bit to fit my map?
There are a few things that I want to change to increase performance and reduce
handle use (Nestharus taught me how to use linked lists ^^)
an update in the not to distant future will probably include such a change, as I have proven to myself that they are indeed faster than stacks
if you want to wait, that is - in the mean time, the alternative is inexplicably slower...
Nah, I'll just fix my current knockback system to completely Overwrite knockbacks
for now, and if an update ever comes up, I'll implement this system for enhanced
awesomeness ^^
GetHandleId(x) - 0x100000 for use in arrays is potentially hazardous. You would need fewer than 8191 handles in the map in order for this to not return a bad value. To compensate, Table indexing or Unit Indexing would be viable alternatives.
camelCased API is JASS convention. I can understand not camelcasing something obvious like "unitid", but some of these names are pretty long and could be clearer to read.
.execute() is slower than .evaluate(), is there a reason why you set it up like that?
I advise a default 0.03125 timeout instead of a 0.5, 1) as it holds the best ratio of realism/efficiency in-game
You could also mention that this system, by default, needs some custom imported artwork to work properly.
__________________
How to post your triggers on the Hive Workshop. JPAG - Bettering the cause of readable source code.
GetHandleId(x) - 0x100000 for use in arrays is potentially hazardous. You would need fewer than 8191 handles in the map in order for this to not return a bad value. To compensate, Table indexing or Unit Indexing would be viable alternatives.
out of principal I refuse to limit this system by employing one of the many indexers available, thereby requiring the user to implement one of them - and which one would I use - they all work just fine and dandy, which would each user prefer? why not just use extended arrays and remove the problem?
Quote:
camelCased API is JASS convention. I can understand not camelcasing something obvious like "unitid", but some of these names are pretty long and could be clearer to read.
never heard of it until now, will update
Quote:
.execute() is slower than .evaluate(), is there a reason why you set it up like that?
only because thats how I had seen it done - if .evaluate works just the same, then I can just as easily switch it over
Quote:
I advise a default 0.03125 timeout instead of a 0.5, 1) as it holds the best ratio of realism/efficiency in-game
.05 is more efficient and plenty for slide type spells with short movements - I don't see that being a big deal?
Quote:
You could also mention that this system, by default, needs some custom imported artwork to work properly.
while this is not entirely true, its worth mentioning the slide effects add to the whole package for aesthetics sake
thereby requiring the user to implement one of them
You have options to avoid that -
1) make it index based on hashtable (faster than any vJass big array)
2) make Table or a unit indexer an optional requirement
3) A combination of 1) and 2) using static ifs
__________________
How to post your triggers on the Hive Workshop. JPAG - Bettering the cause of readable source code.