- Joined
- Mar 28, 2005
- Messages
- 160
Introduction:
In the spirit of Vexorian's classic Collision Missiles system, I present for initial review and comment, a fully functional collision missiles system. Those that have popped up recently are generally neither as robust nor so easy to employ, and always tend to lack on key feature or another. I aimed to incorporate all that necessary functionality you'd expect, without limiting your potential through hard to navigate and use API.
My first attempt, a while back, was teh pooh. This has been rewritten from the ground up, with ease of use at the fore front of my priorities. Things can be as simple or as complicated as you want them to be, its entirely your decision.
What this does well:
What this doesn't do well (* denotes a planned incorporation in future updates):
This currently requires GroupUtils, but as I come to decide on how to handle multiple collisions/target that will probably be removed. UnitAlive native employed for dummy unit life status checking.
How Can I Help?
Review, review, review. I know there is no demo map. I haven't had the time to really put something together at the production level I would like. But, the script is all here. I understand that this is probably no where near an acceptable state, but please bear that in mine as you review, too.
Things to pay particular attention to:
System Script:
Missile Interface:
Example Use:
Thanks for taking the time to read all of this. Hope you enjoyed.
-Hulk3
In the spirit of Vexorian's classic Collision Missiles system, I present for initial review and comment, a fully functional collision missiles system. Those that have popped up recently are generally neither as robust nor so easy to employ, and always tend to lack on key feature or another. I aimed to incorporate all that necessary functionality you'd expect, without limiting your potential through hard to navigate and use API.
My first attempt, a while back, was teh pooh. This has been rewritten from the ground up, with ease of use at the fore front of my priorities. Things can be as simple or as complicated as you want them to be, its entirely your decision.
What this does well:
- Simple missile launching at a specified velocity and direction, with the ability to home onto targets
- Simple API and straight forward usage
- Timed lives, maximum distances, with the ability to dynamically change values
- Safe debri destruction
- Realistic terrain/unit/missile collision physics
- Dynamic missile scaling of size and radius
- Simple parabolic movements, with realistic dummy pitch updates
- Gravity
- Resting state recognition
- OnEvent responses
- Rolling missiles with ground friction
- Optimal periodic execution format and linked list struct stack
What this doesn't do well (* denotes a planned incorporation in future updates):
- Creation at an offset at a point or unit
- Multiple collisions on the same target*
- Find new targets if primary becomes un-reachable*
- Realistic 3D collisions(doesn't take into account z position/velocity when conserving momentum)*
- Determining if a missile is rolling or not*
- O(n) missile-on-missile collisions
- Advanced interface method functionality*:
- createFromUnit
- set/addAcceleration
- projectToPoint
- controlled parabolic movements
- etc.
This currently requires GroupUtils, but as I come to decide on how to handle multiple collisions/target that will probably be removed. UnitAlive native employed for dummy unit life status checking.
How Can I Help?
Review, review, review. I know there is no demo map. I haven't had the time to really put something together at the production level I would like. But, the script is all here. I understand that this is probably no where near an acceptable state, but please bear that in mine as you review, too.
Things to pay particular attention to:
- Can things be optimized at all?
- A better way to tell if missiles are rolling
- A better way to limit facing angle updates when homing
- How do you feel about my cliff/ground detection?
- Can we produce a better means of missile-on-missile collision detection? Rather than O(n).
- Where is this lacking, in the functionality department?
- What separates this from prior art, if anything?
- What do you think about controlling knock-backs using this system as well? Would put all possible map particle movement into one system, opening up a lot of really neat possibilities IMO.
- How to approach multiple collisions/target, w/o having to worry about multiple collisions happening one after the other after the other, until a large enough gap is observed. This can become an issue.
- How to approach removal of initial cast missile collision with a caster.
System Script:
JASS:
//=========================\\
//==System made by emjlr3==\\
//==Version 1.a, 01/24/12==\\
//=========================\\
library CollisionMissiles requires GroupUtils
native UnitAlive takes unit id returns boolean
struct missile
//== CONFIGURATION ==\\
private static constant integer DUMMY = 'dumy' // dummy unit to use for projectiles
private static constant integer ABIL = 'tree' // rawcode of tree destruction ability
private static constant real REST = 45. // units/sec velocity considered to be 0.
private static constant real TIMEOUT = 0.03125 // periodic timer callback interval
//== REQUIRED ==\\
private static group G = CreateGroup()
private static integer C = 0
private static location LOC = Location(0.,0.)
private static thistype array M
private static constant real GRAVITY = -32.144*TIMEOUT
private static constant real FRACPI = bj_PI/16.
private static constant real HALFPI = bj_PI/2.
private static constant real REALREST = REST*TIMEOUT
private static constant real TWOPI = bj_PI*2.
private static constant real TERMZ = 90./(1500.*TIMEOUT)
private static timer T = CreateTimer()
private static trigger TRIG = CreateTrigger()
private static unit DUM = null
private thistype next
private thistype prev
missileData data
unit dum=null
unit targ=null
player p=null
effect sfx=null
real xP
real yP
real zP
real z0
real zV=0.
real ang
real cos
real sin
real vel=0.
real dist=0.
real maxDist=-0.
real life=-0.
real sqRadius
boolean end=false
boolean rolling=false
group g
// destroy this instance
method destroy takes nothing returns nothing
call .data.onDeath(this)
call DestroyEffect(.sfx)
call KillUnit(.dum)
call ReleaseGroup(.g)
call .deallocate()
endmethod
// collision with hostiles/missiles
static method getDeflectionAngle takes thistype this, unit t returns real
return 2.*Atan2(GetUnitY(t)-.yP,GetUnitX(t)-.xP)+bj_PI-.ang
endmethod
// collisions with cliffs
static method getDeflectionAngleCliff takes thistype this, real x, real y returns real
return 2.*Atan2(y-.yP,x-.xP)+bj_PI-.ang
endmethod
static method periodic takes nothing returns boolean
local thistype this=thistype(0).next
local thistype t
local real array r
local real s
local unit u=null
loop
exitwhen this==0
// update positioning
set .dist=.dist+.vel
// target found?
if .targ!=null and UnitAlive(.targ) then
set .ang=.ang*bj_RADTODEG
set s=Atan2(GetUnitY(.targ)-.yP,GetUnitX(.targ)-.xP)*bj_RADTODEG
// Solve for updated facing accounting for angular rotation limitations
// a large limit allows for instant facing changes
if s>.ang then
set s=s-.ang
if s>.data.angularRotation then
set s=.data.angularRotation
endif
set .ang=(.ang+s)*bj_DEGTORAD
else
set s=.ang-s
if s>.data.angularRotation then
set s=.data.angularRotation
endif
set .ang=(.ang-s)*bj_DEGTORAD
endif
set .cos=Cos(.ang)
set .sin=Sin(.ang)
endif
set .xP=.xP+.vel*.cos
set .yP=.yP+.vel*.sin
set .zP=.zP+.zV
// missile is rolling, apply ground friction
if .rolling then
// check incase we've added a z velocity since I last established that this missile was rolling
// if its rolling it should have a 0 z velocity
if RAbsBJ(.zV)>0. then
set .rolling=false
else
set .vel=.vel*.data.friction
endif
else
// if the missile is in the air, apply gravity and drag
set .vel=.vel*.data.drag
if .data.gravity then
set .zV=.zV+GRAVITY
endif
endif
// update position
call SetUnitX(.dum,.xP)
call SetUnitY(.dum,.yP)
call MoveLocation(LOC,.xP,.yP)
// update fly height
call SetUnitFlyHeight(.dum,.zP-GetLocationZ(LOC),0.)
call SetUnitFacing(.dum,.ang*bj_RADTODEG)
// update pitch, maximum zV of 1500/s
// anything above this and our missile is traveling too damned fast to see anyways
call SetUnitAnimationByIndex(.dum,90+R2I(.zV*TERMZ))
// tree destruction if the missile is not above the trees
// kudos to Bribe for this neat trick
// i added in the height gap check because it looks strange of missiles clearly
// above the tree line are destroying trees in their wake
if .data.wantDestroyTrees and .zP-GetLocationZ(LOC)<375. then
call SetUnitAbilityLevel(DUM,ABIL,IMaxBJ(4,R2I(.data.radius/32.)))
call IssuePointOrder(DUM,"flamestrike",.xP,.yP)
endif
// cliff detection
// i divided by 2 because I was having issues with cliffs being found earlier than desired
// for aesthetics sake really
set r[1]=GetLocationZ(LOC)+GetUnitFlyHeight(.dum)-.data.radius/2.
set r[2]=.ang-HALFPI
set r[3]=.ang+HALFPI
// loop in a half circle using the missiles facing
loop
exitwhen r[2]>r[3]
set r[4]=.xP+.data.radius*Cos(r[2])
set r[5]=.yP+.data.radius*Sin(r[2])
call MoveLocation(LOC,r[4],r[5])
set r[6]=GetLocationZ(LOC)
if r[6]>=r[1] and r[6]>r[9] then // collision found
// update dynamic array members
set r[7]=r[4] // x
set r[8]=r[5] // y
set r[9]=r[6] // z
endif
set r[2]=r[2]+FRACPI
endloop
if r[9]>0. then
call MoveLocation(LOC,.xP,.yP)
if r[9]-GetLocationZ(LOC)>=10. then // cliff collision
call .data.onCollisionCliff(this)
// bounce the missile
if .data.bounce then
set .ang=thistype.getDeflectionAngleCliff(this,r[7],r[8])
set .cos=Cos(.ang)
set .sin=Sin(.ang)
endif
else // ground collision
if not .rolling then
call .data.onCollisionGround(this)
if .data.bounce then
// stop ground bouncing, otherwise the bouncing gets really small and looks really odd
if RAbsBJ(.zV)<=REALREST then
set .zV=0.
call SetUnitFlyHeight(.dum,0.,0.)
set .rolling=true
else
// apply restitution values
set .zV=RAbsBJ(.zV*.data.restitution)
set .vel=.vel*.data.restitution
endif
endif
endif
endif
endif
// hostiles collision
// +128 assumes a collision radius for the targets
call GroupEnumUnitsInRange(G,.xP,.yP,.data.radius+128.,null)
loop
set u=FirstOfGroup(G)
exitwhen u==null
call GroupRemoveUnit(G,u)
call MoveLocation(LOC,GetUnitX(u),GetUnitY(u))
// x/y range is good, but is z position also?
// we can currently only collide with hostiled units
// until I decide on a method to remove initial caster collisions
if RAbsBJ(.zP-(GetLocationZ(LOC)+GetUnitFlyHeight(u)))<.data.radius+128. and UnitAlive(u) and IsUnitEnemy(u,.p) then
call data.onCollisionHostile(this,u)
set .ang=thistype.getDeflectionAngle(this,u)
set .cos=Cos(.ang)
set .sin=Sin(.ang)
// no more collision for this hostile with this missile :(
// future updates should remove this limitation, same goes for missiles
call GroupAddUnit(.g,u)
endif
endloop
// missiles collision, O(n) ftl
// most systems don't allow for missile collisions, probably because it's so slow
set t=thistype(0).next
loop
exitwhen t==0
if .dum!=t.dum then
if not IsUnitInGroup(t.dum,.g) then
// is distance between missiles greater than the sum of their radii?
if (t.xP-.xP)*(t.xP-.xP)+(t.yP-.yP)*(t.yP-.yP)+(t.zP-.zP)*(t.zP-.zP)<=.sqRadius+t.sqRadius then
call data.onCollisionHostile(this,t.dum)
set .ang=thistype.getDeflectionAngle(this,t.dum)
set .cos=Cos(.ang)
set .sin=Sin(.ang)
call GroupAddUnit(.g,t.dum)
call data.onCollisionHostile(t,.dum)
set t.ang=thistype.getDeflectionAngle(t,.dum)
set t.cos=Cos(t.ang)
set t.sin=Sin(t.ang)
call GroupAddUnit(t.g,.dum)
endif
endif
endif
set t=t.next
endloop
// maximum distance traveled
if .maxDist>0. then
if .dist+.vel>=.maxDist then
set .end=true
set .vel=.maxDist-.dist
endif
endif
// timed life up
if .life>0. then
set .life=.life-TIMEOUT
if .life<=0. then
set .end=true
endif
endif
// resting state established
if .vel<=REALREST then
call .data.onRest(this)
if .data.wantDestroyOnRest then
set .end=true
endif
endif
call .data.onLoop(this)
// destroy the missile
if .end or not UnitAlive(.dum) then
set this.prev.next=this.next
set this.next.prev=this.prev
call .destroy()
set C=C-1
endif
set this=this.next
endloop
// no more missile instances left
if C==0 then
call PauseTimer(T)
endif
// required for a boolean returning function
return false
endmethod
//== CREATION METHODS ==\\
// currently only the one :(
static method create takes missileData data, player p, real x, real y, real height, real face, real velocity returns thistype
local thistype this=thistype.allocate()
set .data=data
set .g=NewGroup()
// create dummy
set .dum=CreateUnit(p,DUMMY,x,y,face*bj_RADTODEG)
call SetUnitX(.dum,x)
call SetUnitY(.dum,y)
set .p=p
set .ang=face
set .cos=Cos(.ang)
set .sin=Sin(.ang)
call UnitAddAbility(.dum,'Aloc')
call UnitAddAbility(.dum,'Amrf')
call SetUnitScale(.dum,.data.scale,.data.scale,.data.scale)
set .sfx=AddSpecialEffectTarget(.data.sfx,.dum,"origin")
call SetUnitAnimationByIndex(.dum,90)
call SetUnitPathing(.dum,false)
call SetUnitInvulnerable(.dum,true)
// positioning
set .xP=x
set .yP=y
call MoveLocation(LOC,x,y)
set .z0=GetLocationZ(LOC)
set .zP=height+.z0
call SetUnitFlyHeight(.dum,height,0)
if height<=0 then
set .rolling=true
endif
set .vel=velocity*TIMEOUT
set .sqRadius=.data.radius*.data.radius
// update linked list
set C=C+1
set thistype(0).next.prev=this
set this.next=thistype(0).next
set thistype(0).next=this
set this.prev=thistype(0)
if C==1 then
call TimerStart(T,TIMEOUT,true,function thistype.tick)
endif
call data.onCreate(this)
return this
endmethod
//== FORCE APPLICATION METHODS ==\\
// these are pretty self explanatory
method addMaxDistance takes real r returns nothing
set .maxDist=r+.maxDist
endmethod
method setMaxDistance takes real r returns nothing
set .maxDist=r
endmethod
method addVelocity takes real r returns nothing
set .vel=r*TIMEOUT+.vel
endmethod
method setVelocity takes real r returns nothing
set .vel=r*TIMEOUT
endmethod
method addZVelocity takes real r returns nothing
set .zV=r*TIMEOUT+.zV
endmethod
method setZVelocity takes real r returns nothing
set .zV=r*TIMEOUT
endmethod
//== FUNCTIONALITY METHODS ==\\
// these too
method addRadius takes real r returns nothing
set .data.radius=r+.data.radius
set .sqRadius=.data.radius*.data.radius
endmethod
method setRadius takes real r returns nothing
set .data.radius=r
set .sqRadius=.data.radius*.data.radius
endmethod
method addScale takes real r returns nothing
set .data.scale=r+.data.scale
call SetUnitScale(.dum,.data.scale,.data.scale,.data.scale)
endmethod
method setScale takes real r returns nothing
set .data.scale=r
call SetUnitScale(.dum,r,r,r)
endmethod
method setTarget takes unit t returns nothing
set .targ=t
endmethod
method forgetTarget takes unit t returns nothing
set .targ=null
endmethod
method addTimedLife takes real r returns nothing
set .life=r+.life
endmethod
method setTimedLife takes real r returns nothing
set .life=r
endmethod
method remove takes nothing returns nothing
set .end=true
endmethod
// some how some way, this is faster than simply running everything
// in the timer handlerFunc execution
private static method tick takes nothing returns nothing
call TriggerEvaluate(TRIG)
endmethod
private static method onInit takes nothing returns nothing
call TriggerAddCondition(TRIG,Condition(function thistype.periodic))
set DUM=CreateUnit(Player(15),DUMMY,0.,0.,0.)
call UnitAddAbility(DUM,ABIL)
call UnitAddAbility(DUM,'Aloc')
call SetUnitPathing(DUM,false)
call SetUnitInvulnerable(DUM,true)
call PauseUnit(DUM,false)
endmethod
endstruct
endlibrary
JASS:
library MissileData
interface missileData
real angularRotation = 30. // maximum rotation angle/TIMEOUT in degrees
real drag = 1.0 // fraction of velocity retained when flying (0 is most, 1 is least, values should be close to 1.0 as this fires every timer interval)
real restitution = 1.0 // fraction of velocities recovered following collisions (0 is most, 1 is least)
real friction = 1.0 // fraction of velocity retained when rolling (0 is most, 1 is least, values should be close to 1.0 as this fires every timer interval)
real radius = 32. // collision radius
real scale = 1.0 // dummy scale
boolean bounce = true // bounce off the ground and cliffs? (sorry no trees yet)
boolean collide = true // collide with other missiles and hostile units? (sorry no non-hostiles yet)
boolean gravity = true // should gravity affect missiles?
boolean wantDestroyOnRest = true // destroy missiles when they stop moving?
boolean wantDestroyTrees = true // destroy trees in a missiles path?
string sfx = "" // sfx applied to the missile
// these are all very self explanatory and shouldn't require much elaboration
method onCollisionCliff takes missile m returns nothing defaults nothing
method onCollisionGround takes missile m returns nothing defaults nothing
method onCollisionHostile takes missile m, unit target returns nothing defaults nothing
method onCollisionMissile takes missile m1, missile m2 returns nothing defaults nothing
method onCreate takes missile m returns nothing defaults nothing
method onDeath takes missile m returns nothing defaults nothing
method onLoop takes missile m returns nothing defaults nothing
method onRest takes missile m returns nothing defaults nothing
endinterface
endlibrary
JASS:
//========================\\
//==Spell made by emjlr3==\\
//==Version XX, XX/XX/XX==\\
//========================\\
scope FireBomb
private struct data extends missileData
//== Configuration Section ==\\
static constant integer ABIL = 'BOMB' // abilid rawcode
//== Struct Specific ==\\
// interface specific
real angularRotation = 30.
real drag = 1.0
real restitution = .7
real friction = .98
real radius = 32.
real scale = 1.0
boolean bounce = true
boolean collide = true
boolean gravity = true
boolean wantDestroyOnRest = true
boolean wantDestroyTrees = true
string sfx = "Abilities\\Weapons\\LordofFlameMissile\\LordofFlameMissile.mdl"
// specific to this spell
real x
real y
unit u
real ux
real uy
real ang
real r=0.
real dist
missile m
method destroy takes nothing returns nothing
// see if we store our missile struct, we can use its members :)
call DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Other\\NeutralBuildingExplosion\\NeutralBuildingExplosion.mdl",.m.xP,.m.yP))
call .deallocate()
// remove the missile and its struct
call m.remove()
endmethod
method onCollisionGround takes missile m returns nothing
//call .destroy()
// do stuff when we hit the ground
endmethod
method onCollisionHostile takes missile m, unit target returns nothing
//call .destroy()
// do stuff when we strike an enemy unit
endmethod
method onCollisionCliff takes missile m returns nothing
//call .destroy()
// do stuff when we hit a cliff
endmethod
method onLoop takes missile m returns nothing
// do things periodically
endmethod
static method create takes nothing returns thistype
local thistype this=thistype.allocate()
local missile m // we can create a new missile using a struct interface
set .x=GetSpellTargetX()
set .y=GetSpellTargetY()
set .u=GetTriggerUnit()
set .ux=GetUnitX(.u)
set .uy=GetUnitY(.u)
set .ang=ABPXY(.ux,.uy,.x,.y)
set .dist=DBPXY(.ux,.uy,.x,.y)
// create a new missile for the caster, at its location, with fly height 75., facing/moving towards the targeted spot, at 500units/s
set m=missile.create(this,Player(0),.ux,.uy,75.,.ang,500.)
// we only want it to move so far as the targeted spot
call m.addMaxDistance(.dist)
// nice looking parabola
// eventually I will make methods to create a fluid parabola from spot a to spot b, given a max height
// some day....
// currently this will create a seemingly unpredictable arc, and will probably hit the maxDist well before it hits the ground
// if we wanted to use calculus we could solve for the sum of the function that creates it
// some day....
call m.addZVelocity(950.)
// storing the missile struct is a nice touch, I recommend it
set .m=m
return this
endmethod
static method conditions takes nothing returns boolean
if GetSpellAbilityId()==ABIL then
call thistype.create()
endif
return false
endmethod
static method onInit takes nothing returns nothing
local trigger trig=CreateTrigger()
local integer index=0
loop
call TriggerRegisterPlayerUnitEvent(trig,Player(index),EVENT_PLAYER_UNIT_SPELL_EFFECT,null)
set index=index+1
exitwhen index==bj_MAX_PLAYER_SLOTS
endloop
call TriggerAddCondition(trig,Condition(function thistype.conditions))
endmethod
endstruct
endscope
-Hulk3
Last edited by a moderator: