- Joined
- Jan 21, 2006
- Messages
- 2,552
Custom Projectiles
I've re-written this a few times, optimized it, and added enough functionality to make it actually useful and very easy to create projectiles that mimic the WarCraft III projectiles.
When a projectile is created, the user must designate a unit as the "projectile" object. This unit will constantly have its position refreshed so it will not be able to move as a projectile (though orders can still be given). Typically a unit is designated as a projectile and then immediately launched --in which case it is flagged as "active".
Once a projectile is active it will soar through the air with the given input on
When a projectile is created, there are a few available methods that allow you to control the way the projectile will act, such as
The dynamic aspect of the system comes from the ability to extend the projectile, and define methods for special projectile events such as
I was thinking of uploading this as a submission but I hate making descriptions : (
Projectiles
I'll include a test-map too where you can play around with it. It includes the vectors library that is required as well as an example of a struct that extends the projectile.
I've re-written this a few times, optimized it, and added enough functionality to make it actually useful and very easy to create projectiles that mimic the WarCraft III projectiles.
When a projectile is created, the user must designate a unit as the "projectile" object. This unit will constantly have its position refreshed so it will not be able to move as a projectile (though orders can still be given). Typically a unit is designated as a projectile and then immediately launched --in which case it is flagged as "active".
Once a projectile is active it will soar through the air with the given input on
doLaunch()
which takes a two vectors (start/finish 3D points), a speed and an arc. The projectile will then soar through the air with the given input.When a projectile is created, there are a few available methods that allow you to control the way the projectile will act, such as
setProjectileTarget[()
which will cause the projectile's target to be updated in sync with a unit. If projectile homing for that projectile is enabled, the projectile will follow the target properly (like War3 projectiles).The dynamic aspect of the system comes from the ability to extend the projectile, and define methods for special projectile events such as
onUnitCollision (takes unit)
or onFinish
. If a projectile is assigned a target, then the .target
member will be available to the user (if damage were to be dealt, or other effects).I was thinking of uploading this as a submission but I hate making descriptions : (
Projectiles
JASS:
library Projectiles requires Vectors
globals
//************************
//* Configuration
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯
//* Includes default definitions of projectile settings in addition to other values that are used
//* within the system but rely on a value in order to function.
//*
//* >> Medihv's crow-form ability which allows non-flying units to have their fly-height modified;
public constant integer FLY_ABIL_ID = 'Amrf'
//*
//* >> Default constants;
public constant real PROJ_COLLISION_DEFAULT = 70.00
public constant real PROJ_COLLISION_MAX = 200.00
//*
public constant real PROJ_MOTION_REF = 0.03 //* This identifies the amount of time (in seconds) that lapses
//* between each iteration that the projectiles are "updated";
//* >> Active feature defaults;
private constant boolean PROJ_TARG_FOLLOW_ON = true
private constant boolean PROJ_UNIT_COLLIDE_ON = true //* The unit-collision feature of projectiles is on by default.
private constant boolean PROJ_FACING_ROTATION_ON = true //* Whether or not the unit's facing will be updated to match the
//* direction of the velocity vector.
private constant boolean PROJ_PITCH_ROTATION_ON = true //* Whether or not a unit will have its pitch-rotation adjusted
//* when the projectile is updated. Only guaranteed to work with
//* the dummy.mdx included in the imports (by Vexorian).
//*
private constant boolean PROJ_EXPIRE_ON = true //* By default, projectiles are flagged to be destroyed after their
//* determined path unless otherwise specified. If it is necessary
//* to manipulate projectiles after their determined lifespan, then
//* this should be set to false.
private constant boolean PROJ_TO_KILL_ON = true //* Once the projectile expires, the unit associated with the
//* projectile will in turn be killed.
//*
//*
//*********************************************************************************************************
endglobals
interface projectileinterface
//***************************
//* Projectile Interface
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
//* Includes the names for methods (and their parameters) that users can declare in child-structures to
//* acquire reference to a projectile on different occurances that prompt a response.
//*
//* >> Responses associated with time-line "events";
method onStart takes nothing returns nothing defaults nothing
method onFinish takes nothing returns nothing defaults nothing
//*
//* >> Responses associated with physical surrounding occurances;
method onGround takes nothing returns nothing defaults nothing
//*
//* >> Responses associated with widget collision;
method onUnitCollision takes unit u returns nothing defaults nothing
//method onDestCollision takes destructable d returns nothing defaults nothing
//*
//*
//*********************************************************************************************************
endinterface
globals
//********************
//* Dynamic Storage
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
//* In order to optimize the Projectiles efficiency, certain global variables are required to make special
//* calculations and operations; such as GetLocationZ().
//*
private projectile projRef
private location loc = Location(0, 0)
private group grp = CreateGroup()
//*
//*
//*********************************************************************************************************
endglobals
struct projectile extends projectileinterface
//*******************************************
//* Projectile
//* ¯¯¯¯¯¯¯¯¯¯
//* Primary data-structure that encapsullates the projectile functionality. Contains methods for creating,
//* destroying, and manipulating certain aspects of the projectile motion.
//*
//* #######################################################################################################
//*
readonly unit toUnit = null
readonly unit target = null
readonly unit source = null //* In many situations it is necessary for projectiles to have a "source" unit
//* to gather information about where the projectile came from.
//*
readonly real speed //* In order to launch a projectile, a "speed" and "arc" input is necessary. These
readonly real arc //* values are stored in readonly-scope variables so that they can be referenced
//* externally.
//*
public real timescale = 1.00
private real priv_collision = PROJ_COLLISION_DEFAULT
//*
//* These are used to maintain necessary projectile checks. They are also used to determine the features
//* that should be included in each specific projectile, such as "activeUnitCollision".
//*
private boolean active = false //* This is automatically updated depending on whether the projectile
//* has been launched.
public boolean activeTargetFollow = PROJ_TARG_FOLLOW_ON
public boolean activePitch = PROJ_PITCH_ROTATION_ON
public boolean activeRotation = PROJ_FACING_ROTATION_ON //* Whether the unit's facing will be updated to match the 2D direction
//* of the projectile's velocity vector.
public boolean activeUnitCollision = PROJ_UNIT_COLLIDE_ON
//*
//* In order to prevent multiple responses of certain projectile events, the "state" of the projectile is
//* stored in a private-scope variable. The events that are included in this are:
//* • method onGround
//* • method onFinish
//*
private boolean priv_ground = false
private boolean priv_finish = false
//*
//* Other private status checks:
private boolean priv_follow = false
private boolean priv_unitfollow = false
//*
//* Projectile motion is dictated by 4 vectors: position, velocity, acceleration, and target-position. If
//* these values are manipulated they can be used to achieve certain effects.
//*
private s_vector3d posVec
private s_vector3d velVec
private s_vector3d accVec
private s_vector3d tarVec
//*
//* Projectiles are updated in a stack; and require certain data on-hand in order to properly update and
//* manage the stack.
//*
private integer priv_index
private static integer priv_stackDex = 0 //* The amount of projectiles currently in the stack.
//*
private static thistype array priv_stack
private static timer priv_stackLoop = CreateTimer()
private static constant real priv_stackLoopRef = PROJ_MOTION_REF
//*
private real priv_timeleft
public boolean toDestroy = PROJ_EXPIRE_ON
public boolean toKill = PROJ_TO_KILL_ON
//*
//* #######################################################################################################
//*
method operator collision takes nothing returns real
return .priv_collision
endmethod
method operator collision= takes real r returns nothing
if (r>PROJ_COLLISION_MAX) then
set r=PROJ_COLLISION_MAX
endif
set .priv_collision=r
endmethod
//*
//*
method setProjectileTargetPos takes real x, real y, real z returns boolean //* The projectile-target-pos can be manipulated many times.
local real xA = .posVec.x
local real yA = .posVec.y
if (.tarVec.x==x and .tarVec.y==y and .tarVec.z==z) then
return false
endif
set .tarVec.x = x
set .tarVec.y = y
set .tarVec.z = z
set xA = .posVec.x
set yA = .posVec.y
set xA = SquareRoot((x-xA)*(x-xA) + (y-yA)*(y-yA))
set yA = (SquareRoot(.velVec.x*.velVec.x + .velVec.y*.velVec.y)/thistype.priv_stackLoopRef)
set .priv_timeleft = xA/yA
set .priv_follow = true // This must be flagged to notify the "projectile"
return true // that the target has been changed.
endmethod
method setProjectileTarget takes unit who returns boolean //* The projectile-target can only be set once.
if (.target == null) then
set .target = who
set .priv_unitfollow = true
return true
endif
return false
endmethod
method setProjectileSource takes unit who returns boolean //* The projectile-source can only be set once.
if (.source == null) then
set .source = who
return true
endif
return false
endmethod
//*
//*
method onDestroy takes nothing returns nothing
set thistype.priv_stackDex = thistype.priv_stackDex - 1
set thistype.priv_stack[.priv_index] = thistype.priv_stack[thistype.priv_stackDex]
set thistype.priv_stack[.priv_index].priv_index = .priv_index
call .posVec.destroy()
call .velVec.destroy()
call .accVec.destroy()
call .tarVec.destroy()
if (.toKill) then
call KillUnit(.toUnit)
endif
if (thistype.priv_stackDex == 0) then
call PauseTimer(thistype.priv_stackLoop)
endif
endmethod
//*
//*
method doLaunch takes s_vector3d start, s_vector3d finish, real speed, real arc returns boolean
local real d
local real a
local real r
local unit u
if (start == 0) or (finish == 0) or (.active) then
// The launch method will return false under several circumstances. If start/finish vectors are
// null structs or if the projectile has already been launched.
return false
endif
// ----
// Setup the projectile vectors and static information;
set .posVec.x = start.x
set .posVec.y = start.y
set .posVec.z = start.z
set .tarVec.x = finish.x
set .tarVec.y = finish.y
set .tarVec.z = finish.z
set .speed = speed
set .arc = arc
// ----
//
set d = SquareRoot((finish.x-start.x)*(finish.x-start.x) + (finish.y-start.y)*(finish.y-start.y))
set a = Atan2(finish.y-start.y, finish.x-start.x)
// In the situation where the distance between start/finish is 0.00 or the speed of the projectile is
// 0, then it will result in an "instant" projectile.
if (d == 0.00) or (speed == 0.00) then
set .posVec.x = .tarVec.x
set .posVec.y = .tarVec.y
set .posVec.z = .tarVec.z
set .priv_timeleft = 0.00
else
set .priv_timeleft = d/speed
set r = thistype.priv_stackLoopRef
set .accVec.z = (-8*arc*speed*speed/d)
set .velVec.x = speed*Cos(a)
set .velVec.y = speed*Sin(a)
set .velVec.z = (-.accVec.z * (d/speed)/2 + (finish.z-start.z)/(d/speed))
set .accVec.z = .accVec.z *r*r
set .velVec.x = .velVec.x *r
set .velVec.y = .velVec.y *r
set .velVec.z = .velVec.z *r
endif
// ----
// The projectile's position should be updated immediately to match the new one.
set u = .toUnit
call SetUnitX(u, .posVec.x)
call SetUnitY(u, .posVec.y)
call MoveLocation(loc, .posVec.x, .posVec.y)
call SetUnitFlyHeight(u, .posVec.z-GetLocationZ(loc), 0)
set u = null
// ----
// Run the .onStart interface method and set .active to true so that it cannot be launched again.
call .onStart()
set .active = true
return true
endmethod
//*
//*
private static method doLoopEnum takes nothing returns boolean
local unit filt = GetFilterUnit()
local real xA = GetUnitX(filt)
local real yA = GetUnitY(filt)
local real zA
local real xB = projRef.posVec.x
local real yB = projRef.posVec.y
local real zB = projRef.posVec.z
call MoveLocation(loc, xA, yA)
set zA = GetUnitFlyHeight(filt)+GetLocationZ(loc)
if (((xA-xB)*(xA-xB) + (yA-yB)*(yA-yB) + (zA-zB)*(zA-zB)) <= projRef.collision*projRef.collision) then
call projRef.onUnitCollision(filt)
endif
set filt = null
return false
endmethod
private static method doLoop takes nothing returns nothing
local integer i = .priv_stackDex - 1
local thistype dat
local s_vector3d vA
local s_vector3d vB
local s_vector3d vC
local real x
local real y
local real z
local real x2
local real y2
local real z2
local real e
local unit u
loop
exitwhen (i < 0)
set dat = .priv_stack[i]
if (dat != 0) then
// ----
// If the projectile is active, the acceleration vector must be added to the velocity
// and the velocity added to the position. If the projectile is not active these vectors
// will have no roll in its motion.
//
if (dat.active) then
set e = dat.timescale
set vA = dat.velVec
set vB = dat.accVec
set vA.x = vA.x + vB.x *e*e
set vA.y = vA.y + vB.y *e*e
set vA.z = vA.z + vB.z *e*e
set vB = dat.posVec
set vB.x = vB.x + vA.x *e
set vB.y = vB.y + vA.y *e
set vB.z = vB.z + vA.z *e
else
set vA = dat.velVec
set vB = dat.posVec
endif
// ----
// In any situation, update the projectile's position with its position vector.
//
set u = dat.toUnit
set x = vB.x
set y = vB.y
set z = vB.z
call SetUnitX(u, x)
call SetUnitY(u, y)
call MoveLocation(loc, x, y)
call SetUnitFlyHeight(u, z-GetLocationZ(loc), 0)
// ----
// In addition to the position (which is automatically updated) a projectile can also have its
// facing rotation and pitch rotation updated so that they match the direction of the velocity.
// Each rotation has its own setting variable (see above declarations).
//
set x2 = vA.x
set y2 = vA.y
set z2 = vA.z
if (dat.active) and (dat.activePitch) and (vA.magnitude() > 0.00) then
// It is assumed in this feature that the model includes the same pitch animations as the one
// by Vexorian and iNfRaNe; it has been tweaked to be specific to -that- model.
call SetUnitAnimationByIndex(u, R2I(bj_RADTODEG*Atan2(z2, SquareRoot(x2*x2 + y2*y2)+0.5)+90))
endif
if (dat.active) and (dat.activeRotation) and (vA.magnitude() > 0.00) then
// Unlike pitch, this can be changed for any projectile.
call SetUnitFacingTimed(u, Atan2(y2, x2)*bj_RADTODEG, 0)
endif
// ----
// If the projectile's height is below the terrain-height, then run the .onGround method. This
// method should only run once until the projectile has surfaced above the terrain (won't recur).
//
if (z <= GetLocationZ(loc)) then
if not (dat.priv_ground) then
// Once we execute .onGround, it will flag .priv_ground which will disallow it from firing
// again until the projectile has surfaced above the terrain.
call dat.onGround()
set dat.priv_ground = true
endif
else
if (dat.priv_ground) then
set dat.priv_ground = false
endif
endif
// ----
// Projectiles will collide with units enumerated within a spherical area. Unit-collision-size is not
// factored into the enumeration.
//
if (dat.activeUnitCollision) then
set projRef = dat
call GroupEnumUnitsInRange(grp, x, y, dat.collision, Filter(function thistype.doLoopEnum))
// The group will not need to be cleared, since .doLoopEnum will always return false.
endif
// ----
// Target tracking (specifically, units) requires an "active" boolean config value in order for each
// specific projectile to allow tracking, that is, following a moving target.
//
// There are two variations to this, typically when you want to track a unit (which is common) and when
// you simply want to track the vector target (which could possibly be moved using setTargetPos().
//
if (dat.active) and (dat.activeTargetFollow) then
// If the projectile is active and has active-target-tracking, it will narrow down its filter to
// see whether or not the projectile is being tracked to a unit, and whether or not its target
// has been changed from the original position.
if (dat.priv_unitfollow) then
// Unit-follow means the target-vector will be updated to match the 3D position of the given
// unit-target.
set x = GetUnitX(dat.target)
set y = GetUnitY(dat.target)
call MoveLocation(loc, x, y)
set z = GetLocationZ(loc)+GetUnitFlyHeight(dat.target)
call dat.setProjectileTargetPos(x, y, z)
endif
if (dat.priv_follow) then
// If .priv_follow is active this simply means that the target has been moved. Since the
// active-target-tracking is enabled in this scope, we can allow the projectile to be adjusted
// so that it "homes" on the target.
set vC = dat.tarVec
set x = Atan2(vC.y-vB.y, vC.x-vB.x)
set y = SquareRoot((vC.x-vB.x)*(vC.x-vB.x) + (vC.y-vB.y)*(vC.y-vB.y))
set z = SquareRoot((vA.x*vA.x + vA.y*vA.y))
set z2 = vC.z-vB.z
set vA.x = z * Cos(x)
set vA.y = z * Sin(x)
set vA.z = z * Tan(Atan2(z2, y))
set dat.accVec.z = 0
endif
endif
// ----
// If the projectile's life-span expires then the system will automatically destroy the projectile based
// on whether or not the expire constant is on/off (can be changed per projectile).
//
if (dat.active) then
set dat.priv_timeleft = dat.priv_timeleft - .priv_stackLoopRef
if (dat.priv_timeleft <= 0.00) and not (dat.priv_finish) then
call dat.onFinish()
set dat.priv_finish = false
if (dat.toDestroy) then
call dat.destroy()
endif
endif
endif
endif
set i = i - 1
endloop
set u = null
endmethod
//*
//*
static method create takes unit u returns thistype
local thistype dat = thistype.allocate()
local real x = GetUnitX(u)
local real y = GetUnitY(u)
call UnitAddAbility(u, FLY_ABIL_ID)
call UnitRemoveAbility(u, FLY_ABIL_ID)
if (.priv_stackDex == 0) then
call TimerStart(.priv_stackLoop, .priv_stackLoopRef, true, function thistype.doLoop)
endif
call MoveLocation(loc, x, y)
set dat.toUnit = u
set dat.priv_index = .priv_stackDex
set dat.posVec = s_vector3d.createEx(x, y, GetLocationZ(loc)+GetUnitFlyHeight(u))
set dat.tarVec = s_vector3d.createEx(0, 0, 0)
set dat.velVec = s_vector3d.createEx(0, 0, 0)
set dat.accVec = s_vector3d.createEx(0, 0, 0)
set .priv_stack[.priv_stackDex] = dat
set .priv_stackDex = .priv_stackDex + 1
return dat
endmethod
//*
//*
//*********************************************************************************************************
endstruct
endlibrary
I'll include a test-map too where you can play around with it. It includes the vectors library that is required as well as an example of a struct that extends the projectile.
Attachments
Last edited: