- Joined
- May 9, 2014
- Messages
- 1,823
Hello everyone.
Looking at the recent versions of Warcraft 3, I had felt the need to recreate Missile systems in order to use special effects directly. However, as it is currently in development, not many features have been developed yet, and my aim for this is to keep it as lightweight as possible, both in importation and resource usage.
I want to make this missile library as collaborative as possible, thus my decision in posting it here in the Lab. I also want to implement some functionalities that may arise from suggestions or feedbacks.
Dependencies:
Main library:
For those who read up until the end, congratulations, you are an excellent reader. And for those of you who just want to enjoy the write-up, I wish a Happy New Year to you.
Looking at the recent versions of Warcraft 3, I had felt the need to recreate Missile systems in order to use special effects directly. However, as it is currently in development, not many features have been developed yet, and my aim for this is to keep it as lightweight as possible, both in importation and resource usage.
I want to make this missile library as collaborative as possible, thus my decision in posting it here in the Lab. I also want to implement some functionalities that may arise from suggestions or feedbacks.
Dependencies:
JASS:
library EffectUtils requires /*
-----------
*/ Alloc /*
-----------
- Sevion
- link: https://www.hiveworkshop.com/threads/snippet-alloc.192348/
(Note: The Alloc library supplied here is a modified version that bridges throughout
the previous versions of warcraft 3 and the most recent versions.)
--------------------------
*/ optional GameVersion /*
--------------------------
- TriggerHappy
- link: https://www.hiveworkshop.com/threads/detect-game-version-getpatchlevel.308204/
--------------------------------------------------------------------
-----------------------
| EffectUtils |
-----------------------
- A structified version of the native effect handle.
-----------------------
| Author notes: |
-----------------------
- A game version detector is used in the case that the
patch version is at least 1.30+ (where the native
BlzGetLocalUnitZ will work). However, this is optional
and one can manually
--------------------------------------------------------------------
API:
--------------------------------------------------------------------
- struct Effect
--------------------------------------------------------
Core methods:
- method addXY(string mdlPath, real x, real y) -> Effect
- Acts like AddSpecialEffect()
- method addTarget(string mdlPath, widget target, string attach) -> Effect
- Acts like AddSpecialEffectTarget()
- method destroy()
- Destroys an Effect instance immediately.
- method destroyTimed(real dur)
- Destroys an Effect instance after dur seconds.
- Note that calling destroy after this will not work.
--------------------------------------------------------
Library Extension methods:
- static method getPairZ(real x, real y) -> real
- static method getGroundZ(real x, real y) -> real
- Returns the z-coordinate of a given point.
--------------------------------------------------------
Functionality methods:
- method setColorByChar(char colorCode)
- Modifies the vertex color of an Effect instance.
- The parameter type is integer, but it is recommended
to pass hexadecimal values for readability.
- The color code is interpreted as follows:
(RRGGBBAA)
- method setRed(integer value)
- method setGreen(integer value)
- method setBlue(integer value)
- method setAlpha(integer value)
- Modifies the vertex color of an Effect instance.
- Effect is singular, and may waste performance
if used to modify all four directly.
- Integer values are recommended (from 0 - 255)
- method setX(real x)
- method setY(real y)
- method setZ(real z)
- Sets the respective coordinates of the Effect instance.
- method setYaw(real yaw)
- method setPitch(real pitch)
- method setRoll(real roll)
- Modifies the orientation of the model of the Effect instance.
- Might be tricky to work with.
Note: All setter methods have a respective getter method
with no parameters.
- method getModelPath() -> string
- Returns the model path of the Effect instance.
- method getAttachmentPoint() -> string
- Returns the attachment point of the Effect instance.
--------------------------------------------------------
--------------------------------------------------------------------
*/
globals
// Will determine whether version detection will be handled by the script
// in interpreting which object to use when getting the z-coordinate of a
// certain point. (Either a unit or a location handle).
// Set this to false if you are sure of the game version you are using
// for development.
private constant boolean AUTOMATIC_VERSION_DETECTION = true
endglobals
static if DEBUG_MODE then
private function Error takes string msg, string funcName, boolean isError returns nothing
call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 50000, msg)
if isError then
call PauseGame(true)
endif
endfunction
endif
static if not LIBRARY_GameVersion then
private struct LibraryFlags extends array
static constant boolean IS_PATCH_130_OR_GREATER = false
endstruct
endif
private module Init
private static method onInit takes nothing returns nothing
call thistype.init()
endmethod
endmodule
private module EffectMethods
private static method setOrientation takes effect fx, real yaw, real pitch, real roll returns nothing
call BlzSetSpecialEffectOrientation(fx, roll, pitch, yaw)
endmethod
method getRed takes nothing returns integer
return this.red
endmethod
method getGreen takes nothing returns integer
return this.green
endmethod
method getBlue takes nothing returns integer
return this.blue
endmethod
method getAlpha takes nothing returns integer
return this.alpha
endmethod
method getX takes nothing returns real
return BlzGetLocalSpecialEffectX(this.fx)
endmethod
method getY takes nothing returns real
return BlzGetLocalSpecialEffectY(this.fx)
endmethod
method getZ takes nothing returns real
return BlzGetLocalSpecialEffectZ(this.fx)
endmethod
method getHeight takes nothing returns real
return this.getZ() - thistype.getPairZ(this.getX(), this.getY())
endmethod
method getYaw takes nothing returns real
return this.yaw
endmethod
method getFacing takes nothing returns real
return this.yaw*bj_RADTODEG
endmethod
method getPitch takes nothing returns real
return this.pitch
endmethod
method getRoll takes nothing returns real
return this.roll
endmethod
method getModelPath takes nothing returns string
return this.model
endmethod
method getAttachmentPoint takes nothing returns string
return this.orient
endmethod
method setColorByChar takes integer hex returns nothing
local integer array color
if hex < 0 then
set color[0] = color[0] + 0x80
set hex = hex - 0x80000000
endif
set color[0] = color[0] + ModuloInteger(hex/0x1000000, 0x100)
set color[1] = color[1] + ModuloInteger(hex/0x10000, 0x100)
set color[2] = color[2] + ModuloInteger(hex/0x100, 0x100)
set color[3] = color[3] + ModuloInteger(hex, 0x100)
set this.red = color[0]
set this.green = color[1]
set this.blue = color[2]
set this.alpha = color[3]
call BlzSetSpecialEffectColor(this.fx, color[0], color[1], color[2])
call BlzSetSpecialEffectAlpha(this.fx, color[3])
endmethod
method setRed takes integer newRed returns nothing
set this.red = newRed
call BlzSetSpecialEffectColor(this.fx, newRed, this.green, this.blue)
endmethod
method setGreen takes integer newGreen returns nothing
set this.green = newGreen
call BlzSetSpecialEffectColor(this.fx, this.red, newGreen, this.blue)
endmethod
method setBlue takes integer newBlue returns nothing
set this.blue = newBlue
call BlzSetSpecialEffectColor(this.fx, this.red, this.green, newBlue)
endmethod
method setAlpha takes integer newAlpha returns nothing
set this.alpha = newAlpha
call BlzSetSpecialEffectAlpha(this.fx, newAlpha)
endmethod
method setX takes real newX returns nothing
call BlzSetSpecialEffectPosition(this.fx, newX, this.getY(), this.getZ())
endmethod
method setY takes real newY returns nothing
call BlzSetSpecialEffectPosition(this.fx, this.getX(), newY, this.getZ())
endmethod
method setZ takes real newZ returns nothing
call BlzSetSpecialEffectPosition(this.fx, this.getX(), this.getY(), newZ)
endmethod
method setYaw takes real newYaw returns nothing
set this.yaw = ModuloReal(newYaw, 2*bj_PI)
call thistype.setOrientation(this.fx, this.yaw, -this.pitch, this.roll)
endmethod
method setFacing takes real newFacing returns nothing
set this.yaw = ModuloReal(newFacing*bj_DEGTORAD, 2*bj_PI)
call thistype.setOrientation(this.fx, this.yaw, -this.pitch, this.roll)
endmethod
method setPitch takes real newPitch returns nothing
set this.pitch = ModuloReal(newPitch, 2*bj_PI)
call thistype.setOrientation(this.fx, this.yaw, -this.pitch, this.roll)
endmethod
method setRoll takes real newRoll returns nothing
set this.roll = ModuloReal(newRoll, 2*bj_PI)
call thistype.setOrientation(this.fx, this.yaw, -this.pitch, this.roll)
endmethod
endmodule
struct Effect extends array
implement Alloc
private static timer tm = null
private static unit zGetter = null
private static location zGetterLoc = null
private static Table timerMap = 0
private effect fx
private string model
private string orient
private real yaw
private real pitch
private real roll
private integer red
private integer green
private integer blue
private integer alpha
static method getPairZ takes real x, real y returns real
local boolean flag = false
static if LIBRARY_GameVersion and AUTOMATIC_VERSION_DETECTION then
set flag = (GetPatchLevel() <= PATCH_LEVEL_129)
else
set flag = not LibraryFlags.IS_PATCH_130_OR_GREATER
endif
if flag then
call MoveLocation(Effect.zGetterLoc, x, y)
return GetLocationZ(Effect.zGetterLoc)
endif
call SetUnitX(Effect.zGetter, x)
call SetUnitY(Effect.zGetter, y)
return BlzGetLocalUnitZ(Effect.zGetter)
endmethod
static method getGroundZ takes real x, real y returns real
return Effect.getPairZ(x, y)
endmethod
implement EffectMethods
method destroy takes nothing returns nothing
if this.fx != null then
if Effect.timerMap.timer.has(this) then
debug call Error("Cannot destroy timed effect manually.", "Effect.destroy", true)
else
call DestroyEffect(this.fx)
set this.fx = null
set this.model = null
set this.orient = null
set this.yaw = 0
set this.pitch = 0
set this.roll = 0
call this.deallocate()
endif
endif
endmethod
private static method create takes string filePath returns Effect
local Effect result = Effect.allocate()
set result.model = filePath
return result
endmethod
private static method onDestroyCallback takes nothing returns nothing
local Effect object
set Effect.tm = GetExpiredTimer()
set object = Effect(Effect.timerMap.integer[-GetHandleId(Effect.tm)])
call Effect.timerMap.timer.remove(object)
call Effect.timerMap.remove(-GetHandleId(Effect.tm))
call DestroyTimer(Effect.tm)
call object.destroy()
endmethod
method destroyTimed takes real dur returns nothing
// If the effect has already been destroyed, return.
if this.fx == null then
return
endif
// If the effect object is to be destroyed later on.
if not Effect.timerMap.timer.has(this) then
set Effect.tm = CreateTimer()
set Effect.timerMap.timer[this] = Effect.tm
set Effect.timerMap[-GetHandleId(Effect.tm)] = this
call TimerStart(Effect.tm, dur, false, function Effect.onDestroyCallback)
else
set Effect.tm = Effect.timerMap.timer[this]
if TimerGetRemaining(Effect.tm) < dur then
call PauseTimer(Effect.tm)
call TimerStart(Effect.tm, dur, false, function Effect.onDestroyCallback)
endif
endif
endmethod
static method addXY takes string filePath, real x, real y returns Effect
local Effect object = Effect.create(filePath)
set object.fx = AddSpecialEffect(filePath, x, y)
return object
endmethod
static method addTarget takes string filePath, widget targ, string orient returns Effect
local Effect object = Effect.create(filePath)
set object.fx = AddSpecialEffectTarget(filePath, targ, orient)
set object.orient = orient
return object
endmethod
private static method init takes nothing returns nothing
set Effect.timerMap = Table.create()
set Effect.zGetter = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), 'hfoo', 0, 0, 0)
set Effect.zGetterLoc = Location(0, 0)
call ShowUnit(Effect.zGetter, false)
call PauseUnit(Effect.zGetter, true)
call SetUnitInvulnerable(Effect.zGetter, true)
endmethod
implement Init
endstruct
endlibrary
Main library:
JASS:
library Missile requires /*
----------------------
*/ EffectUtils /*
----------------------
# Alloc
# Table
*/
private module Init
private static method onInit takes nothing returns nothing
call thistype.init()
endmethod
endmodule
struct Missile extends array
implement Alloc
private static constant real INTERVAL = 1/32.
private static timer tm = null
private static code missileCallback = null
private boolean hasHit
private boolean destroyWasInvoked
private boolean isLaunched
private thistype next
private thistype prev
/*
----------------
| |
| Missile mems |
| |
----------------
*/
private Effect fx
private trigger onHit
private integer moveType
private real moveSpeed
private real facingSpeed
private real initialHeight
private real initialDist
private real maxHeight
private real zSpeed
private real zRate
private boolean hasBounced
readonly boolean isArced
readonly real targX
readonly real targY
readonly real targHeight
readonly widget target
readonly unit targetUnit
boolean destroyOnDeath
boolean destroyOnRemove
integer data
method getSpeed takes nothing returns real
return this.moveSpeed/thistype.INTERVAL
endmethod
method getX takes nothing returns real
return this.fx.getX()
endmethod
method getY takes nothing returns real
return this.fx.getY()
endmethod
method setSpeed takes real speed returns thistype
set this.moveSpeed = speed*thistype.INTERVAL
return this
endmethod
method setFacingSpeed takes real facing returns thistype
set this.facingSpeed = RMinBJ(facing * bj_DEGTORAD, 2*bj_PI)
return this
endmethod
method setTargXY takes real x, real y returns thistype
if not this.isLaunched then
set this.moveType = 1
set this.targX = x
set this.targY = y
set this.targHeight = 0.
endif
return this
endmethod
method setTarget takes widget targ returns thistype
set this.moveType = 2
set this.isArced = false
set this.target = targ
set this.targX = GetWidgetX(targ)
set this.targY = GetWidgetY(targ)
set this.targetUnit = null
return this
endmethod
method setTargetUnit takes unit targUnit returns thistype
call this.setTarget(targUnit)
set this.targetUnit = targUnit
set this.targHeight = GetUnitFlyHeight(targUnit)
return this
endmethod
method setXY takes real x, real y returns thistype
call this.fx.setX(x)
call this.fx.setY(y)
return this
endmethod
method onHitEvent takes code func returns thistype
if this.onHit != null then
call DestroyTrigger(this.onHit)
endif
set this.onHit = CreateTrigger()
call TriggerAddCondition(this.onHit, Condition(func))
return this
endmethod
method setTargHeight takes real newHeight returns thistype
local real dist
if (this.moveType == 1) then
set this.targHeight = RMaxBJ(newHeight, 0)
if not this.isArced then
set dist = (this.fx.getX() - this.targX)*(this.fx.getX() - this.targX) + (this.fx.getY() - this.targY)*(this.fx.getY() - this.targY)
set dist = SquareRoot(dist)
set this.zSpeed = (this.targHeight - this.initialHeight)/dist
endif
endif
return this
endmethod
method applyArc takes real initHeight, real maxHeight returns thistype
local real h
local real a
local real dist
local real x1 = this.fx.getX()
local real x2 = this.targX
local real y1 = this.fx.getY()
local real y2 = this.targY
local real dist2
local real y3
local real dy
if (this.moveType == 1) then
set dist2 = (x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)
set dist = SquareRoot(dist2)
call this.fx.setZ(Effect.getGroundZ(this.fx.getX(), this.fx.getY()) + initHeight)
set this.initialHeight = this.fx.getHeight()
if not this.isArced then
set this.isArced = true
set this.initialDist = dist
elseif this.hasBounced then
set maxHeight = maxHeight * (dist/this.initialDist)
endif
set maxHeight = RMaxBJ(maxHeight, RMaxBJ(this.initialHeight, this.targHeight))
set this.maxHeight = maxHeight
if not (this.initialHeight != this.targHeight) then
set h = dist/2
set a = (this.initialHeight - maxHeight)/((-h)*(-h))
else
set dy = this.initialHeight - maxHeight
set y3 = this.initialHeight - this.targHeight
set h = (((dy)*dist) + SquareRoot((dy*dy*dist2)-(y3)*(dy*dist2)))/(y3)
set a = (y3)/(2*h*dist -dist2)
/*
call BJDebugMsg("Distance (x2): " + R2S(dist))
call BJDebugMsg("Initial height (y1): " + R2S(this.initialHeight))
call BJDebugMsg("Target height (y2): " + R2S(this.targHeight))
call BJDebugMsg("h -> " + R2S(h))
call BJDebugMsg("a -> " + R2S(a))
call BJDebugMsg("k -> " + R2S(maxHeight) + "\n\n")
*/
endif
set this.zRate = 2*a
set this.zSpeed = this.zRate*(-h)
endif
return this
endmethod
// Launch does not allow chained calls.
method launch takes real x, real y, boolean faceImmediately returns nothing
local real dist
if this.moveType <= 0 then
// The instance has either been destroyed, or has not been mapped out yet
return
elseif this.isLaunched then
// The instance has been included in the list of moving projectiles.
return
endif
call this.setXY(x, y)
if faceImmediately then
call this.fx.setFacing(Atan2(this.targY - y, this.targX - x))
endif
set this.initialHeight = this.fx.getHeight()
set this.next = 0
set this.prev = this.next.prev
set this.next.prev = this
set this.prev.next = this
set this.isLaunched = true
set dist = SquareRoot((x - this.targX)*(x - this.targX) + (y - this.targY)*(y - this.targY))
set this.zSpeed = (this.targHeight - this.initialHeight)/dist
if thistype(0).next != 0 then
call TimerStart(thistype.tm, thistype.INTERVAL, true, thistype.missileCallback)
endif
endmethod
method destroy takes nothing returns nothing
// Destroying a missile while the onHit event is being processed is considered invalid.
// However, any data associated with the Missile instance is nullified.
if this.hasHit then
set this.destroyWasInvoked = true
set this.data = 0
debug call BJDebugMsg("Attempted to destroy Missile instance ("+I2S(this)+") during an onHit event!")
return
endif
// Perhaps the missile was launched, but has not hit anything yet
if this.isLaunched then
set this.next.prev = this.prev
set this.prev.next = this.next
set this.isLaunched = false
debug call BJDebugMsg("Missile: User has called this function.")
endif
call DestroyTrigger(this.onHit)
call this.fx.destroy()
set this.onHit = null
set this.target = null
set this.targetUnit = null
set this.fx = 0
set this.data = 0
set this.moveSpeed = 0.
set this.facingSpeed = 0.
set this.targX = 0.
set this.targY = 0.
set this.destroyOnDeath = false
set this.destroyOnRemove = false
set this.destroyWasInvoked = false
call this.deallocate()
endmethod
static method create takes string mdlFile returns thistype
local thistype this = thistype.allocate()
set this.fx = Effect.addXY(mdlFile, 0, 0)
set this.moveType = -1
return this
endmethod
readonly static thistype current = 0
readonly static real array rVar
/*
Handler functions are processed here.
*/
private method onHitCallback takes nothing returns nothing
set this.hasHit = true
call TriggerEvaluate(this.onHit)
set this.hasHit = false
endmethod
private static method applyZMovement takes nothing returns nothing
set rVar[13] = Effect.getGroundZ(current.fx.getX(), current.fx.getY())
set rVar[6] = current.fx.getZ()
set rVar[7] = RMaxBJ(rVar[6] + current.zSpeed*current.moveSpeed, rVar[13])
if current.isArced then
set current.zSpeed = current.zSpeed + (current.zRate*current.moveSpeed)
endif
set rVar[8] = Atan2((rVar[7] - rVar[6])*Cos(rVar[5]), current.moveSpeed)
if (current.facingSpeed*bj_RADTODEG != 0.) then
set rVar[12] = (rVar[5] - rVar[11])*bj_RADTODEG
//call BJDebugMsg("Difference of angles: " + R2S(rVar[12]))
set rVar[9] = RMinBJ(Atan2(rVar[5] - rVar[11], current.facingSpeed), bj_PI/2)
endif
//call BJDebugMsg("Radian value for pitch: " + R2S(rVar[8]))
//call BJDebugMsg("Radian value for roll: " + R2S(rVar[9]))
call current.fx.setZ(rVar[7])
call current.fx.setPitch(rVar[8])
call current.fx.setRoll(rVar[9])
if current.isArced then
if rVar[7] <= rVar[13] then
set current.hasBounced = true
call current.applyArc(current.fx.getHeight(), current.maxHeight)
set current.hasBounced = false
endif
// call BJDebugMsg("Current fx's ("+I2S(current)+") z-speed: " + R2S(current.zSpeed))
endif
endmethod
private static method retrieveFacing takes nothing returns nothing
if current.facingSpeed <= 0 then
// The facing speed is instantaneous
set rVar[5] = rVar[3]
else
// The facing speed is not instantaneous, simulate rotation.
set rVar[3] = ModuloReal(rVar[3], 2*bj_PI)
set rVar[5] = current.fx.getYaw()
set rVar[6] = rVar[3] + 2*bj_PI
set rVar[9] = rVar[3] - 2*bj_PI
set rVar[7] = RAbsBJ(rVar[6] - rVar[5])
set rVar[8] = RAbsBJ(rVar[3] - rVar[5])
set rVar[10]= RAbsBJ(rVar[9] - rVar[5])
if rVar[7] < rVar[8] then
set rVar[11] = rVar[6]
elseif rVar[8] < rVar[10] then
set rVar[11] = rVar[3]
else
set rVar[11] = rVar[9]
endif
if RAbsBJ(rVar[11] - rVar[5]) <= current.facingSpeed*thistype.INTERVAL then
set rVar[5] = rVar[11]
else
if rVar[11] > rVar[5] then
set rVar[5] = rVar[5] + current.facingSpeed*thistype.INTERVAL
else
set rVar[5] = rVar[5] - current.facingSpeed*thistype.INTERVAL
endif
endif
endif
set rVar[11] = current.fx.getYaw()
endmethod
private static method onMissileUpdate takes nothing returns nothing
set rVar[0] = 0.
set current = thistype(0).next
loop
exitwhen current == 0
if current.moveType == 1 then
set rVar[1] = current.fx.getX()
set rVar[2] = current.fx.getY()
set rVar[3] = Atan2(current.targY - rVar[2], current.targX - rVar[1])
set rVar[4] = (rVar[1] - current.targX)*(rVar[1] - current.targX) + (rVar[2] - current.targY)*(rVar[2] - current.targY)
call thistype.retrieveFacing()
call current.fx.setFacing(rVar[5])
if rVar[4] > current.moveSpeed*current.moveSpeed then
call current.fx.setX(rVar[1] + current.moveSpeed*Cos(rVar[5]))
call current.fx.setY(rVar[2] + current.moveSpeed*Sin(rVar[5]))
call thistype.applyZMovement()
else
call current.fx.setX(current.targX)
call current.fx.setY(current.targY)
call thistype.applyZMovement()
set current.moveType = -1
call current.onHitCallback()
// Either the instance was destroyed, or it wasn't included in the move list any longer
if current.moveType <= 0 then
set current.isLaunched = false
set current.next.prev = current.prev
set current.prev.next = current.next
if current.destroyWasInvoked then
call current.destroy()
set rVar[0] = rVar[0] - 1
endif
endif
endif
elseif current.moveType == 2 then
set rVar[1] = current.fx.getX()
set rVar[2] = current.fx.getY()
//
if GetHandleId(current.target) != 0 then
set current.targX = GetWidgetX(current.target)
set current.targY = GetWidgetY(current.target)
else
set current.moveType = 1
set current.target = null
if current.targetUnit != null and (current.destroyOnRemove or (current.destroyOnDeath and (GetWidgetLife(current.target) <= 0.))) then
call current.destroy()
set rVar[0] = rVar[0] - 1
else
set rVar[3] = Atan2(current.targY - rVar[2], current.targX - rVar[1])
set rVar[4] = (rVar[1] - current.targX)*(rVar[1] - current.targX) + (rVar[2] - current.targY)*(rVar[2] - current.targY)
call thistype.retrieveFacing()
call current.fx.setFacing(rVar[5])
if rVar[4] > current.moveSpeed*current.moveSpeed then
call current.fx.setX(rVar[1] + current.moveSpeed*Cos(rVar[5]))
call current.fx.setY(rVar[2] + current.moveSpeed*Sin(rVar[5]))
else
call current.fx.setX(current.targX)
call current.fx.setY(current.targY)
set current.moveType = -1
call current.onHitCallback()
// Either the instance was destroyed, or it wasn't included in the move list any longer
if current.moveType <= 0 then
set current.isLaunched = false
set current.next.prev = current.prev
set current.prev.next = current.next
if current.destroyWasInvoked then
call current.destroy()
set rVar[0] = rVar[0] - 1
endif
endif
endif
endif
endif
endif
set rVar[0] = rVar[0] + 1
set current = current.next
endloop
if rVar[0] < 0.5 then
call PauseTimer(thistype.tm)
endif
endmethod
private static method init takes nothing returns nothing
set thistype.tm = CreateTimer()
set thistype(0).next = 0
set thistype(0).prev = 0
set thistype.missileCallback = function thistype.onMissileUpdate
endmethod
implement Init
endstruct
endlibrary
For those who read up until the end, congratulations, you are an excellent reader. And for those of you who just want to enjoy the write-up, I wish a Happy New Year to you.