| 09-18-2009, 10:20 PM | #1 |
Hmnn , so it is 'Hydra' from 'diablo 2'. Anyway, you summon this fire hydra which is like 3 towers and they spit fireballs on units, the units can dodge, yadda yadda yadda. I am in the process of porting spells to vJass, xe and patch 1.24b, this is the first victim. Requires: xe, xecollider, xedamage TimerUtils Recommended: BoundSentinel code:library Hydra initializer init requires TimerUtils, xebasic, xecollider, xedamage /************************************************************************** Hydra 2.1: ---------- Requires: * A vJass compiler (i.e. jasshelper) * TimerUtils, xebasic, xecollider and xedamage * A point-targeteable base ability, update SPELL_ID to its rawcode * A unit for the head, update HEAD_UNIT_ID to its rawcode Hint: The Area effect art field in the base spell determines the missile model used by the bolts. More stuff at http;//www.wc3c.net/vexorian **************************************************************************/ //================================================================ // Configuration: // globals private constant integer SPELL_ID = 'A006' private constant integer HEAD_UNIT_ID = 'o001' // Attack animation duration: private constant real HEAD_ATTACK_ANIMATION_POINT = 0.5 // Time to wait between spawning each head: private constant real HEAD_SPAWN_INTERVAL = 0.23 // polar projection angle for the first head private constant real FIRST_HEAD_ANGLE = 90.0 // The initial facing angle private constant real FIRST_FACING_ANGLE = 225.0 // The maximum number of heads a single instance of any level private constant integer MAX_HEADS = 3 endglobals // Note: the index integer argument to some functions specifies // the number ( 0-based) of the head . //====================================================================================================== // Balance stuff: // --------------- // headCount : The number of heads to summon // bodyRadius : The distance between the heads and the casting point. // headDuration : The duration of the spell. // detectRange : The minimum distance between the heads and a unit for the heads to aim against it. // // attackCooldown : Each head must wait at least this time before shooting a new missile // attackPeriod : Minimum delay between attacks. // multiMissile : If true, each hydra head will be able to have more than one simultaneous missile. // // missileRange : The maximum distance a missile can reach. // missileSpeed : Movement speed of the missile // missileCollision: The minimum distance between a missile and a unit for it to explode. // damage : The damage to perform on succesfully hit units. // private function headCount takes integer level returns integer return 3 + 0*level endfunction private function bodyRadius takes integer level returns real return 50.0 + 0*level endfunction private function headDuration takes integer level returns real return 20.0 + 4.0 * level endfunction private function detectRange takes integer level returns real return 900.0 + 100*level endfunction private function attackCooldown takes integer level returns real return 1.0 - level * 0.0 endfunction private function attackPeriod takes integer level returns real return 0.15 + level * 0.0 endfunction private function multiMissile takes integer level returns boolean return true endfunction private function missileRange takes integer level returns real return 2000.0 + 0*level endfunction private function missileSpeed takes integer level returns real return 750.0 + 0*level endfunction private function missileCollision takes integer level returns real return 35.0 + 0*level endfunction private function damage takes integer level returns real return 20.0 + 10.0 * level endfunction //================================================================================== // Target options, these functions specify valid targets for various functions // using a xedamage object, for more info about how to change the options of a // xedamage objevt, read xedamage's readme file.... // // * configDetection: Units that give a factor different to 0.0 will be detectable // by heads and the heads will aim towards them. // * configExplosion: Units that give a factor different to 0.0 will get hit // by the bolt. // * configDamage : After a unit is hit, this xedamage object is used to // harm the hit unit. // private function configDetection takes xedamage d, integer level returns nothing set d.damageAllies = false set d.damageNeutral = false set d.damageEnemies = true set d.visibleOnly = true endfunction private function configExplosion takes xedamage d, integer level returns nothing set d.damageAllies = false set d.damageNeutral = true set d.damageEnemies = true set d.exception = UNIT_TYPE_FLYING endfunction private function configDamage takes xedamage d, integer level returns nothing set d.dtype = DAMAGE_TYPE_FIRE set d.atype = ATTACK_TYPE_NORMAL call d.factor(UNIT_TYPE_STRUCTURE, 0.33) // does 1/3 damage to buildings. endfunction //====================================================================== // Eye candy: // ----------- // headAnimationSuffix : Suffix for the animations of the head. // headScale : The size of each head // // missileHeight : Flying height for the missile. // missileScale : The model scale for the missile // private function headAnimationSuffix takes integer index, integer level returns string if (index == 0) then return "first" elseif (index == 1) then return "second" else return "third" endif endfunction private function headScale takes integer index, integer level returns real return 1.0 + 0*level endfunction private function missileHeight takes integer index, integer level returns integer if (index==0) then return 65 endif return 55 endfunction private function missileScale takes integer level returns real return 0.5 + 0*level endfunction //============================================================================================= // Code: // /// data struct /// ----------- /// Keeps spell data and objects for each instance. /// private struct data integer heads = 0 real x real y integer level unit owner timer mainTimer timer shootTimer unit array head [MAX_HEADS] real array lastShoot[MAX_HEADS] unit array target [MAX_HEADS] boolean array withMissile[MAX_HEADS] integer currentAttackHead = -1 real currentAttackAngle xedamage targetOptions xedamage damageOptions xedamage explodeOptions static method create takes unit owner, real x, real y, integer level returns data local data d=data.allocate() set d.targetOptions = xedamage.create() set d.damageOptions = xedamage.create() set d.explodeOptions = xedamage.create() call configDetection(d.targetOptions, level) call configDamage(d.damageOptions, level) call configExplosion(d.explodeOptions, level) set d.x = x set d.y = y set d.level = level set d.owner = owner set d.mainTimer = NewTimer() set d.shootTimer = NewTimer() call SetTimerData(d.shootTimer, integer(d) ) call SetTimerData(d.mainTimer, integer(d) ) return (d) endmethod method onDestroy takes nothing returns nothing local integer i=this.heads -1 call this.damageOptions.destroy() call this.targetOptions.destroy() call this.explodeOptions.destroy() loop exitwhen (i<0) call KillUnit(this.head[i]) set this.head[i]=null set i=i-1 endloop call ReleaseTimer(this.mainTimer) call ReleaseTimer(this.shootTimer) endmethod //: Thuis whole ref count works so that if the timer expires before // one of the fireballs collides, it would not destroy the data // too soon, instead, it will wait till all the missiles are out. integer refCount = 0 method decreaseRefCount takes nothing returns nothing set this.refCount = this.refCount - 1 debug if (this.refCount < 0 ) then debug call BJDebugMsg(SCOPE_PREFIX+" error!, refcount too low!") debug endif if(this.refCount <= 0) then call this.destroy() endif endmethod static method staticDecreaseRefCount takes nothing returns nothing call data( GetTimerData(GetExpiredTimer()) ).decreaseRefCount() endmethod endstruct //: missile struct // --------------- // This xecollider takes care of all our avoidable projectile needs. // private struct missile extends xecollider delegate data parent integer headId method onUnitHit takes unit target returns nothing // 1. Can we go boom on this unit? then boom and damage the unit. if( .explodeOptions.allowedTarget(this.parent.owner, target) ) then call .damageOptions.damageTarget( this.parent.owner, target, damage(.level) ) call this.terminate() endif endmethod method onDestroy takes nothing returns nothing set this.withMissile[this.headId] = false call this.decreaseRefCount() endmethod endstruct //: Shooting // --------- globals // private code attemptShoot_code //darn function call cycles!!! endglobals private function shootNow takes nothing returns nothing local timer t = GetExpiredTimer() local data dat = data(GetTimerData(t)) local integer level = dat.level local unit head = dat.head[ dat.currentAttackHead ] //1. Create and tweak the missile local missile m = missile.create( GetUnitX(head), GetUnitY(head), dat.currentAttackAngle ) set dat.refCount = dat.refCount + 1 set m.parent = dat set m.headId = dat.currentAttackHead set m.teamcolor = GetPlayerColor( GetOwningPlayer(dat.owner) ) set m.fxpath = GetAbilityEffectById( SPELL_ID, EFFECT_TYPE_AREA_EFFECT, 0) set m.speed = missileSpeed(level) set m.expirationTime = missileRange(level) / m.speed set m.z = missileHeight( dat.currentAttackHead, level) set m.scale = missileScale(level) set m.collisionSize = missileCollision(level) // 2. return to the shooting loop call TimerStart( t, RMaxBJ( 0, attackPeriod(level) - HEAD_ATTACK_ANIMATION_POINT), false, attemptShoot_code) set head=null set t=null endfunction //: Target detection // --------- private function validDetectTarget takes data dat, unit u returns boolean if (u!=null) and (GetWidgetLife(u)>=0.405) and IsUnitInRangeXY(u, dat.x, dat.y, detectRange(dat.level) ) then return (dat.targetOptions.allowedTarget(dat.owner, u)) endif return false endfunction // globals used in the target selection process... globals private group forbiddenTargets=CreateGroup() private group enumGroup = CreateGroup() private data enumInstance private unit bestNewTarget // /Hold the closest new target private real bestNewDist // \ private unit bestTarget // / Hold the closest target that is private real bestDist // \ not necessarily new endglobals private function detectEnum takes nothing returns boolean local data dat = enumInstance local unit u = GetFilterUnit() local real dis //1. Is the unit a valid target? if validDetectTarget(dat, u) then //2. Qualifies for best distance for a target? set dis = Pow( GetUnitX(u)-dat.x,2)+Pow( GetUnitY(u)-dat.y,2) if ( (bestTarget==null) or (bestDist > dis) ) then set bestTarget = u set bestDist = dis endif //3. Qualifies for best distance for a new target? if ( not IsUnitInGroup(u,forbiddenTargets) ) then if ( (bestNewTarget==null) or (bestNewDist > dis) ) then set bestNewTarget = u set bestNewDist = dis endif endif endif set u=null return false endfunction private function getTarget takes data dat returns unit local integer i = 0 local integer j call GroupClear(forbiddenTargets) call GroupClear(enumGroup) //1. add forbidden targets to the unit group. set i = dat.heads - 1 loop exitwhen (i<0) call GroupAddUnit(forbiddenTargets, dat.target[i]) set i=i-1 endloop //2. pick a target with a enum. set enumInstance = dat set bestTarget = null set bestNewTarget = null call GroupEnumUnitsInRange( enumGroup, dat.x, dat.y , detectRange(dat.level)+XE_MAX_COLLISION_SIZE, Condition(function detectEnum) ) //3. If it is not possible to acquire a new target, at least attack someone another head is already attacking. if( bestNewTarget == null) then return bestTarget else return bestNewTarget endif endfunction //: Shooting process: //--------------------- // private function attemptShoot takes nothing returns nothing local timer t = GetExpiredTimer() local data dat = data( GetTimerData(t) ) local real ang local real cool = attackCooldown(dat.level) local real ctime = TimerGetElapsed(dat.mainTimer) local integer i = -1 local integer j = 0 local unit picked //1. Pick a good head to shoot the next missile loop exitwhen (j>=dat.heads) if ( not dat.withMissile[j] ) and (dat.lastShoot[j] + cool <= ctime) then if( (i==-1) or (dat.lastShoot[i] > dat.lastShoot[j] ) ) then set i=j endif endif set j=j+1 endloop if( i==-1) then //all busy, lame call TimerStart(t, attackPeriod(dat.level), false, function attemptShoot) else // 2. Pick a good target for it. (Try to shoot the last target, if possible) set picked = dat.target[i] if( not validDetectTarget(dat, picked) ) then set picked = getTarget(dat) endif if( picked == null) then call TimerStart(t, attackPeriod(dat.level), false, function attemptShoot) else // 3. Begin shooting process (animation), start timer. set ang = Atan2( GetUnitY(picked) - dat.y, GetUnitX(picked) - dat.x) if ( not multiMissile(dat.level) ) then set dat.withMissile[i] = true endif set dat.lastShoot[i] = ctime set dat.target[i] = picked set dat.currentAttackHead = i set dat.currentAttackAngle = ang call SetUnitFacing(dat.head[i], ang*bj_RADTODEG) call SetUnitAnimation(dat.head[i], "attack "+headAnimationSuffix(i, dat.level) ) call TimerStart(t, HEAD_ATTACK_ANIMATION_POINT, false, function shootNow) endif endif set picked=null set t=null endfunction //: Head creation // ----- private function spawnHead takes nothing returns nothing // 1. create head local data dat = data( GetTimerData(GetExpiredTimer()) ) local integer level = dat.level local integer id = dat.heads local unit head local real ang = FIRST_HEAD_ANGLE*bj_DEGTORAD + id * ( (2* bj_PI) / headCount(level) ) local real rad = bodyRadius(level) local real ox = rad*Cos(ang) local real oy = rad*Sin(ang) local string suf = headAnimationSuffix(id, level) set head = CreateUnit( GetOwningPlayer(dat.owner), HEAD_UNIT_ID , dat.x+ox, dat.y+oy, FIRST_FACING_ANGLE) call UnitAddAbility(head, 'Aloc') set dat.withMissile[id] = false set dat.head [id] = head set dat.target[id] = null set dat.lastShoot[id] = - attackCooldown(level) call SetUnitAnimation(head,"birth "+suf) call QueueUnitAnimation(head,"stand "+suf) // 2. If there are enough heads, continue to shooting process // else continue spawning heads. set dat.heads = id + 1 if( dat.heads < headCount(level) ) then call TimerStart( GetExpiredTimer(), HEAD_SPAWN_INTERVAL, false, function spawnHead) else call TimerStart( dat.shootTimer, attackPeriod(level), false, function attemptShoot ) call TimerStart( GetExpiredTimer(), headDuration(level), false, function data.staticDecreaseRefCount ) endif set head = null endfunction //: triggers //---------- private function onSpellEffect takes nothing returns nothing local real x = GetSpellTargetX() //this, is awesome local real y = GetSpellTargetY() local integer level = GetUnitAbilityLevel(GetTriggerUnit(), SPELL_ID) local data dat = data.create( GetTriggerUnit(), x,y , level) set dat.refCount = 1 call TimerStart( dat.mainTimer, 0.0, false, function spawnHead) endfunction private function spellIdMatch takes nothing returns boolean return (SPELL_ID == GetSpellAbilityId() ) endfunction private function init takes nothing returns nothing local trigger t= CreateTrigger() set attemptShoot_code = function attemptShoot call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_EFFECT) call TriggerAddCondition(t, Condition(function spellIdMatch)) call TriggerAddAction(t, function onSpellEffect) endfunction endlibrary Changelog::
* 2.1 :
- Shockwave is evil, base spell replaced with Carrion Swarn, this improves
performance A LOT.
- Removed GroupUtils requirement.
- It uses the area effect art field instead of the missile art field for
fire bolts models.
- Fixed head-not-set-to-null leaks. |
| 09-19-2009, 03:00 AM | #2 |
Thought i don't quite understand the entire code, your stuffs are always epic :) |
| 09-19-2009, 10:11 AM | #3 |
doesn't shootNow and spawnHead leak heads? btw: Pretty neat spell Edit: don't know if its that leak, but damn this thing is a fps hog - i played the test map and started out with about 100-120fps after i destroyed the computer (with hydra only - i guess about 25 casts) i got like 15 left and it didn't go up again after all the hydras where gone. (after that i killed him again without a single hydra cast - never went below 80fps) Edit2: the struct leaks heads too o/c (at-least temporary) |
| 09-19-2009, 01:16 PM | #4 |
I forgot about setting heads to null yes (will fix, I will not, ever set struct things to null in onDestroy, it is lame to do it because it is not a leak). However, handle not set null are not a big deal. Even 25*3 leaks of that kind shouldn't affect your fps that much. So the thing is something else. I will try casting a lot of hydras again and see if it happens to me. Edit: Yes, tested and there's definitely a big issue in here. Will investigate. Edit: Fixed the leaks, memory is stable, fps keeps dropping. Edit: This is lame, I disabled the whole spell trigger, I just cast the base spell many times and it still drops the fps permanently... edit: also disabled the demo map trigger that runs when a spell is casted. I think this could be an odd bug with the base spell when you give it the data it is using. |
| 09-19-2009, 02:21 PM | #5 | ||
Quote:
but back to topic: about the fps think you can rule out one thing: shootNow and everything that gets called by it (except for attemptShot o/c), because the fps drops without shooting a single time Edit: Quote:
btw you could do set attemptShoot_code = function attemptShoot once in the init function instead of every shoot attempt |
| 09-19-2009, 02:55 PM | #6 |
that really does no improvement, but maybe I will. |
| 09-19-2009, 03:12 PM | #7 |
It is definitely related the spell casting itself. I made a debug code to call 100 hydras, and after these 100 hydras are gone, fps goes back to 125.0. What's odd is that I used a retail shockwave as a base spell and it still caused issues. |
| 09-19-2009, 03:16 PM | #8 |
seems like something is really of here |
| 09-19-2009, 03:47 PM | #9 |
Shockwave as a base spell lags because of terrain deformations it causes. Use Carrion Swarm instead. |
| 09-19-2009, 03:52 PM | #10 | |
Quote:
What is the rawcode of "carrion swarm" plz ? |
| 09-19-2009, 03:53 PM | #11 |
Uh, I don't know... Vex does have the English Wc3, though, or at least knows what Carrion Swarm is. Anyways, it is the line spell used by the Dreadlord -- And I pray to God that you know what that is. |
| 09-19-2009, 03:57 PM | #12 |
'AUcs' is the ability code by default and since when does shockwave case permanent and giant fps drops (for me like 80fps after about 20 casts) |
| 09-19-2009, 03:58 PM | #13 |
Oh this one, ok. Sometimes i play undead in a melee game, so yes i know it ^^ But it's not a straight line, or maybe you can edit some things to do it. |
| 09-19-2009, 04:01 PM | #14 |
you can make carrion swam look exactly like shockwave (minus the terrain deformation o/c) |
| 09-19-2009, 04:21 PM | #15 | |
Quote:
|
