System Strilanc's Vehicle System [beta]

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!

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> &gt; 0 then
                return i
            endif

            //remember the first open slot
            if j == -1 and counts<i> &lt;= 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 &gt;= 0 then
            set counts<i> = counts<i> + 1
            return i
        endif
        
        //Fail if at capacity
        if num_keys &gt;= CAPACITY then
            if SHOW_ERROR_MESSAGES then
                call BJDebugMsg(&quot;Index: Couldn&#039;t index key[&quot; + I2S(key) + &quot;] due to maximum capacity.&quot;)
            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(&quot;Index: Added key[&quot; + I2S(key) + &quot;] =&gt; &quot; + I2S(i) + &quot; [total:&quot; + I2S(num_keys) + &quot;]&quot;)
        debug endif
        return i
    endfunction

    public function Destroy takes integer key returns nothing
        local integer i = Get(key)
        if i &lt; 0 then
            return
        elseif counts<i> &gt; 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(&quot;Index: Removed key[&quot; + I2S(key) + &quot;] =&gt; &quot; + I2S(i) + &quot; [total:&quot; + I2S(num_keys) + &quot;]&quot;)
        debug endif
            
        //Check if the following slot is in a probe tail
        set i = i - PROBE_JUMP
        if i &lt; 0 then
            set i = i + TABLE_SIZE
        endif
        if counts<i> != 0 then
            return
        endif
        
        //We&#039;re at the end of a probe tail; cut the tail
        loop
            set i = i + PROBE_JUMP
            if i &gt;= 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(&quot;Initialized Index with SHOW_DEBUG_MESSAGES&quot;)
        debug endif
    endfunction
endlibrary
///////////////////////////////////////////////////////
/// Strilanc&#039;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(&quot;Map Error: Failed to allocate a Vector. (Vector:Vector.create)&quot;)
                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 &quot;&lt;&quot;+R2S(.x)+&quot;, &quot;+R2S(.y)+&quot;, &quot;+R2S(.z)+&quot;&gt;&quot;
        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&#039;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&#039;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 &#039;c&#039; withour changing the sum of this and c
        ///Won&#039;t increase the length of either vector, and won&#039;t change the direction of &#039;c&#039;
        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 &lt; 0 then
                    call .inplace_add(c, -s)
                    call c.inplace_add(c, s)
                endif
            endif
        endmethod
    endstruct
endlibrary
///Strilanc&#039;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 &#039;on&#039; 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;Nice&#039; or t == &#039;Iice&#039; then
            set GROUND_friction = 0.01 //ice is slippery
        elseif t == &#039;Zbks&#039; then
            set GROUND_friction = 1.2 //roads are grippy
        elseif t == &#039;Zdrt&#039; then
            set GROUND_friction = 0.8 //dirt is loose
        elseif t == &#039;Dlvc&#039; 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(&quot;Unit&quot;, &quot;u&quot;, &quot;i&quot;)
                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;Arav&#039;) == 0 then
                    call UnitAddAbility(.u, &#039;Arav&#039;)
                    call UnitRemoveAbility(.u, &#039;Arav&#039;)
                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 &lt;= 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&#039;s speed along d
            set s2 = p2.vel.scalarProject(d) //p2&#039;s speed along d
            set sc = (p1.mass*s1 + p2.mass*s2) / (p1.mass + p2.mass) //center of mass&#039;s speed along d
            set e = p1.elasticity*p2.elasticity
            set dmg = p1.dmg + p2.dmg
            if s1 &gt; 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 &gt; 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) &lt;= 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) &gt; 0 then
                if argParticle.pos.z &lt;= GetDestructableOccluderHeight(d) + dz + 10 then
                    if dz &lt;= 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&#039;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 &gt; 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 &gt; 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 &gt; -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 &gt; 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) &gt; 0 then
                    if .vel.abs() &gt; 0 then
                        call SetUnitAnimation(.u, &quot;walk&quot;)
                    else
                        call SetUnitAnimation(.u, &quot;stand&quot;)
                    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() &lt; 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 &gt;= 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 &lt; 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&#039;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 &#039;vehicle ability&#039; 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;A008&#039;
        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 &gt; 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) &gt; 0 then
            call add_force(GetTriggerUnit())
        endif
    endfunction
    
    public function add takes unit u returns Particle
        if GetUnitAbilityLevel(u, VEHICLE_ABIL_ID) &gt; 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) &gt; 0 and GetIssuedOrderId() == OrderId(&quot;smart&quot;) then
            set p = Particle.find(u)
            if p == 0 then
                if SHOW_ERROR_MESSAGES then
                    call BJDebugMsg(&quot;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)&quot;)
                endif
                return
            endif
            
            //stop braking
            set p.thrusting = true
            set p.rolling = true
            
            //show &quot;!&quot; at order point
            //! runtextmacro TI_FastGet(&quot;Unit&quot;, &quot;u&quot;, &quot;i&quot;)
            if move_tags<i> != null then
                call DestroyEffect(move_tags<i>)
            endif
            set s = &quot;&quot;
            if USE_MOVE_EFFECTS and GetLocalPlayer() == GetOwningPlayer(u) then
                set s = &quot;Abilities\\Spells\\Other\\TalkToMe\\TalkToMe.mdl&quot;
            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, &quot;stop&quot;)
            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 &lt; -180 then
                    set d = d + 360
                elseif d &gt; 180 then 
                    set d = d - 360
                endif
                
                //stop when facing point or taking too long
                exitwhen RAbsBJ(d) &lt; 10
                set t = t - 1
                exitwhen t &lt;= 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) &gt; 0 and GetIssuedOrderId() == OrderId(&quot;smart&quot;) 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) &gt; 0 and GetIssuedOrderId() == OrderId(&quot;holdposition&quot;) then
            set p = Particle.find(GetTriggerUnit())
            if p == 0 then
                if SHOW_ERROR_MESSAGES then
                    call BJDebugMsg(&quot;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)&quot;)
                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) &gt; 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>
 

Attachments

  • ramp.PNG
    ramp.PNG
    491 KB · Views: 372
  • Lordaeron Grand Prix.w3x
    407.1 KB · Views: 253
Reaction score
341
After a few minutes of browsing the code, i couldn't really find anything wrong with it, though it sure is long :mad:.

Good luck :thup:

+rep.
 

Mac Dre

New Member
Reaction score
6
Umm whats up with the test map? I tried testing it and it just gave me a weird camera angle that wouldn't let me move and had floating text "Hillside Drive"
 

Strilanc

Veteran Scripter
Reaction score
42
Umm whats up with the test map? I tried testing it and it just gave me a weird camera angle that wouldn't let me move and had floating text "Hillside Drive"

I quote from the main post:
Use -select to enter and exit levels in the map.

*edit* I've modified the map so it tells you this. It's a beta, man!
 

Mac Dre

New Member
Reaction score
6
Oh my bad haha. Hmm well I tested it and its very sexy, the code seems to be fine to. Good work man +rep
 

Blackrage

Ultra Cool Member
Reaction score
25
You should make this multiplayer and turn it into a map... i played with it for about 20 minutes.. its pretty fun (funny too with below 100 gravity)
 
Reaction score
456
Keeps telling me "Attempt to destroy a null struct of type: Vector".

Anyway, looks nice :).
 

Strilanc

Veteran Scripter
Reaction score
42
the um "fields" dont work for S*** i mean it only goes to the top right square

You use the arrow keys to switch between tracks. The map should be telling you this at the start, unless you downloaded before I updated it last night.
 

Strilanc

Veteran Scripter
Reaction score
42
no it is all im saying is that its buggy for your NYI areas

Oh. Well duh, those tracks aren't implemented. It just uses a camera I threw down in the top right and doesn't even change their starting position from 0, 0.

If you really want to go into one of the deserts, you can do it by going off the ramp in ice capade really, really fast.
 

CaptDeath

New Member
Reaction score
103
oh and ice capade seems like it isnt done lol cause i went down and theres a dead end went up and it says wrong way "(
 

saw792

Is known to say things. That is all.
Reaction score
280
^You should add a gear shift system

And power steering. And ABS. And passenger side airbags. And a 3 year warranty. Ooh and suspension. And windscreen wipers. And GPS. And a full leather interior. And all that at just $19,990 with alloy wheels.

But seriously, this looks very nice. I always look forward to your systems Strilanc, they never disappoint.
 
General chit-chat
Help Users
  • No one is chatting at the moment.

      The Helper Discord

      Members online

      No members online now.

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top