- Joined
- Jan 21, 2006
- Messages
- 2,552
This is the code for a projectiles library that I've been working on. If anybody could go through the code and tell me if I'm doing anything seriously wrong, or where I could improve things, I would appreciate the feedback.
Here's the code to the ranged-projectiles structure that extends more to the user-end.
Now, here is the problem. When I have "projectile collision" and "unit collision" factored out, this system can handle quite a few projectiles at once. Once I put those two components into the equation, it will only support a handful. When I spammed the creation of these it actually ended up lagging ridiculously, and then telling me that it was unable to allocate an ID for my vectors (indicating that I've either leaked vectors, which I don't think I have, or something else).
JASS:
library Projectiles requires Vectors optional TimerUtils
globals
//**********************
//* Configuration
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯
//* Many default constants.
//*
//*
private constant integer FLY_ABIL_ID = 'Amrf' //* This is medihv's crow-form ability.
//*
public constant real COLLISION_DEFAULT = 50 //* This is the range at which units/projectiles will "collide".
public constant real COLLISION_MAX = 180 //* This is the maximum value for projectile collision. If 'COLLISION_DEFAULT'
//* is higher then this it will not over-ride it.
//*
public constant boolean EXPIRATION_DEFAULT = true //* The projectile's tendency to die once it has "finished". Setting this to false
//* should only be done if you are managing the projectiles yourself.
//*
private constant boolean hitDead = false //* Projectiles will "strike" dead units.
private constant boolean hitEnemy = true //* Projectiles will "strike" enemy units.
private constant boolean hitAllied = false //* Projectiles will "strike" allied units.
private constant boolean hitStructure = false //* Projectiles will "strike" structures.
//*
//*
//*****************************************
endglobals
interface projectileinterface
//*************
//* Projectile Interface
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
//* The following functions can be re-declared in child-structs and gain their logical functionality. For example, declaring
//* 'onUnitCollision' will allow you to customize the actions executed when the projectile comes into contact with a unit.
//*
//*
method onLoop takes nothing returns nothing defaults nothing //* Each iteration that updates the projectile.
method onFinish takes nothing returns nothing defaults nothing //* Once the projectile reaches the end of its path (unless otherwise specified).
method onBirth takes nothing returns nothing defaults nothing //* When the projectile starts its projection path.
method onUnitCollision takes unit u returns nothing defaults nothing //* When the projectile collides with a unit (u = collide with -unit-).
method onCollision takes projectileinterface p returns nothing defaults nothing //* When the projectile collides with another projectile (p = other projectile).
method onGround takes nothing returns nothing defaults nothing //* Projectile has struck the ground.
//*
//*
endinterface
struct projectile extends projectileinterface
//***************************
//* Projectiles
//* ¯¯¯¯¯¯¯¯¯¯¯
//* Data-structure that supports the creation/launch (as well as other functions) of in-game projectiles. The projectiles
//* are meant to mimic the basic ones as close as possible.
//*
//*
// Primary handle-base for the projectile.
readonly unit toUnit = null //* This unit is "projected" as if it were a projectile.
readonly real speed
readonly real arc
real collision = COLLISION_DEFAULT
real timescale = 1.00 //* Time-scale is at 100% by default.
// Active feature data
boolean activeUnitCollision = false //* The projectile can collide with units.
boolean activeCollision = false //* The projectile can collide with other projectiles.
private static location p_sampleLoc = Location(0, 0) //* Location used often for 'MoveLocatin' and 'GetLocationZ'
private static group p_sampleGroup = CreateGroup() //* Group used to enumerate nearby units.
private static thistype p_sampleProj //* Used to store 'this' when doing a group enum.
// These vectors store information including projectile motion.
private s_vector3d p_currentPos
private s_vector3d p_currentVelocity
private s_vector3d p_currentAcceleration
private s_vector3d p_currentTargetPos //* When the target-position vector is changed during flight, it will activate the projectile's
//* "homing" capabilities, which will eliminate 'arc' and move directly towards the target.
private real p_timeRemaining
// --------
// The following method operators allow the user more access to the projectile's vectors, without being able to
// do anything "illegal".
//
// Returns a COPY of the position vector.
method operator position takes nothing returns s_vector3d
return .p_currentPos.copy()
endmethod
// Returns a COPY of the velocity vector.
method operator velocity takes nothing returns s_vector3d
return .p_currentVelocity.copy()
endmethod
// Returns a COPY of the acceleration vector.
method operator acceleration takes nothing returns s_vector3d
return .p_currentAcceleration.copy()
endmethod
// Returns a COPY of the target-position vector.
method operator targetposition takes nothing returns s_vector3d
return .p_currentTargetPos.copy()
endmethod
//
// Destroy removes projectile from the stack, but it does not kill the unit.
method onDestroy takes nothing returns nothing
call RemoveSavedInteger(thistype.p_table, GetHandleId(.p_loop), 1)
call PauseTimer(.p_loop)
call DestroyTimer(.p_loop)
// Don't forget to destroy the vectors!
call .p_currentPos.destroy()
call .p_currentTargetPos.destroy()
call .p_currentVelocity.destroy()
call .p_currentAcceleration.destroy()
endmethod
// We don't want the projectile struct to leak so we need to remove it once its trajectory has completed.
method onFinish takes nothing returns nothing
call this.destroy()
endmethod
private static hashtable p_table = InitHashtable()
// In order to update the stack, we must use a timer. If the 'TimerUtils' library is available then it will use 'NewTimer' instead of 'CreateTimer'.
private static constant real p_loopPeriod = 0.03 //* Rate that the timer will update the stack. Increasing it will decrease the performance.
static if (LIBRARY_TimerUtils) then
private timer p_loop = NewTimer() //* Uses TimerUtils. The timer is never destroyed so its not that important, I just wanted to
else //* use the 'optional' keyword and try out static if statements.
private timer p_loop = CreateTimer() //* This does not.
endif
boolean toDestroy = EXPIRATION_DEFAULT //* If this is 'false' then the projectile will not run .onFinish() and its remaining time
//* will not decrease.
boolean hitDead = hitDead //* Dead units are valid targets.
boolean hitEnemy = hitEnemy //* Enemy units are valid targets.
boolean hitAllied = hitAllied
boolean hitStructure = hitStructure
static method onLoopUnitEnum takes nothing returns boolean
local thistype this = .p_sampleProj
local unit filt = GetFilterUnit()
local integer c = 0
local real x
local real y
local real z
set x = GetUnitX(filt)-this.p_currentPos.x
set y = GetUnitY(filt)-this.p_currentPos.y
call MoveLocation(.p_sampleLoc, x, y)
set z = GetLocationZ(.p_sampleLoc)+GetUnitFlyHeight(filt) - this.p_currentPos.z
if not (IsUnitInRange(this.toUnit, filt, this.collision) and (SquareRoot(x*x + y*y + z*z) <= this.collision)) then
return false
endif
// .onUnitCollision is only called if a valid target is struck.
if (hitDead == (IsUnitType(filt, UNIT_TYPE_DEAD) and GetUnitTypeId(filt)==0)) then
set c=c+1
endif
if (hitEnemy == IsUnitEnemy(filt, GetOwningPlayer(this.toUnit))) then
set c=c+1
endif
if (hitAllied == IsUnitAlly(filt, GetOwningPlayer(this.toUnit))) then
set c=c+1
endif
if (hitStructure == IsUnitType(filt, UNIT_TYPE_STRUCTURE)) then
set c=c+1
endif
if (c==4) then
call this.onUnitCollision(filt)
endif
return false
endmethod
// Updates stack .p_stack on a periodic timer.
static method onLoopStatic takes nothing returns nothing
local integer j
local thistype this
local s_vector3d vec
set this = LoadInteger(.p_table, GetHandleId(GetExpiredTimer()), 1)
// Scale the added acceleration based on the time-scale of the projectile.
set this.p_currentVelocity.x = this.p_currentVelocity.x+(this.p_currentAcceleration.x*this.timescale*this.timescale)
set this.p_currentVelocity.y = this.p_currentVelocity.y+(this.p_currentAcceleration.y*this.timescale*this.timescale)
set this.p_currentVelocity.z = this.p_currentVelocity.z+(this.p_currentAcceleration.z*this.timescale*this.timescale)
// Scale the added velocity (to position) based on time-scale.
set this.p_currentPos.x = this.p_currentPos.x+(this.p_currentVelocity.x*this.timescale)
set this.p_currentPos.y = this.p_currentPos.y+(this.p_currentVelocity.y*this.timescale)
set this.p_currentPos.z = this.p_currentPos.z+(this.p_currentVelocity.z*this.timescale)
// Update the projectile.
call SetUnitX(this.toUnit, this.p_currentPos.x)
call SetUnitY(this.toUnit, this.p_currentPos.y)
call MoveLocation(.p_sampleLoc, this.p_currentPos.x, this.p_currentPos.y)
call SetUnitFlyHeight(this.toUnit, this.p_currentPos.z-GetLocationZ(.p_sampleLoc), 0)
// .onLoop should be executed after the projectile has been updated but before any of the other user methods.
call this.onLoop()
// Since 'doSync' was just called, .p_sampeLoc will still have the correct coordinates.
if (this.p_currentPos.z <= GetLocationZ(.p_sampleLoc)) then
// Indicates that the projectile has struck the ground.
call this.onGround()
endif
// Projectiles will only collide with projectiles if .activeCollision is true (active).
if (this.activeCollision) then
//set j = 0
//loop
// exitwhen (j == .p_stackTrack)
// if not (.p_stack[j] == this) then
// set vec = .p_stack[j].p_currentPos.copy()
// call vec.sub(this.p_currentPos)
// if (vec.magnitude() < this.collision) then
// // Indicates that a projectile has come within collision-range of another projectile.
// call this.onCollision(.p_stack[j])
// endif
// call vec.destroy()
// endif
// set j = j + 1
//endloop
endif
if (this.toDestroy) then
set this.p_timeRemaining = this.p_timeRemaining - .p_loopPeriod
if (this.p_timeRemaining <= 0.00) then
// Indicates the projectile's trajectory is complete.
call this.onFinish()
endif
endif
// Projectiles will only collide with units if .activeUnitCollision is true.
if (this.activeUnitCollision) then
// Enumerate nearby units so that they can be "detected". Store 'this' in global variable for referencing in enum response.
set .p_sampleProj = this
call GroupEnumUnitsInRange(.p_sampleGroup, this.p_currentPos.x, this.p_currentPos.y, COLLISION_MAX, Filter(function thistype.onLoopUnitEnum))
endif
endmethod
// Launches the projectile with provided input. If the projectile cannot be created with specified input then this
// method will return false.
method doLaunch takes s_vector3d start, s_vector3d target, real speed, real pitchArc returns boolean
local real d
local real a
if (start==0) or (target==0) then
return false
endif
// Initialize vectors and specific info.
set .p_currentPos = start.copy()
set .p_currentTargetPos = target.copy()
set .speed = speed //* Speed/arc are never used internally, but they are there for user reference in any
set .arc = pitchArc //* case.
call SaveInteger(thistype.p_table, GetHandleId(.p_loop), 1, this)
call TimerStart(.p_loop, thistype.p_loopPeriod, true, function thistype.onLoopStatic)
set d = SquareRoot((start.x-target.x)*(start.x-target.x) + (start.y-target.y)*(start.y-target.y)) //* This is a 2D distance.
set a = Atan2(target.y-start.y, target.x-start.x)
if (d==0.0) or (speed==0) then
set .p_currentPos.x = .p_currentTargetPos.x
set .p_currentPos.y = .p_currentTargetPos.y
set .p_currentPos.z = .p_currentTargetPos.z
set .p_timeRemaining = 0.0
// This indicates the projectile will be instant.
set .p_currentAcceleration = s_vector3d.createEx(0, 0, 0)
set .p_currentVelocity = s_vector3d.createEx(0, 0, 0)
// The projectile should be destroyed immediately (ignoring the inaccuracy of the timer).
else
set .p_timeRemaining = d/speed
set .p_currentAcceleration = s_vector3d.createEx(0, 0, -8*pitchArc*speed*speed/d)
set .p_currentVelocity = s_vector3d.createEx(speed*Cos(a), speed*Sin(a), -.p_currentAcceleration.z * (d/speed)/2 + (target.z-start.z)/(d/speed))
endif
// Scale projectile vectors
set .p_currentAcceleration.x=.p_currentAcceleration.x*thistype.p_loopPeriod*thistype.p_loopPeriod
set .p_currentAcceleration.y=.p_currentAcceleration.y*thistype.p_loopPeriod*thistype.p_loopPeriod
set .p_currentAcceleration.z=.p_currentAcceleration.z*thistype.p_loopPeriod*thistype.p_loopPeriod
set .p_currentVelocity.x=.p_currentVelocity.x*thistype.p_loopPeriod
set .p_currentVelocity.y=.p_currentVelocity.y*thistype.p_loopPeriod
set .p_currentVelocity.z=.p_currentVelocity.z*thistype.p_loopPeriod
// Sync projectile and call .onBirth user-method. If a projectile is "instant" it will not be destroyed until the first
// timer-loop. This is a maximum inaccuracy of the timer's loop timeout.
call SetUnitX(.toUnit, .p_currentPos.x)
call SetUnitY(.toUnit, .p_currentPos.y)
call MoveLocation(thistype.p_sampleLoc, .p_currentPos.x, .p_currentPos.y)
call SetUnitFlyHeight(.toUnit, .p_currentPos.z-GetLocationZ(thistype.p_sampleLoc), 0)
call .onBirth()
return true
endmethod
// Creates a projectile based on a unit.
static method create takes unit u returns thistype
local thistype proj = thistype.allocate()
set proj.toUnit = u
call UnitAddAbility(u, FLY_ABIL_ID)
call UnitRemoveAbility(u, FLY_ABIL_ID)
return proj
endmethod
endstruct
endlibrary
Here's the code to the ranged-projectiles structure that extends more to the user-end.
JASS:
library Ranged requires Projectiles
globals
//*****************
//* Configuration
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯
//*
//*
//* The unit used to create ranged projectiles. The dummy model I am using is Vexorian's pitch animation model, so I can
//* simulate projectile "tilt".
public constant integer DUMMY_ID = 'dumy'
//*
//*
//****************************
endglobals
struct rangedprojectile extends projectile
//********************
//* Ranged Projectile
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
//* A structure with methods that allows the user to easily create missiles that look/act exactly like WarCraft III missiles.
//*
//*
readonly unit source = null //* Source from which the projectile came.
readonly effect model = null //* The effect that represents the projectile's "model".
readonly string modelpath
private static location p_sampleLoc = Location(0, 0) //* Used to optimize GetLocationZ
method onDestroy takes nothing returns nothing
call KillUnit(.toUnit)
if (.model!=null) then
call DestroyEffect(.model)
endif
endmethod
// Each time the projectile is updated on the timer-loop.
method onLoop takes nothing returns nothing
local s_vector3d vec = .velocity
call SetUnitFacingTimed(.toUnit, Atan2(vec.y, vec.x)*bj_RADTODEG, 0)
call SetUnitAnimationByIndex(.toUnit, R2I(bj_RADTODEG*Atan2(vec.z, SquareRoot(vec.x*vec.x + vec.y*vec.y)+0.5)+90))
// Since we are only given a copy of the velocity, we must destroy it.
call vec.destroy()
endmethod
// Easy-to-use constructor method.
static method create takes unit source, real heightoff, real targx, real targy, real speed, real pitchArc, string modelpath returns thistype
local thistype r
local unit u
local real x
local real y
local real theta
local s_vector3d vec
local s_vector3d vecb
set x = GetUnitX(source)
set y = GetUnitY(source)
set theta = Atan2(targy-y, targx-x)
set u = CreateUnit(GetOwningPlayer(source), DUMMY_ID, x, y, theta*bj_RADTODEG)
set r = thistype.allocate(u)
set r.source = source
set r.modelpath = modelpath
set r.model = AddSpecialEffectTarget(modelpath, u, "origin")
call MoveLocation(thistype.p_sampleLoc, x, y)
set vec = s_vector3d.createEx(x, y, GetLocationZ(thistype.p_sampleLoc)+GetUnitFlyHeight(source)+heightoff)
call MoveLocation(thistype.p_sampleLoc, targx, targy)
set vecb = s_vector3d.createEx(targx, targy, GetLocationZ(thistype.p_sampleLoc))
call r.doLaunch(vec, vecb, speed, pitchArc)
call vec.destroy()
call vecb.destroy()
return r
endmethod
endstruct
endlibrary
Now, here is the problem. When I have "projectile collision" and "unit collision" factored out, this system can handle quite a few projectiles at once. Once I put those two components into the equation, it will only support a handful. When I spammed the creation of these it actually ended up lagging ridiculously, and then telling me that it was unable to allocate an ID for my vectors (indicating that I've either leaked vectors, which I don't think I have, or something else).
Last edited: