Strilanc
Veteran Scripter
- Reaction score
- 42
I've been working on a racing map with realistic physics, and I've decided to post it here so other people can take a look at how the vehicles are done.
I'm calling it a system because if you copy-paste the triggers in the 'Vehicle System' category [TI, Vector, Physics, Vehicle], you can have vehicles in your map too!
Just assign the VEHICLE_ABIL_ID constant in the Vehicle trigger to whatever ability you want to represent 'moves like a vehicle', give that ability to some units in the object editor, and those units will move like vehicles!
Keep in mind this is all still in beta. Although the system is usable and should have few bugs, it's not easy or obvious how to do a lot of things like "don't go faster on lava". I'm especially looking for comments on what the system *should* be making easy to do.
I'm calling it a system because if you copy-paste the triggers in the 'Vehicle System' category [TI, Vector, Physics, Vehicle], you can have vehicles in your map too!
Just assign the VEHICLE_ABIL_ID constant in the Vehicle trigger to whatever ability you want to represent 'moves like a vehicle', give that ability to some units in the object editor, and those units will move like vehicles!
Code:
Vehicles:
- Accelerate in the direction they are facing
- Interpret 'smart' commands as 'face this point'
- Brake when 'hold position' is used
- Stop braking when 'smart' is used
- Will slide down hills, bounce off cliffs
- Collide with units and destructables
- Totally ignore doodads
Keep in mind this is all still in beta. Although the system is usable and should have few bugs, it's not easy or obvious how to do a lot of things like "don't go faster on lava". I'm especially looking for comments on what the system *should* be making easy to do.
JASS:
//This is all the libraries pasted one after the other [old version, will post newer one later]
///////////////////////////////////////////////////////
/// Strilanc's Tidy Indexer
/// Last Updated: January 18, 2009
/// Version: beta
///////////////////////////////////////////////////////
/// Summary:
/// - Assigns unique indexes to handle types, and automatically cleans decayed handles
///
/// Usage:
/// - Default available types are Unit, Item, Dest (destructable), Timer, Trigger
/// - Use TI_GetTypeIndex to get a handle's index
/// - Use macro TI_FastGet("Type", "handle", "variable") to efficiently assign 'variable' the index of 'handle'
/// - Use TI_RegTypeCleaner to add a callback for when a handle of type is cleaned because of decay
///////////////////////////////////////////////////////
library TI initializer init uses TIndex
globals
private constant real TIDY_PERIOD = 1.0 //how often indexes are tidied
private constant integer TIDY_RATE = 1 //base number of indexes tidied each period
endglobals
//===================================================================================================
globals
private integer type_arg_index = 0
private integer array type_list_indexes //indexes to handles in type-lists
private trigger array type_list_cleaners //type-specific cleaning functions for handles
private integer num = 0 //number of indexed handles
private integer tidy_count = 0 //number of hndles to tidy next
private integer tidy_index = 0 //last tidied handle
public integer tk = 0 //temporary storage for FastGet macro
endglobals
///Returns the address of a handle
public function H2I takes handle h returns integer
return h
return 0
endfunction
///Add available types
//! runtextmacro TI_AddType("unit", "Unit", "GetUnitTypeId(h) == 0")
//! runtextmacro TI_AddType("item", "Item", "GetItemTypeId(h) == 0")
//! runtextmacro TI_AddType("destructable", "Dest", "GetDestructableTypeId(h) == 0")
//! runtextmacro TI_AddType("timer", "Timer", "TimerGetElapsed(h) == 0 and TimerGetRemaining(h) == 0 and TimerGetTimeout(h) == 0")
private function isTriggerDecayed takes trigger t returns boolean
local triggeraction ta = TriggerAddAction(t, function DoNothing)
if ta != null then
call TriggerRemoveAction(t, ta)
set ta = null
return false
endif
return true
endfunction
//! runtextmacro TI_AddType("trigger", "Trigger", "isTriggerDecayed(h)")
///Efficiently retrieves the index of a key
///Favors the common case where the index already exists and does not collide
//! textmacro TI_FastGet takes name, key, val
set TI_tk = TI_H2I($key$)
set $val$ = TI_tk - (TI_tk/TIndex_TABLE_SIZE)*TIndex_TABLE_SIZE
if TIndex_keys[$val$] != TI_tk or TIndex_counts[$val$] <= 0 then
set $val$ = TI_Get$name$Index($key$)
endif
//! endtextmacro
//! textmacro TI_AddType takes type, name, decay_check_h
globals
private integer $type$_num = 0
private $type$ array $type$_list
private trigger $type$_outside_cleaner = CreateTrigger()
private trigger $type$_inside_cleaner = null
public $type$ arg$name$ = null
endglobals
///Removes a handle from the system
private function $type$_clean takes nothing returns boolean
local $type$ h = $type$_list[type_arg_index]
if $decay_check_h$ then
//notify
set arg$name$ = h
call TriggerExecute($type$_outside_cleaner)
set arg$name$ = null
//clean
call TIndex_Destroy(H2I(h))
set $type$_num = $type$_num - 1
set $type$_list[type_arg_index] = $type$_list[$type$_num]
set $type$_list[$type$_num] = null
set h = null
return true
endif
set h = null
return false
endfunction
///Adds a callback for when a handle is cleaned
public function Reg$name$Cleaner takes code c returns nothing
call TriggerAddAction($type$_outside_cleaner, c)
endfunction
///Returns a unique index for the given handle
public function Get$name$Index takes $type$ h returns integer
local integer i = TIndex_Get(H2I(h))
if i < 0 then
//create
if $type$_inside_cleaner == null then
set $type$_inside_cleaner = CreateTrigger()
call TriggerAddCondition($type$_inside_cleaner, Condition(function $type$_clean))
endif
set i = TIndex_Create(H2I(h))
//insert in main list
set type_list_indexes[num] = $type$_num
set type_list_cleaners[num] = $type$_inside_cleaner
set num = num + 1
//insert in type list
set $type$_list[$type$_num] = h
set $type$_num = $type$_num + 1
set tidy_count = tidy_count + 1
endif
return i
endfunction
//! endtextmacro
///Checks a few handles in the system for decay
private function iterate_tidy takes nothing returns nothing
set tidy_count = tidy_count + TIDY_RATE
loop
//countdown
exitwhen num <= 0
exitwhen tidy_count <= 0
set tidy_count = tidy_count - 1
//next index
if tidy_index <= 0 then
set tidy_index = num
endif
set tidy_index = tidy_index - 1
//clean
set type_arg_index = type_list_indexes[tidy_index]
if TriggerEvaluate(type_list_cleaners[tidy_index]) then
set num = num - 1
set type_list_indexes[tidy_index] = type_list_indexes[num]
set type_list_cleaners[tidy_index] = type_list_cleaners[num]
set type_list_cleaners[num] = null
set type_list_indexes[num] = 0
endif
endloop
endfunction
private function init takes nothing returns nothing
local trigger t = CreateTrigger()
call TriggerAddAction(t, function iterate_tidy)
call TriggerRegisterTimerEvent(t, TIDY_PERIOD, true)
endfunction
endlibrary
library TIndex initializer init
globals
private constant boolean SHOW_DEBUG_MESSAGES = true //show debug messages? (only in debug mode)
private constant boolean SHOW_ERROR_MESSAGES = true //show error messages when things go obviously wrong?
endglobals
//===========================================================================================================
globals
public constant integer CAPACITY = 6000 //maximum allowed number of indexed keys
public constant integer TABLE_SIZE = 8191
public constant integer PROBE_JUMP = 29 //the number of slots probing skips backwards over
public integer array counts //usage count; -1 indicates empty but in probe chain
public integer array keys
public integer num_keys = 0
endglobals
public function Get takes integer key returns integer
local integer j = -1
local integer i = key - (key/TABLE_SIZE)*TABLE_SIZE
loop
//keep in range
if i < 0 then
set i = i + TABLE_SIZE
endif
//return match if found
if keys<i> == key and counts<i> > 0 then
return i
endif
//remember the first open slot
if j == -1 and counts<i> <= 0 then
set j = i
endif
//exit when the probe chain ends
exitwhen counts<i> == 0
//next probe slot
set i = i - PROBE_JUMP
endloop
return -1-j //[j is guaranteed to be a valid index by this point]
endfunction
public function Create takes integer key returns integer
local integer i = Get(key)
//Just increase usage count if already exists
if i >= 0 then
set counts<i> = counts<i> + 1
return i
endif
//Fail if at capacity
if num_keys >= CAPACITY then
if SHOW_ERROR_MESSAGES then
call BJDebugMsg("Index: Couldn039;t index key[" + I2S(key) + "] due to maximum capacity.")
endif
return -1
endif
//Index the key
set i = -1-i
set keys<i> = key
set counts<i> = 1
set num_keys = num_keys + 1
debug if SHOW_DEBUG_MESSAGES then
debug call BJDebugMsg("Index: Added key[" + I2S(key) + "] => " + I2S(i) + " [total:" + I2S(num_keys) + "]")
debug endif
return i
endfunction
public function Destroy takes integer key returns nothing
local integer i = Get(key)
if i < 0 then
return
elseif counts<i> > 1 then
set counts<i> = counts<i> - 1
return
endif
//Clean the index
set counts<i> = -1
set num_keys = num_keys - 1
debug if SHOW_DEBUG_MESSAGES then
debug call BJDebugMsg("Index: Removed key[" + I2S(key) + "] => " + I2S(i) + " [total:" + I2S(num_keys) + "]")
debug endif
//Check if the following slot is in a probe tail
set i = i - PROBE_JUMP
if i < 0 then
set i = i + TABLE_SIZE
endif
if counts<i> != 0 then
return
endif
//We're at the end of a probe tail; cut the tail
loop
set i = i + PROBE_JUMP
if i >= TABLE_SIZE then
set i = i - TABLE_SIZE
endif
exitwhen counts<i> != -1
set counts<i> = 0
endloop
endfunction
//=== Initialization ================================================
private function init takes nothing returns nothing
debug if SHOW_DEBUG_MESSAGES then
debug call BJDebugMsg("Initialized Index with SHOW_DEBUG_MESSAGES")
debug endif
endfunction
endlibrary
///////////////////////////////////////////////////////
/// Strilanc's Vector Library
/// Last Updated: January 18, 2009
/// Version: beta
///////////////////////////////////////////////////////
/// Summary:
/// - Just a typical vector library.
///////////////////////////////////////////////////////
library Vectors
globals
integer numVectors = 0
endglobals
struct Vector
real x
real y
real z
public static method create takes real x, real y, real z returns Vector
local Vector this = Vector.allocate()
if this == 0 then
call BJDebugMsg("Map Error: Failed to allocate a Vector. (Vector:Vector.create)")
return 0
endif
set .x = x
set .y = y
set .z = z
set numVectors = numVectors + 1
return this
endmethod
public static method createSpherical takes real length, real yaw, real pitch returns Vector
return Vector.create(length*Cos(yaw)*Cos(pitch), length*Sin(yaw)*Cos(pitch), length*Sin(pitch))
endmethod
private method onDestroy takes nothing returns nothing
set numVectors = numVectors - 1
endmethod
///=== Management Methods
public method clone takes nothing returns Vector
return Vector.create(.x, .y, .z)
endmethod
public method paste takes Vector v returns nothing
set v.x = .x
set v.y = .y
set v.z = .z
endmethod
public method clear takes nothing returns nothing
set .x = 0
set .y = 0
set .z = 0
endmethod
///=== Properties
public method abs takes nothing returns real
return SquareRoot(.x*.x + .y*.y + .z*.z)
endmethod
public method abs2 takes nothing returns real
return .x*.x + .y*.y + .z*.z
endmethod
public method toString takes nothing returns string
return "<"+R2S(.x)+", "+R2S(.y)+", "+R2S(.z)+">"
endmethod
///=== Mutators
public method inplace_add takes Vector v, real c returns nothing
set .x = .x + v.x * c
set .y = .y + v.y * c
set .z = .z + v.z * c
endmethod
public method inplace_scale takes real c returns nothing
set .x = .x * c
set .y = .y * c
set .z = .z * c
endmethod
public method destroyReturn takes Vector r returns Vector
call .destroy()
return r
endmethod
public method setLength takes real d returns nothing
local real r = .abs()
if r == 0 then
return //zero vector can't be scaled
endif
call .inplace_scale(d/r)
endmethod
public method normalize takes nothing returns nothing
call .setLength(1)
endmethod
///=== Operations
public static method dot takes Vector v1, Vector v2 returns real
return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z
endmethod
public static method sum takes Vector v1, Vector v2 returns Vector
return Vector.create(v1.x+v2.x, v1.y+v2.y, v1.z+v2.z)
endmethod
public static method weightedSum takes Vector v1, real c1, Vector v2, real c2 returns Vector
return Vector.create(v1.x*c1+v2.x*c2, v1.y*c1+v2.y*c2, v1.z*c1+v2.z*c2)
endmethod
public method cross takes Vector v returns Vector
return Vector.create(.y*v.z-v.y*.z, .z*v.x-v.z*.x, .x*v.y-v.x*.y)
endmethod
public method minus takes Vector v returns Vector
return Vector.create(.x-v.x, .y-v.y, .z-v.z)
endmethod
///=== Projection
///Scalar projection of v onto p
public method scalarProject takes Vector p returns real
local real d = p.abs()
if d == 0 then
return 0. //can't project onto zero vector
endif
return Vector.dot(this, p) / d
endmethod
///Projection of v onto p
public method project takes Vector p returns Vector
set p = p.clone()
call p.setLength(.scalarProject(p))
return p
endmethod
///Perpendicular projection of v onto p
public method perp takes Vector p returns Vector
set p = this.project(p)
call p.inplace_scale(-1)
call p.inplace_add(this, 1)
return p
endmethod
///Length of perpendicular projection of v onto p
public method scalarPerp takes Vector p returns real
local real s
set p = this.perp(p)
set s = p.abs()
call p.destroy()
return s
endmethod
///Minimizes this vector using 'c' withour changing the sum of this and c
///Won't increase the length of either vector, and won't change the direction of 'c'
public method inplace_cancel takes Vector c returns nothing
local real s = c.abs()
if s != 0 then
set s = RMaxBJ(.scalarProject(c)/s, -s)
if s < 0 then
call .inplace_add(c, -s)
call c.inplace_add(c, s)
endif
endif
endmethod
endstruct
endlibrary
///Strilanc's physics system
///beta
library Physics initializer init uses Vectors, TI
globals
///distance is measured in wc3 distance, time is measured in game seconds
private constant real GRAVITY = 300
private constant real GROUND_REACH = 1.0 //units at least this close to the ground are considered 'on' the ground
private constant real DESTRUCTABLE_DEATH_MOMENTUM = 3000 //momentum required to kill a destructable in collision
private constant real DESTRUCTABLE_MASS = 1000 //mass of destructables in collisions
private constant boolean AUTO_TICK = true //automatically advance state of all particles
private constant real AUTO_DT = 0.01 //period between automatick ticks
private constant boolean AUTO_ADJUST_TICK = true //lower tick rate for lots of particles
private constant integer AUTO_ADJUST_THRESHOLD = 10 //number of particles required to jump between tick rates
private constant integer AUTO_ADJUST_SMOOTH = 2 //smooths adjustments (must be greater than 0)
endglobals
globals
private real GROUND_friction
private real GROUND_elasticity
private real GROUND_speed
endglobals
///Add your own tile logic as needed here
private function analyze_terrain takes real x, real y returns nothing
local integer t = GetTerrainType(x, y)
set GROUND_elasticity = 0.25
set GROUND_speed = 1
set GROUND_friction = 1.0
if t == 039;Nice039; or t == 039;Iice039; then
set GROUND_friction = 0.01 //ice is slippery
elseif t == 039;Zbks039; then
set GROUND_friction = 1.2 //roads are grippy
elseif t == 039;Zdrt039; then
set GROUND_friction = 0.8 //dirt is loose
elseif t == 039;Dlvc039; then
set GROUND_speed = 3.0 //lava is a speed boost
set GROUND_friction = 3.0
endif
endfunction
///============================================================================
globals
private constant integer TILE_SIZE = 128
private constant real EPSILON = 0.001
private boolexpr NO_FILTER = null
//scratch space for ground functions
private location SLOPE_LOC = Location(0, 0)
//particle tracking
private Particle array particles
private integer numParticles = 0
private Particle array particleMap
//args
private Particle argParticle
private unit argUnit
private destructable argDestructable
//conditions to allow particles to auto-die
private constant trigger unlockTrigger = CreateTrigger()
//tick adjustment
private constant timer autoTicker = CreateTimer()
private integer curTickThreshold = 0
private real curDT = 0
endglobals
///Adds a condition for releasing particles from the system
function addUnlockCondition takes boolexpr c returns nothing
call TriggerAddCondition(unlockTrigger, c)
endfunction
///Returns the terrain height at (x,y)
function GetCoordZ takes real x, real y returns real
call MoveLocation(SLOPE_LOC, x, y)
return GetLocationZ(SLOPE_LOC)
endfunction
///Returns the terrain normal at (x,y)
function GetCoordNormal takes real x, real y returns Vector
local real z = GetCoordZ(x, y)
local Vector v = Vector.create(z-GetCoordZ(x+1,y), z-GetCoordZ(x,y+1), 1)
call v.normalize()
return v
endfunction
///Returns the normal vector of cliffs at (x,y), or the null vector if there are no cliffs
function GetCoordCliffNormal takes real x, real y returns Vector
local Vector v
local real dx
local real dy
if not IsTerrainPathable(x, y, PATHING_TYPE_AMPHIBIOUSPATHING) then
return 0
endif
set dx = GetTerrainCliffLevel(x+TILE_SIZE/2, y) - GetTerrainCliffLevel(x-TILE_SIZE/2, y)
set dy = GetTerrainCliffLevel(x, y+TILE_SIZE/2) - GetTerrainCliffLevel(x, y-TILE_SIZE/2)
if dx == 0 and dy == 0 then
return 0
endif
set v = Vector.create(-dx, -dy, 0)
call v.normalize()
return v
endfunction
struct Particle
//state
Vector pos //position
Vector vel //velocity
Vector acc //acceleration
Vector acc_slowing //acceleration that may only cause decreases in speed
//properties
Vector acc_constant //acceleration preserved over ticks
real drag = 0 //air friction factor
real fric = 1 //ground friction factor
real dmg = 0 //collision damage factor
real mass = 1 //mass [only really used for collisions]
boolean rolling = false //should friction only act sideways
real elasticity = 1 //bounce factor in collisions
real thrust = 0 //constant forward acceleration
real collision_lift_z = 0
real collision_radius = 100
boolean thrusting = false
boolean collides = true
boolean auto_destroy = true
//wrapper
readonly unit u //unit
private integer i //list index
private integer h = 0 //map index
///Returns the particle associated with the given unit
public static method find takes unit u returns Particle
local integer i = 0
if u != null then
//! runtextmacro TI_FastGet("Unit", "u", "i")
set i = particleMap<i>
endif
return i
endmethod
///Creates a particle for the given unit or, if one already exists, returns it
public static method create takes unit u returns Particle
local Particle this
if u != null then
set this = Particle.find(u)
if this != 0 then
return this
endif
endif
set this = Particle.allocate()
set .pos = Vector.create(0,0,0)
set .vel = Vector.create(0,0,0)
set .acc = Vector.create(0,0,0)
set .acc_slowing = Vector.create(0,0,0)
set .acc_constant = Vector.create(0,0,-GRAVITY)
set .u = u
if .u != null then
//position
set .pos.x = GetUnitX(u)
set .pos.y = GetUnitY(u)
set .pos.z = GetCoordZ(.pos.x, .pos.y) + GetUnitFlyHeight(u)
//insert in map
set .h = TI_GetUnitIndex(.u)
set particleMap[.h] = this
//enable SetUnitFlyHeight for unit
if GetUnitAbilityLevel(.u, 039;Arav039;) == 0 then
call UnitAddAbility(.u, 039;Arav039;)
call UnitRemoveAbility(.u, 039;Arav039;)
endif
endif
//insert in list
set .i = numParticles
set particles[.i] = this
set numParticles = numParticles + 1
return this
endmethod
private method onDestroy takes nothing returns nothing
call .pos.destroy()
call .vel.destroy()
call .acc.destroy()
call .acc_constant.destroy()
call .acc_slowing.destroy()
if .u != null then
//go back to normal flying height slowly
call SetUnitFlyHeight(.u, GetUnitDefaultFlyHeight(.u), 100)
//remove from particle map
set particleMap[.h] = 0
set .u = null
endif
//remove from particle list
set numParticles = numParticles - 1
set particles[.i] = particles[numParticles]
set particles[.i].i = .i
endmethod
///Shorthand functions
public method isOnGround takes nothing returns boolean
return .pos.z <= GetCoordZ(.pos.x, .pos.y) + GROUND_REACH
endmethod
///Collides two particles using conservation of momentum (screw energy!)
private static method collide takes Particle p1, Particle p2 returns nothing
local Vector d
local real s1
local real s2
local real sc
local real dmg
local real e
if not p1.collides or not p2.collides then
return
endif
set d = p2.pos.minus(p1.pos) //p1 to p2
set s1 = p1.vel.scalarProject(d) //p1's speed along d
set s2 = p2.vel.scalarProject(d) //p2's speed along d
set sc = (p1.mass*s1 + p2.mass*s2) / (p1.mass + p2.mass) //center of mass's speed along d
set e = p1.elasticity*p2.elasticity
set dmg = p1.dmg + p2.dmg
if s1 > s2 then //if particles moving towards each other
call d.normalize()
//convert s1 and s2 to magnitude of impulse along d
set s1 = (sc-s1)*(1+e)
set s2 = (sc-s2)*(1+e)
//damage
if p1.u != null and p2.u != null and dmg > 0 then
call UnitDamageTarget(p1.u, p2.u, RAbsBJ(s2)*dmg, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
call UnitDamageTarget(p2.u, p1.u, RAbsBJ(s1)*dmg, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
endif
//BANG!
call p1.vel.inplace_add(d, s1)
call p2.vel.inplace_add(d, s2)
endif
call d.destroy()
endmethod
///Sets argUnit to any living unit in range
private static method enumFindUnit takes nothing returns nothing
local unit u = GetEnumUnit()
local real uz = GetUnitFlyHeight(u)+GetCoordZ(GetUnitX(u), GetUnitY(u))
if argParticle.u != u and IsUnitType(u, UNIT_TYPE_STRUCTURE) == false then
if RAbsBJ(uz - argParticle.pos.z) <= argParticle.collision_radius then
if IsUnitType(u, UNIT_TYPE_DEAD) == false then
set argUnit = u
endif
endif
endif
set u = null
endmethod
///Sets argDestructable to any living destructable in range
private static method enumFindDestructable takes nothing returns nothing
local destructable d = GetEnumDestructable()
local real dz = GetCoordZ(GetDestructableX(d), GetDestructableY(d))
if GetDestructableLife(d) > 0 then
if argParticle.pos.z <= GetDestructableOccluderHeight(d) + dz + 10 then
if dz <= argParticle.pos.z + argParticle.collision_radius then
set argDestructable = d
endif
endif
endif
set d = null
endmethod
///Updates argUnit and argDestructable to be any living things in range
private method findNearbyObjects takes nothing returns nothing
local real d = .collision_radius
local rect r = Rect(.pos.x-d, .pos.y-d, .pos.x+d, .pos.y+d)
local group g = CreateGroup()
set argParticle = this
set argUnit = null
set argDestructable = null
call EnumDestructablesInRect(r, NO_FILTER, function Particle.enumFindDestructable)
call GroupEnumUnitsInRect(g, r, NO_FILTER)
call ForGroup(g, function Particle.enumFindUnit)
call DestroyGroup(g)
call RemoveRect(r)
set r = null
set g = null
endmethod
///Checks for nearby objects to collide with
private method tryCollide takes nothing returns nothing
local Particle p
if not .collides then
return
endif
//get stuff in range [sets argUnit, argDestructable]
call .findNearbyObjects()
if IsTerrainPathable(.pos.x, .pos.y, PATHING_TYPE_FLYABILITY) then
//shouldn't be moving here
set .vel.x = 0
set .vel.y = 0
elseif argDestructable != null then
//smash into a tree [pretty inelastic]
set p = Particle.create(null)
set p.pos.x = GetDestructableX(argDestructable)
set p.pos.y = GetDestructableY(argDestructable)
set p.pos.z = .pos.z //[collide like destructable is a cylinder]
set p.mass = DESTRUCTABLE_MASS
if .vel.abs()*.mass > DESTRUCTABLE_DEATH_MOMENTUM then
call KillDestructable(argDestructable)
endif
set p.elasticity = 0.1
call Particle.collide(this, p)
call p.destroy()
elseif argUnit != null then
//collide with a unit
set p = Particle.find(argUnit)
if p == 0 then
set p = Particle.create(argUnit)
set p.pos.x = GetUnitX(argUnit)
set p.pos.y = GetUnitY(argUnit)
set p.pos.z = GetCoordZ(p.pos.x, p.pos.y)+GetUnitFlyHeight(argUnit)+.collision_lift_z
endif
call Particle.collide(this, p)
set p.pos.z = p.pos.z - .collision_lift_z
endif
endmethod
///Computes common accelerations and decelerations
private method compute takes real dt returns nothing
local Vector n_ground
local Vector n_cliff
local real s_ground
local real s_cliff
local Vector f
local Vector x
local real fn
//drag
if .drag != 0 then
call .acc_slowing.inplace_add(.vel, -.vel.abs()*.drag)
endif
//ground interactions
set n_ground = GetCoordNormal(.pos.x, .pos.y)
set s_ground = -.vel.scalarProject(n_ground) //speed into ground
set n_cliff = GetCoordCliffNormal(.pos.x, .pos.y)
if n_cliff != 0 then
set s_cliff = -.vel.scalarProject(n_cliff) //speed into cliff
else
set s_cliff = 0
endif
if .isOnGround() then
//stay above ground
set .pos.z = RMaxBJ(.pos.z, GetCoordZ(.pos.x, .pos.y))
//bounce off cliffs
if s_cliff > EPSILON then
call .vel.inplace_add(n_cliff, s_cliff * (1 + GROUND_elasticity*.elasticity))
set s_ground = -.vel.scalarProject(n_ground)
endif
//ground
if s_ground > -EPSILON then
call analyze_terrain(.pos.x, .pos.y)
set fn = .fric * GROUND_friction
//normal force
call .acc.inplace_add(n_ground, GRAVITY*n_ground.z)
//thrust
if .thrusting and .u != null then
set x = Vector.createSpherical(1, GetUnitFacing(.u)*bj_DEGTORAD, 0) //flag facing
set x = x.destroyReturn(x.perp(n_ground)) //facing along terrain
call x.setLength(GROUND_speed * RMinBJ(fn, 1) * .thrust / .mass) //thrust
call .acc.inplace_add(x, 1)
call x.destroy()
endif
//bounce off ground
if s_ground > EPSILON then
call .vel.inplace_add(n_ground, s_ground * (1 + GROUND_elasticity*.elasticity))
endif
//friction
if fn != 0 then
set f = Vector.weightedSum(.vel, 1, .acc, dt) //predicted velocity
if .rolling and .u != null then //rolling friction only acts sideways to facing, not forward-back
set x = Vector.createSpherical(1, GetUnitFacing(.u)*bj_DEGTORAD, 0) //flat facing
set x = x.destroyReturn(x.perp(n_ground)) //facing along terrain
set f = f.destroyReturn(f.perp(x)) //predicted velocity not along facing
call x.destroy()
endif
set f = f.destroyReturn(f.perp(n_ground)) //predicted velocity along terrain
call f.setLength(-fn*GRAVITY*n_ground.z) //predicted friction
call .acc_slowing.inplace_add(f, 1)
call f.destroy()
endif
endif
endif
call n_ground.destroy()
call n_cliff.destroy()
endmethod
///Advances the state of this particle by dt
public method tick takes real dt returns nothing
//constant forces
call .acc.inplace_add(.acc_constant, 1)
//ground forces
call .compute(dt)
//convert accelerations to impulses
call .acc.inplace_scale(dt)
call .acc_slowing.inplace_scale(dt)
//accelerate
call .acc.inplace_cancel(.acc_slowing)
call .vel.inplace_cancel(.acc_slowing)
call .vel.inplace_add(.acc, 1)
//displace
call .pos.inplace_add(.vel, dt)
call .pos.inplace_add(.acc, dt*-0.5)
if .u != null then
if GetUnitState(.u, UNIT_STATE_LIFE) > 0 then
if .vel.abs() > 0 then
call SetUnitAnimation(.u, "walk")
else
call SetUnitAnimation(.u, "stand")
endif
endif
//interact with other things
call .tryCollide()
//draw
if not IsTerrainPathable(.pos.x, .pos.y, PATHING_TYPE_FLYABILITY) then
call SetUnitX(.u, .pos.x)
call SetUnitY(.u, .pos.y)
else
set .pos.x = GetUnitX(.u)
set .pos.y = GetUnitY(.u)
endif
call SetUnitFlyHeight(.u, .pos.z-GetCoordZ(.pos.x, .pos.y), 100000)
endif
//reset
call .acc.clear()
call .acc_slowing.clear()
endmethod
///Destroys this particle if it is inert
public method tryDestroy takes nothing returns nothing
if .u != null and GetUnitTypeId(.u) == 0 then //decayed
call .destroy()
elseif .vel.abs() < 100 and .isOnGround() and .auto_destroy then //inert
set argParticle = this
if IsUnitType(.u, UNIT_TYPE_DEAD) == true or TriggerEvaluate(unlockTrigger) then
call .destroy()
endif
endif
endmethod
endstruct
///Advances the state of all particles by dt seconds
public function tick takes real dt returns nothing
local integer i = 0
loop
exitwhen i >= numParticles
call particles<i>.tick(dt)
set i = i + 1
endloop
//release particles that have become inert
set i = numParticles
loop
set i = i - 1
exitwhen i < 0
call particles<i>.tryDestroy()
endloop
endfunction
//Updates state of every particle and adjusts tick rate to keep from lagging too much
private function autoTick takes nothing returns nothing
call tick(curDT)
if AUTO_ADJUST_TICK and numParticles / AUTO_ADJUST_THRESHOLD != curTickThreshold then
set curTickThreshold = numParticles / AUTO_ADJUST_THRESHOLD
set curDT = (curTickThreshold+AUTO_ADJUST_SMOOTH)*AUTO_DT/AUTO_ADJUST_SMOOTH
call PauseTimer(autoTicker)
call TimerStart(autoTicker, curDT, true, null)
endif
endfunction
private function truth takes nothing returns boolean
return true
endfunction
private function init takes nothing returns nothing
local trigger t
set NO_FILTER = Condition(function truth)
if AUTO_TICK then
set t = CreateTrigger()
call TriggerAddAction(t, function autoTick)
call TriggerRegisterTimerExpireEvent(t, autoTicker)
set curDT = AUTO_DT
call TimerStart(autoTicker, curDT, true, null)
endif
endfunction
endlibrary
///////////////////////////////////////////////////////
/// Strilanc's Vehicle System
/// Last Updated: January 18, 2009
/// Version: beta
///////////////////////////////////////////////////////
/// Summary:
/// - Imbues units with vehicle-like controls and movement
///
/// Usage:
/// - Assign an ability as the 'vehicle ability' by changing the VEHICLE_ABIL_ID constant
/// - Any units with the ability will automatically move like vehicles
/// - Use Vehicle_add to give a unit the vehicle ability
/// - Use Vehicle_remove to return a unit to normal
///
/// Notes:
/// - Do not add the vehicle ability with triggers, use Vehicle_add(unit) instead
///////////////////////////////////////////////////////
library Vehicle initializer init uses Physics, TI
globals
private constant integer VEHICLE_ABIL_ID = 039;A008039;
private constant boolean USE_MOVE_EFFECTS = true
private constant boolean SHOW_ERROR_MESSAGES = true
endglobals
///============================================================================
globals
private effect array move_tags
private boolexpr NO_FILTER = null
endglobals
private function add_force takes unit u returns Particle
local Particle p = Particle.create(u)
set p.auto_destroy = false
set p.dmg = .2
set p.thrust = 1200
set p.rolling = true
set p.thrusting = false
set p.drag = 0.00005
set p.mass = 4
set p.fric = 1
set p.collision_lift_z = 100
call SetUnitPathing(u, false)
call UnitAddAbility(u, VEHICLE_ABIL_ID)
return p
endfunction
private function remove_force takes unit u returns nothing
local Particle p = Particle.find(u)
if p > 0 then
set p.auto_destroy = true
set p.thrusting = false
set p.rolling = false
set p.thrust = 0
endif
call SetUnitPathing(u, true)
call UnitRemoveAbility(u, VEHICLE_ABIL_ID)
endfunction
private function enter takes nothing returns nothing
if GetUnitAbilityLevel(GetTriggerUnit(), VEHICLE_ABIL_ID) > 0 then
call add_force(GetTriggerUnit())
endif
endfunction
public function add takes unit u returns Particle
if GetUnitAbilityLevel(u, VEHICLE_ABIL_ID) > 0 then
return Particle.find(u)
endif
return add_force(u)
endfunction
public function remove takes unit u returns nothing
if GetUnitAbilityLevel(u, VEHICLE_ABIL_ID) == 0 then
return
endif
call remove_force(u)
endfunction
private function order_smart_move takes nothing returns nothing
local unit u = GetTriggerUnit()
local real x = GetOrderPointX()
local real y = GetOrderPointY()
local integer i
local Particle p
local integer t = 8
local real d
local effect e
local string s
if GetUnitAbilityLevel(u, VEHICLE_ABIL_ID) > 0 and GetIssuedOrderId() == OrderId("smart") then
set p = Particle.find(u)
if p == 0 then
if SHOW_ERROR_MESSAGES then
call BJDebugMsg("Vehicle Error: Vehicle was removed from physics system. (Vehicle<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite9" alt=":eek:" title="Eek! :eek:" loading="lazy" data-shortname=":eek:" />rder_smart_move)")
endif
return
endif
//stop braking
set p.thrusting = true
set p.rolling = true
//show "!" at order point
//! runtextmacro TI_FastGet("Unit", "u", "i")
if move_tags<i> != null then
call DestroyEffect(move_tags<i>)
endif
set s = ""
if USE_MOVE_EFFECTS and GetLocalPlayer() == GetOwningPlayer(u) then
set s = "Abilities\\Spells\\Other\\TalkToMe\\TalkToMe.mdl"
endif
set e = AddSpecialEffect(s, x, y)
set move_tags<i> = e
//cancel smart order; otherwise ground pathing will screw things up
call PauseUnit(u, true)
call IssueImmediateOrder(u, "stop")
call PauseUnit(u, false)
//face target point
loop
//start facing point
set d = Atan2(y - GetUnitY(u), x - GetUnitX(u))*bj_RADTODEG
call SetUnitFacingTimed(u, ModuloReal(d, 360), 0)
set d = GetUnitFacing(u) - d
if d < -180 then
set d = d + 360
elseif d > 180 then
set d = d - 360
endif
//stop when facing point or taking too long
exitwhen RAbsBJ(d) < 10
set t = t - 1
exitwhen t <= 0
//wait a bit for unit to face the point
call TriggerSleepAction(0.1)
exitwhen e != move_tags<i> //[another move order issued]
endloop
//cleanup
if e == move_tags<i> then
call DestroyEffect(e)
set move_tags<i> = null
endif
set e = null
endif
set u = null
endfunction
private function order_smart_move_target takes nothing returns nothing
if GetUnitAbilityLevel(GetTriggerUnit(), VEHICLE_ABIL_ID) > 0 and GetIssuedOrderId() == OrderId("smart") then
call IssuePointOrderById(GetTriggerUnit(), GetIssuedOrderId(), GetWidgetX(GetOrderTarget()), GetWidgetY(GetOrderTarget()))
endif
endfunction
private function order_brake takes nothing returns nothing
local Particle p
if GetUnitAbilityLevel(GetTriggerUnit(), VEHICLE_ABIL_ID) > 0 and GetIssuedOrderId() == OrderId("holdposition") then
set p = Particle.find(GetTriggerUnit())
if p == 0 then
if SHOW_ERROR_MESSAGES then
call BJDebugMsg("Vehicle Error: Vehicle was removed from physics system. (Vehicle<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite9" alt=":eek:" title="Eek! :eek:" loading="lazy" data-shortname=":eek:" />rder_brake)")
endif
return
endif
set p.thrusting = false
set p.rolling = false
endif
endfunction
private function add_preplaced_vehicles takes nothing returns nothing
local group g = CreateGroup()
local rect r = GetWorldBounds()
local unit u
call GroupEnumUnitsInRect(g, r, NO_FILTER)
loop
set u = FirstOfGroup(g)
exitwhen u == null
call GroupRemoveUnit(g, u)
if GetUnitAbilityLevel(u, VEHICLE_ABIL_ID) > 0 then
call add_force(u)
endif
endloop
call RemoveRect(r)
call DestroyGroup(g)
set r = null
set g = null
endfunction
private function truth takes nothing returns boolean
return true
endfunction
private function init takes nothing returns nothing
local trigger t = CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER)
call TriggerAddAction(t, function order_smart_move)
set t = CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER)
call TriggerAddAction(t, function order_smart_move_target)
set t = CreateTrigger()
call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_ORDER)
call TriggerAddAction(t, function order_brake)
set t = CreateTrigger()
call TriggerRegisterEnterRectSimple(t, GetWorldBounds())
call TriggerAddAction(t, function enter)
set t = CreateTrigger()
call TriggerRegisterTimerEvent(t, 0, false)
call TriggerAddAction(t, function add_preplaced_vehicles)
set NO_FILTER = Condition(function truth)
endfunction
endlibrary</i></i></i></i></i></i></i></i></i></i></i></i></i></i></i></i></i></i></i></i></i></i></i></i>