HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

Simple Knockback System

10-11-2010, 01:11 PM#1
Quillraven
Simple Knockback System

What is that?
In this case a Knockback describes the translation of a unit over a several range, within a specific time, towards a given direction. The Simple Knockback System exactly supports this functionality plus the possibilty to add multiple KBs to the same target.

Why simple?
Because it is a very tiny and simple system without any bonusfeatures like other KB-systems do support( f.e. destruction of destructables or KB-callback functions ).
This system _ONLY_ supports the functionality mentioned before.

Why doesn't it support all these bonusfeatures?
I thought about that quite a time and came to the final decision that i for myself don't need all that stuff. The mentioned features are imo special cases that are not needed in each map. And those who need them can easily include them to fit their own desires.
Additionaly it keeps the code small and fast, since it doesn't need checks for deactivated features or unused features.

Why should i use this system?
Since i couldn't find any KB system that fits my desires, i could motivate myself to write my own.
I had used Rising_Dusk's system before but imo it hadn't intuitive parameters. As a user i want to define the range and the time need for a KB and not a startspeed and deceleration per second. I couldn't find out how to define f.e. a KB for range 500 within 1.25 seconds.
Then i found a recently new system on inwarcraft.de which was written by kricz. This system had a lot of bonusfeatures, like callback functions, the possibilty of chaning the angle of the KB during the KB etc.. Since i didn't need all that functionality it would've been dumb imo to use it if i am not even using 50% of the possible features. Additionaly it needed an external library called Listmodule, that i haven't seen before and that i haven't used anywhere. I am no fan of importing libraries to my map that i'm not using myself, so this system didn't fit my purposes either.
I checked out wc3c for other KB systems, but all of them had "mistakes" or were not supported with the "new" wc3 patch.

So if you are searching for a simple to use system and are looking for a system like i did, use SKBS :)

How to use it?
It only works if you have the JNGP. If you don't have it, you are screwed!
It should be a standard by now for each mapper that tries to make "good" maps and i don't see any point in not having it.
If you have the JNGP you'll need one more library, that is included in the map:
  • TerrainPathability by Rising_Dusk

You may ask yourself, why i use an external library, when i blamed kricz's system before for the listmodule library.
There is no real good alternative to TerrainPathability as far as i know. TerrainPathability doesn't create any objecteditor data and so it shouldn't be a problem for anybody to include/use it. KB needs accurate tile pathability checks and TerrainPathability does that for us. So why don't use it?

Credits
  • Vexorian for vJass
  • default KB effect for water and ground by Rising_Dusk
  • TerrainPathability by Rising_Dusk
  • Formula for speed and deceleration copied from kricz's system
  • random people who developed JNGP




And finally the code:
Changelog


12.10.2010
removed optional TimerUtils
added KB stacking( there are now multiple KBs allowed on one unit )
added user function IsUnitKnocked
added leave detect ability to the testmap
updated documentation
some code refactoring

14.10.2010
removed autoindex requirement
if a unit dies during a KB its corpse will still be affected by the KB
removed leaks from GUI spell
updated documentation


Code


Collapse JASS:
library SimpleKnockbackSystem requires TerrainPathability

// #############################################
// ########## SIMPLE KNOCKBACK SYSTEM ##########
// #############################################
//
//
// ################ INFORMATION ################
// This library provides a basic knockback function
// that allows the user to knockback a target unit
// over a sevral range within a specific time towards
// a given angle. It also allows the user to specify
// a special effect path, that will be automatically
// used for the knockback. If no special effect path
// is defined, the knockback for the unit will use
// the default effect paths( Models by RISING_DUSK ).
//
// I know that it is a simple system and does not 
// support destructable destroying or 
// knockback_callback functions. I can also assure you
// that this will be never added to this system.
// It's meant to be a simple knockback system with
// no additional overhead.
//
//
// ################# HOW TO USE #################
//
// Since i wanted to keep it simple, there are only two
// functions that a user will need:

// function KnockbackUnit takes unit u, real dist, real time, real angle, string fxpath returns nothing
// u -> the unit that should be knockbacked
// dist -> the maximum distance for the KB
// time -> the total time needed for the KB
// angle -> the direction in which the unit should be knockbacked
// fxpath -> the special effect that should be used for the KB
//           pass null for the fxpath to use the default effects for the KB
//
// If a unit is already knockbacked, the new KB will be added to the current KB
// so yes, KB stacking is possible.
//
//
// The second function is:
//
// function IsUnitKnocked takes unit u returns boolean
// 
// returne true, if the specific unit is currently knockbacked.
// else returns false
//
//
// ############### HOW TO IMPORT #################
//
// Just copy the System folder of this map into your map or
// copy the TerrainPathability and SKBS trigger into your map.
//
//
// ############# HOW TO CONFIGURATE ##############
//
// There are a few constants that can be manipulated.
// Check out the globals block below.

private keyword kbData
globals
    // TIMER_INTERVAL is the interval used for the KB timer
    // a lower value makes the KB look smoother but requires 
    // more computing power
    private constant real TIMER_INTERVAL    = 0.03125
    // EFFECT_GND and EFFECT_WTR are the default special effect
    // paths, that will be used for the KB if the fxpath parameter
    // is null
    private constant string EFFECT_GND      = "KnockbackDust.mdx"
    private constant string EFFECT_WTR      = "KnockbackWater.mdx"
    // EFFECT_RANGE defines the range between two special effects.
    // Since we don't want to spam unnecessary effects to keep
    // a better performance
    private constant integer EFFECT_RANGE   = 50
    
    
    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    // !!! DO NOT TOUCH ANYTHING BELOW  !!!
    // !!! YOU WILL BE DAMNED IF YOU DO !!!
    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
endglobals






private struct kbData
    unit target
    real speed
    real acceleration
    real sfxrng
    real cos
    real sin
    effect sfx
    integer uid
    string fxpath
    
    static timer TIMER
    static kbData array KB_DATA
    static integer KB_DATA_SIZE
    static key KB_COUNTER_INDEX
    static hashtable TABLE
    
    method addEffect takes real x, real y returns nothing
        call DestroyEffect( .sfx )
        if( .fxpath != null )then
            set .sfx = AddSpecialEffect( fxpath, x, y )
        else
            if( IsTerrainLand( x, y ) )then
                set .sfx = AddSpecialEffect( EFFECT_GND, x, y )
            else
                set .sfx = AddSpecialEffect( EFFECT_WTR, x, y )
            endif
        endif
    endmethod
    
    method update takes nothing returns boolean
        local real newx = 0
        local real newy = 0
        
        // if the target is already removed -> stop the KB
        if( GetUnitTypeId( .target ) == 0 )then
            return false
        endif
        
        // update unit position
        set newx = GetUnitX( .target ) + .speed * .cos
        set newy = GetUnitY( .target ) + .speed * .sin
        if( not IsTerrainWalkable( newx, newy ) )then
            set newx = TerrainPathability_X
            set newy = TerrainPathability_Y
        endif
        call SetUnitX( .target, newx )
        call SetUnitY( .target, newy )
        
        // update effect status
        set .sfxrng = .sfxrng - .speed
        if( .sfxrng <= 0 )then
            set .sfxrng = EFFECT_RANGE
            call .addEffect( newx, newy )
        endif
            
        // update speed
        // as long as the speed is greater than zero, the KB will continue
        set .speed = .speed + .acceleration
        return .speed > 0
    endmethod
    
    static method timer_callback takes nothing returns nothing
        local integer i = 0
        
        // loop through all KB instances and update them
        loop
            exitwhen i >= kbData.KB_DATA_SIZE
            if( not kbData.KB_DATA[i].update() )then
                // remove instance
                if( i == kbData.KB_DATA_SIZE-1 )then
                    // remove tail element of the queue
                    call kbData.KB_DATA[i].destroy()
                else
                    // remove a head element of the queue
                    // destroy it
                    call kbData.KB_DATA[i].destroy()
                    // and swap the position with the tail element
                    // remember to use KB_DATA_SIZE for the array index
                    // since the index gets reduced in the onDestroy method
                    set kbData.KB_DATA[i] = kbData.KB_DATA[kbData.KB_DATA_SIZE]
                    // also reduce i by one to update the swapped
                    // element correctly
                    set i = i - 1
                endif
            endif
            set i = i + 1
        endloop
    endmethod
    
    static method create takes unit u, real v, real a, real angle, string sfxpath returns kbData
        local kbData this = kbData.allocate()
        
        set .target = u
        set .speed = v
        set .acceleration = a
        set .cos = Cos( angle )
        set .sin = Sin( angle )
        set .uid = GetHandleId( u )
        set .sfx = null
        set .fxpath = sfxpath
        set .sfxrng = EFFECT_RANGE
        call .addEffect( GetUnitX( u ), GetUnitY( u ) )
        
        // set the IS_KNOCKED flag to true
        // and add the KB instance to the global queue
        if( HaveSavedInteger( kbData.TABLE, .uid, kbData.KB_COUNTER_INDEX ) )then
            call SaveInteger( kbData.TABLE, .uid, kbData.KB_COUNTER_INDEX, LoadInteger( kbData.TABLE, .uid, kbData.KB_COUNTER_INDEX ) + 1 )
        else
            call SaveInteger( kbData.TABLE, .uid, kbData.KB_COUNTER_INDEX, 1 )
        endif
        set kbData.KB_DATA[kbData.KB_DATA_SIZE] = this
        set kbData.KB_DATA_SIZE = kbData.KB_DATA_SIZE + 1
        if( kbData.KB_DATA_SIZE == 1 )then
            call TimerStart( kbData.TIMER, TIMER_INTERVAL, true, function kbData.timer_callback )
        endif
        
        return this
    endmethod
    
    method onDestroy takes nothing returns nothing
        call DestroyEffect( .sfx )
        
        // clear the IS_KNOCKED flag and remove the instance of the global queue
        call SaveInteger( kbData.TABLE, .uid, kbData.KB_COUNTER_INDEX, LoadInteger( kbData.TABLE, .uid, kbData.KB_COUNTER_INDEX ) - 1 )
        set kbData.KB_DATA_SIZE = kbData.KB_DATA_SIZE - 1
        if( kbData.KB_DATA_SIZE == 0 )then
            call PauseTimer( kbData.TIMER )
        endif
    endmethod
    
    static method onInit takes nothing returns nothing
        set kbData.TIMER = CreateTimer()
        set kbData.TABLE = InitHashtable()
        set kbData.KB_DATA_SIZE = 0
    endmethod
endstruct





// #############################################
// ########## USER FUNCTIONS GO BELOW ##########
// #############################################
function IsUnitKnocked takes unit u returns boolean
    return LoadInteger( kbData.TABLE, GetHandleId( u ), kbData.KB_COUNTER_INDEX ) > 0
endfunction

function KnockbackUnit takes unit u, real dist, real time, real angle, string fxpath returns nothing
    local real speed = 0
    local real acceleration = 0
    
    if( u == null or IsUnitType( u, UNIT_TYPE_DEAD ) or dist <= 0 or time <= 0 )then
        return
    else
        set speed = 2 * dist / (( time / TIMER_INTERVAL ) + 1)
        set acceleration = -speed / (time / TIMER_INTERVAL)
        call kbData.create( u, speed, acceleration, angle, fxpath )
    endif
endfunction

endlibrary



The testmap contains an example in GUI, to demonstrate, how easy it is to use it even in GUI.
Attached Files
File type: w3xKnockback.w3x (35.8 KB)
10-12-2010, 07:45 PM#2
Anitarf
Wait, did you intend this to be a resource submission? If that is the case, the thread is posted in the wrong forum, however if you did not intend to submit the code to our resource section then there's nothing wrong with it being posted where it is, so I'll leave it here unless you request otherwise.
10-12-2010, 09:30 PM#3
Quillraven
hm thought i post it here first to get some feedback, suggestions, etc.. and if anyone thinks its good enough or worthy to become an "approved system" i m going to post it in the system forum(?).

however if this is the wrong place for that, move it pls :) i m not familiar with the wc3c "forum structure".
10-12-2010, 10:47 PM#4
Anitarf
Like I said, there's nothing wrong with it being posted here, it's just the content of the post made me think you might have intended to post it as a resource submission. If you didn't, then everything is fine. Anyway, here are my comments:


You should update your description along with the code. Right now, the description says knockback stacking is not supported, but the code does support it.

The IsUnitKnocked function doesn't seem essential to the system. In a larger system, it would make sense to include it anyway along with other non-essential features, but in this simple system it seems unnecessary, especially because it adds the AutoIndex requirement (speaking of which, it wasn't really needed back when you had a limit of one knockback per unit, a global unit group could get the job done. Not that I'm suggesting you go back to that needless limit.)

Quote:
Additionaly it needed an external library called Listmodule, that i haven't seen before and that i haven't used anywhere. I am no fan of importing libraries to my map that i'm not using myself, so this system didn't fit my purposes either.
Quote:
TerrainPathability doesn't create any objecteditor data and so it shouldn't be a problem for anybody to include/use it.
You appear rather hypocritical here. ;)

Quote:
I couldn't find out how to define f.e. a KB for range 500 within 1.25 seconds.
It is rather simple: the average speed for such a knockback needs to be 500/1.25=400, so the starting speed needs to be twice as much, 800. The deceleration needs to be sufficient to reduce the speed from 800 to 0 in 1.25 seconds, so it needs to be 800/1.25=640.
10-13-2010, 06:29 AM#5
Quillraven
Quote:
It is rather simple: the average speed for such a knockback needs to be 500/1.25=400, so the starting speed needs to be twice as much, 800. The deceleration needs to be sufficient to reduce the speed from 800 to 0 in 1.25 seconds, so it needs to be 800/1.25=640.

1) thx for the math
2) the disadvantage is imo, that the user has to calculate that by himself. "my" approach is more intuitive
3) afaik a comment in dusk's system says, that the deceleration must be higher then the startspeed, so your calculation may be correct, but it isn't correct for the system? i also know, that some people told me, that some of my knockbacks sometimes last forever/to long. that was maybe the problem of deceleration<startspeed that sometimes appeared.
edit: i was using dusk's system in my maps

Quote:
Right now, the description says knockback stacking is not supported,
where does it say, that it is not supported?
"// If a unit is already knockbacked, the new KB will be added to the current KB
// so yes, KB stacking is possible.
"

but you are still right. there is still a part talking about timerutils that was already removed.



fixing doko soon, and i remove the isunittype dead check during the KB, so if a unit dies during the kb its corpse will still be affected.


Quote:
The IsUnitKnocked function doesn't seem essential to the system
that is also true, but it provides you a simple way, to avoid kb stacking f.e. or to add additional effects to a spell( if a unit is already kbed, deal 100 extra damage ).
and since it is a simple counter inside the system and an inlined(?) return value for one function, i will keep it.
10-13-2010, 11:28 AM#6
Anitarf
Quote:
Originally Posted by Quillraven
that is also true, but it provides you a simple way, to avoid kb stacking f.e. or to add additional effects to a spell( if a unit is already kbed, deal 100 extra damage ).
and since it is a simple counter inside the system and an inlined(?) return value for one function, i will keep it.
It's not so simple, it requires AutoIndex. If users want this functionality, they can always write a wrapper library. If they want to limit it to one knockback at a time (I see no reason why anyone would want that, but okay), then even their wrapper wouldn't require AutoIndex since they could just use a single group, or they could use a hashtable instead. In the meantime, all those users who don't care about whether a unit is being knockbacked don't have to bother with AutoIndex.

I wouldn't make such a big deal out of this normally but since this is supposed to be a simple knockback system, I have to point out what I see as needless complications.
10-14-2010, 07:23 AM#7
Quillraven
*Update*

changelog in first post
10-15-2010, 03:40 PM#8
Tot
Quote:
Originally Posted by Anitarf
It's not so simple, it requires AutoIndex. If users want this functionality, they can always write a wrapper library. If they want to limit it to one knockback at a time (I see no reason why anyone would want that, but okay), then even their wrapper wouldn't require AutoIndex since they could just use a single group, or they could use a hashtable instead. In the meantime, all those users who don't care about whether a unit is being knockbacked don't have to bother with AutoIndex.

I wouldn't make such a big deal out of this normally but since this is supposed to be a simple knockback system, I have to point out what I see as needless complications.

[ljass]library ... requires optional AutoIndex ... [l/jass]
static if LIBRARY_AutoIndex then ??