Some of the most sought after, attempted, and submitted systems in wc3 mapping is a good system that handles smooth triggered unit movement. This includes Jumps, Knockbacks, and Throws. But from all these systems, if you look through the approved resources, not a single one does quite what everyone's looking for.
I don't promise that my system will do what you want (In fact, there are some simple things it can't do), but I use it in almost all of my maps, and if it's good enough for me, I hope the same applies to you.
Limitations:
There are a few important things about this system that make it imperfect.
It isn't configurable to use SetUnitPosition(), because it handles flying units with SetUnitMoveSpeed (Either 0 or GetUnitDefaultMoveSpeed). This means that if other scripts in your map use SetUnitMoveSpeed, this will interfere and may reset those effects.
No support for custom callback functions. If you want units to damage surrounding enemies when they hit the ground, or explode when they kill trees, or conserve momentum by taking into account a unit's size, or bounce off of walls/units/destructables, this script can't help you (yet).
This script uses a constant gravity value. If you want to make a projectile that falls slower than normal units, you can't do that (yet).
This script stores velocities as cartesian components. That means if you want a unit's trajectory to track/home in on a moving object, this isn't for you.
Lastly and most importantly, this script won't do any polynomial math to find where trajectory==ground. Put more simply, you can't use this system to make a thrown object land in a specific place.
Pre-requisites:
The only required library is "IsTerrainWalkable" by Anitarf and Vexorian. I've uploaded the version I have to pastebin in case the original is too difficult to find: http://pastebin.com/0zjeaBc7
In addition to this library, the script is written in vJass and therefore requires JassHelper. (Though I recommend using JNGP to compile)
API:
Constants
boolean USE_MOVESPEED_MODIFIERS
This defines whether or not units should have their movespeed set to 0 and then back to "default" speed (Using GetUnitDefaultMoveSpeed()). If false, units in mid air will be able to fully control themselves. Recommend value true.
real FIDELITY
How often the periodic trigger fires. A value of (1./30.) represents 30 frames per second. I'd recommend (1./30.) but it should look good with any value in [ 1./60. , 1./20. ].
real BOUNCE_COEFFICIENT
How much momentum is retained when a unit bounces. A value of .4 means 40% of the z velocity is retained.
real FRICTION
What percentage of momentum is lost while a unit is sliding. A value of .15 means that 85% of the unit's speed is retained with each slide iteration. A sliding unit is one that is touching the ground.
real GRAVITY
The best way to define this constant is with a value like "...GRAVITY=FIDELITY*R" where R is the z-acceleration of units in game units per second squared. Default is "FIDELITY*41.25".
real MAX_Z_VELOCITY_TO_BOUNCE
This represents a minimum fall-speed for a unit to bounce. For a value of (FIDELITY*-300.), units have to have a negative zVelocity of at least 300 units per second, or they won't bounce.
real MIN_FLY_HEIGHT
This value represents the maximum fly height for which a unit is still considered sliding. Below this number, units are affected by friction. Default value is 5.
real MIN_FOR_KNOCKBACK
This value is the minimum horizontal velocity for a unit to be moving before it will be removed from the stack. Default value is (FIDELITY*30.), which represents 30 units per second.
real MIN_FRICTION_FOR_EFFECT
This is the minimum horizontal velocity a sliding unit can be moving before a "friction" special effect will be spawned. Default value is (FIDELITY*180.) which represents 180 units per second.
real MIN_Z_VELOCITY_TO_BECOME_AIRBORNE
This is the minimum z-velocity of a unit to actually have it's flying height changed. Otherwise, it just slides. Default value is (FIDELITY*150.), which represents 150 units per second.
real DESTRUCTABLE_ENUM_RADIUS
This represents the radius at which destructables are enumerated around units in the stack, before checking to destroy them. Note that this radius is converted to a square, and therefore in reality destructables can be DESTRUCTABLE_ENUM_RADIUS*Sqrt(2) units away by origin, and still be destroyed. Default value is 130.
real MIN_VEL_DESTROY_DESTRUCTABLE
This represents the minimum horizontal velocity a unit can be moving for it to destroy destructables it collides with. The default value of (FIDELITY*240.) represents 240 units per second.
real DESTROY_DESTRUCTABLE_MOMENTUM_CONSERVED
This is the percentage of momentum to conserve if a sliding unit destroys a destructable. Default value is the same as BOUNCE_COEFFICIENT.
real MAX_HEIGHT_DESTROY_DESTRUCTABLE
This is the height below which a unit in the stack is elligible to destroy a destructable. Ideally it should be the average height of your destructables. Default value is 150.
string FRICTION_MODEL
This is the model that spawns when a unit is sliding with a horizontal velocity > MIN_FRICTION_FOR_EFFECT. Default value is the "impale dust target" model.
This is the most important (and in my maps, normally the only) function. Use this to add a vector to any unit. Velocity is given in game units per second. A trajectory of 0. represents a normal slide, while a trajectory of bj_PI/2 represents knocking a unit directly in the air. If a unit is already in the stack, these values are instead added to the existing vector. DO NOT give this function an angle in degrees. It handles radians and won't warn you if you put a large number. (See http://en.wikipedia.org/wiki/Angle#Units if new to highschool level maths)
This function is for overwriting the current vector of a unit in the stack. If the unit is not already in the stack, it behaves exactly like the "add" function.
The script checks each iteration to verify the unit in the stack is still in bj_mapInitialPlayableArea, but in the event that you want to change the rect the script works for, you can use this function to update the rect.
The script:
Knockback3D
Jass:
library Knockback3D initializer i requires IsTerrainWalkable privatestruct knockDat unit u real xOffs real yOffs real zOffs endstruct
globals privateconstantboolean USE_MOVESPEED_MODIFIERS=true//This defines whether or not units should have their movespeed set to 0 and then back to "default" speed (Using GetUnitDefaultMoveSpeed()). If false, units in mid air can still fully control themselves. privateconstantinteger CROW_ID='Arav' privateconstantreal FIDELITY=1./30.//How often the periodic trigger fires. A value of (1./30.) represents 30 frames per second. privateconstantreal BOUNCE_COEFFICIENT=.4//How much momentum is retained when a unit bounces. A value of .4 means 40% of the z velocity is retained. privateconstantreal FRICTION=.15//What percentage of momentum is lost while a unit is sliding. A value of .15 means that 85% of the unit's speed is retained with each slide iteration. privateconstantreal GRAVITY=FIDELITY*41.25//The downward acceleration of all units in the stack. A value of FIDELITY*41.25 means they accelerate downwards by 41.25 game units per second. privateconstantreal MAX_Z_VELOCITY_TO_BOUNCE=FIDELITY*-300.//This represents a minimum fall-speed for a unit to bounce. For a value of (-10.), units have to have a negative zVelocity of at least 10, or they won't bounce. privateconstantreal MIN_FLY_HEIGHT=5.//This is the minimum height a unit can be at to be considered "sliding" (friction is applied to it) privateconstantreal MIN_FOR_KNOCKBACK=FIDELITY*30.//This is the minimum horizontal velocity a unit can be sliding at before it is removed from the stack. privateconstantreal MIN_FRICTION_FOR_EFFECT=FIDELITY*180.//While a sliding unit's horizontal velocity is higher than this number, a "friction" effect is spawned. privateconstantreal MIN_Z_VELOCITY_TO_BECOME_AIRBORNE=FIDELITY*150.//This is the minimum z-velocity of a unit to actually have it's flying height changed. Otherwise, it just slides. privateconstantreal DESTRUCTABLE_ENUM_RADIUS=130.//This is the distance from the center of a sliding unit to a nearby destructable for it to be destroyed. Note that the radius is coverted to a square and therefor the user must consider this value *Sqrt(2). privateconstantreal MIN_VEL_DESTROY_DESTRUCTABLE=FIDELITY*300.//This is the minimum horizontal velocity a unit must have to destroy a destructable. You can set this to a very high number to disable the feature. privateconstantreal DESTROY_DESTRUCTABLE_MOMENTUM_CONSERVED=BOUNCE_COEFFICIENT //This is the percentage of momentum to conserve if a sliding unit destroys a destructable. Default value is the same as BOUNCE_COEFFICIENT privateconstantreal MAX_HEIGHT_DESTROY_DESTRUCTABLE=150.//This is the height below which a unit in the stack is elligible to destroy a destructable. Ideally it should be the average height of your destructables. privateconstantstring FRICTION_MODEL="Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl"//This is the model to spawn when a unit's horizontal velocity > MIN_FRICTION_FOR_EFFECT privateboolean hitDestructable privateeffect fx privateinteger dbIndex=-1 private knockDat array knockDB privatelocation zLoc=Location(0.,0.) privatereal minX privatereal maxX privatereal minY privatereal maxY privaterect rct privatetimer time=CreateTimer() endglobals
privatefunction getZ takesreal x,real y returnsreal callMoveLocation(zLoc,x,y) returnGetLocationZ(zLoc) endfunction
privatefunction d takesnothingreturnsnothing localdestructable des=GetEnumDestructable() ifGetDestructableLife(des)>0.then callKillDestructable(des) set hitDestructable=true endif set des=null endfunction
privatefunction p takesnothingreturnsnothing localboolean newInMap localinteger index=0 localreal flyHeight localreal unitX localreal unitY localreal heightDifference localreal newX localreal newY localreal vel2d local knockDat tempDat loop exitwhen index>dbIndex set tempDat=knockDB[index] set unitX=GetUnitX(tempDat.u) set unitY=GetUnitY(tempDat.u) set newX=unitX+tempDat.xOffs set newY=unitY+tempDat.yOffs set newInMap=newX>minX and newX<maxX and newY>minY and newY<maxY set flyHeight=GetUnitFlyHeight(tempDat.u) set vel2d=(tempDat.xOffs*tempDat.xOffs+tempDat.yOffs*tempDat.yOffs) if flyHeight<MIN_FLY_HEIGHT then if IsTerrainWalkable(newX,newY)and newInMap then callSetUnitX(tempDat.u,unitX+tempDat.xOffs) callSetUnitY(tempDat.u,unitY+tempDat.yOffs) set tempDat.xOffs=tempDat.xOffs*(1.-FRICTION) set tempDat.yOffs=tempDat.yOffs*(1.-FRICTION) staticif USE_MOVESPEED_MODIFIERS then callSetUnitMoveSpeed(tempDat.u,GetUnitDefaultMoveSpeed(tempDat.u)) endif if vel2d>MIN_FRICTION_FOR_EFFECT then set fx=AddSpecialEffect(FRICTION_MODEL,unitX,unitY) callDestroyEffect(fx) endif else set tempDat.xOffs=0 set tempDat.yOffs=0 endif if tempDat.zOffs<MAX_Z_VELOCITY_TO_BOUNCE then set tempDat.zOffs=tempDat.zOffs*-1.*BOUNCE_COEFFICIENT endif if tempDat.zOffs>MIN_Z_VELOCITY_TO_BECOME_AIRBORNE then callSetUnitFlyHeight(tempDat.u,flyHeight+tempDat.zOffs,0) set tempDat.zOffs=tempDat.zOffs-GRAVITY endif elseif newInMap then set tempDat.zOffs=tempDat.zOffs-GRAVITY set heightDifference=getZ(newX,newY)-getZ(unitX,unitY) callSetUnitFlyHeight(tempDat.u,flyHeight+tempDat.zOffs-heightDifference,0) callSetUnitX(tempDat.u,newX) callSetUnitY(tempDat.u,newY) staticif USE_MOVESPEED_MODIFIERS then callSetUnitMoveSpeed(tempDat.u,0) endif endif if vel2d<MIN_FOR_KNOCKBACK and tempDat.zOffs>MAX_Z_VELOCITY_TO_BOUNCE and tempDat.zOffs<-1*MAX_Z_VELOCITY_TO_BOUNCE and flyHeight<MIN_FLY_HEIGHT then set knockDB[index]=knockDB[dbIndex] set dbIndex=dbIndex-1 callSetUnitFlyHeight(tempDat.u,0,0) staticif USE_MOVESPEED_MODIFIERS then callSetUnitMoveSpeed(tempDat.u,GetUnitDefaultMoveSpeed(tempDat.u)) endif call tempDat.destroy() set index=index-1 if dbIndex<0then callPauseTimer(time) endif endif if vel2d>MIN_VEL_DESTROY_DESTRUCTABLE and flyHeight<MAX_HEIGHT_DESTROY_DESTRUCTABLE then set hitDestructable=false callMoveRectTo(rct,newX,newY) callEnumDestructablesInRect(rct,null,function d) if hitDestructable then set tempDat.xOffs=tempDat.xOffs*DESTROY_DESTRUCTABLE_MOMENTUM_CONSERVED set tempDat.yOffs=tempDat.yOffs*DESTROY_DESTRUCTABLE_MOMENTUM_CONSERVED endif endif set index=index+1 endloop endfunction
privatefunction getUnitIndexFromStack takesunit u returnsinteger localinteger index=0 localinteger returner=-1 local knockDat tempDat loop exitwhen index>dbIndex or returner!=-1 set tempDat=knockDB[index] if tempDat.u==u then set returner=index endif set index=index+1 endloop return returner endfunction
publicfunction add takesunit u,real velocity,real angleInRads,real zAngleInRads returnsnothing//make sure setVel matches this! localinteger index=getUnitIndexFromStack(u) local knockDat tempDat localreal instVel=velocity*FIDELITY if index==-1then set tempDat=knockDat.create() set tempDat.u=u set tempDat.xOffs=instVel*Cos(angleInRads)*Cos(zAngleInRads)//Warning! Don't send angles in degrees to these functions if you value your life! set tempDat.yOffs=instVel*Sin(angleInRads)*Cos(zAngleInRads) set tempDat.zOffs=instVel*Sin(zAngleInRads) set dbIndex=dbIndex+1 set knockDB[dbIndex]=tempDat ifUnitAddAbility(tempDat.u,CROW_ID)then callUnitRemoveAbility(tempDat.u,CROW_ID) endif if dbIndex==0then callTimerStart(time,FIDELITY,true,function p) endif else set tempDat=knockDB[index] set tempDat.xOffs=tempDat.xOffs+instVel*Cos(angleInRads)*Cos(zAngleInRads) set tempDat.yOffs=tempDat.yOffs+instVel*Sin(angleInRads)*Cos(zAngleInRads) set tempDat.zOffs=tempDat.zOffs+instVel*Sin(zAngleInRads) endif endfunction
publicfunction setVel takesunit u,real velocity,real angleInRads,real zAngleInRads returnsnothing//make sure this matches add! localinteger index=getUnitIndexFromStack(u) local knockDat tempDat localreal instVel=velocity*FIDELITY if index==-1then set tempDat=knockDat.create() set tempDat.u=u set tempDat.xOffs=instVel*Cos(angleInRads)*Cos(zAngleInRads) set tempDat.yOffs=instVel*Sin(angleInRads)*Cos(zAngleInRads) set tempDat.zOffs=instVel*Sin(zAngleInRads) set dbIndex=dbIndex+1 set knockDB[dbIndex]=tempDat ifUnitAddAbility(tempDat.u,CROW_ID)then callUnitRemoveAbility(tempDat.u,CROW_ID) endif if dbIndex==0then callTimerStart(time,FIDELITY,true,function p) endif else set tempDat=knockDB[index] set tempDat.xOffs=instVel*Cos(angleInRads)*Cos(zAngleInRads) set tempDat.yOffs=instVel*Sin(angleInRads)*Cos(zAngleInRads) set tempDat.zOffs=instVel*Sin(zAngleInRads) endif endfunction
publicfunction updateMapArea takesrect rct returnsnothing set minX=GetRectMinX(rct) set minY=GetRectMinY(rct) set maxX=GetRectMaxX(rct) set maxY=GetRectMaxY(rct) endfunction
privatefunction i takesnothingreturnsnothing set rct=Rect(-1*DESTRUCTABLE_ENUM_RADIUS,-1*DESTRUCTABLE_ENUM_RADIUS,DESTRUCTABLE_ENUM_RADIUS,DESTRUCTABLE_ENUM_RADIUS) set minX=GetRectMinX(bj_mapInitialPlayableArea) set maxX=GetRectMaxX(bj_mapInitialPlayableArea) set minY=GetRectMinY(bj_mapInitialPlayableArea) set maxY=GetRectMaxY(bj_mapInitialPlayableArea) endfunction endlibrary
Example Test Scope:
test
Jass:
scope test initializer i privatefunction c takesnothingreturnsboolean localinteger index=0 localunit u loop exitwhen index>4 set u=CreateUnit(Player(0),'hfoo',-512.+256.*index,0.,90.) callUnitApplyTimedLife(u,'BTLF',5.) call Knockback3D_add(u,GetRandomReal(300.,1000.),bj_PI/2.,GetRandomReal(0.,bj_PI/2.)) set index=index+1 endloop set u=null returnfalse endfunction
2012.06.09 - Replaced the "power" argument with "velocity". Initial vector is now calculated based on FIDELITY, and therefore you can use a velocity value in game units per second. Updated the add API to make sure the client understands which angles it references, and added a comment to remind them the script is for radians. 2012.06.01 #2 - Updated the 'add' and 'setVel' functions to not remove 'Arav' from units which already have it. 2012.06.01 - Initial upload to hive workshop jass submissions section.
Special Thanks:
Anitarf and Vexorian for the original IsTerrainWalkable library.
jim7777 for showing me how to safely add/remove Arav.
-Kobas- for creating a map submission template, which turned out useful for system submissions as well.
Vexorian for developing JassHelper. Without vJass, I almost certainly would not still be scripting for wc3.
The various developers of JNGP including PitzerMike and MindworX. Integrating JassHelper and TESH is a godsend.
Author's Notes:
This script is open source. If you know how to modify the script so it's better, I'd encourage doing so and uploading it!
the link is somewhere around wc3jass but can't open the site as of now...
there's also in wc3c.net.. but someone already updated it (not so sure, or perhaps it's a different lib)
just to be safe in case the unit has the crow id already
oh if you won't mind.. i guess you may make the user have an opportunity to use a dynamic effect (or any string provided in the call) so it won't be limited to the constant provided
the link is somewhere around wc3jass but can't open the site as of now...
there's also in wc3c.net.. but someone already updated it (not so sure, or perhaps it's a different lib)
just to be safe in case the unit has the crow id already
Cool, thanks! I updated it. Never seen that before actually.
Quote:
oh if you won't mind.. i guess you may make the user have an opportunity to use a dynamic effect (or any string provided in the call) so it won't be limited to the constant provided
Hmm I'm not sure that's worth while. Perhaps I'll make a function addEx which allows the user to define a custom friction model. I'm not sure how useful it will be, but if you get another "vote", I'll make it happen.
My GUI Knockback 2D resource uses one trick to determine collision size, though it is unfortunately a bit flawed. It uses a custom inferno ability with 4 levels corresponding to collision sizes 16, 32, 48 and 64, and casts the ability on the queried point. If the point is pathable for that collision size, IssuePointOrderById returns true, if it's not, it returns false.
A better approach, and one I intend to use in the next update, is to use those 4 units combined with SetUnitPosition.
__________________
How to post your triggers on the Hive Workshop. JPAG - Bettering the cause of readable source code.
The issue isn't that I don't know how, it's that I just never had to make it. This script is just a slightly modified version of one I'm using all the time myself.
Is callback functions really something people want?
The way I understand it, 16, 32, 48 and 64 are the only necessary values because wc3 pathing map uses cells of 16x16 units, right? So therefore this system would work with units of a collision size 0.-64.
...but that uses object data :( Come on man I've never had a problem with IsTerrainWalkable.
What if someone were to make IsTerrainWalkable which returns a real instead of a boolean, which determines the pathing availability of the target point.
Like "if IsTerrainWalkable(x,y)<16. then" ...
But then you come to the problem that there's no native to get a unit's collision size, so I don't see how either method really helps avoid the issue.
But then you come to the problem that there's no native to get a unit's collision size, so I don't see how either method really helps avoid the issue.
There is a function to get it in the JASS section, but that shouldn't be used for these purposes ; \.
IsTerrainWalkable is not 100% accurate ;\. If you look at the block style comments in the primary function of Is Pathable or w/e the heck it is in the JASS section, you'll come to understand why. They explain it very clearly and include diagrams ; ).
IsTerrainWalkable is not 100% accurate ;\. If you look at the block style comments in the primary function of Is Pathable or w/e the heck it is in the JASS section, you'll come to understand why. They explain it very clearly and include diagrams ; ).
I checked it out. That looks really nice actually, and quite useful...
But it still uses LUA to generate object data ;(
What did you think of my idea?
Quote:
Like "if IsTerrainWalkable(x,y)<16. then" ...
Even if that ignores units/structures changing pathing, it could be worthwhile since it does't need any object data.
Also you didn't answer my question about collision sizing. You said that's not necessary here, but actually how can you tell the system how big a space it needs if you don't know how big the unit (in the Knockback3D stack) is?
public function add takes unit u, real power, real direction, real trajectory returns nothing
"real power" what power? Needs defining. For example, what does the value 2 represent as opposed to 1.
"real direction" is this the horizontal angle? Usually this keyword is "angle" as defined by Blizzard. More intuitive to stick to tried and true keywords.
"real trajectory" this one I didn't get until I looked at your description. Just name it zAngle and be done with it.
Make sure the user only passes radians by adding warnings.
__________________
How to post your triggers on the Hive Workshop. JPAG - Bettering the cause of readable source code.