• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Realistic Airplane System

Requires:
ListModule

Optional:
AutoIndex

This is a lightweight system for adding realistic airplane movement to your map. It also features inertia, which means the airplanes will "slide" more or less depending on how low you set the air resistance to be. Please note that setting the air resistance to values above or equal to 1 will result in your airplanes accelerating out of control, and values below 0.95 might also cause your airplanes to spin out when turning.

To give airplane motion to a unit type, all you need to do is use the following line:

JASS:
call registerPlaneType('xxxx')

Where 'xxxx' is the unit ID of your airplane (you can find unit IDs by pressing CTRL+D in the object editor). This line must be put inside the function named initPlaneTypes, which you can easily find inside the library. The airplanes will use the movement and turning speed that you have specified in the object editor, but the max speed will be scaled by a variable called "SPEED_FACTOR", which defaults to 160%. This variable is used to bypass the hardcoded limit for unit speed that warcraft has.

Some things to keep in mind when creating units to work with this system:
  • It works best with low turning speeds - you may have to shift-click the turning speed field to enter values lower than 0.1. I've found that somewhere around 0.05 is good.
  • You will need to tweak the turning speed together with the movement speed so that the unit has a turning circle that works for doing attack runs. If the turning circle is too large, the unit won't be able to attack, and if it is too small, it will no longer look like a plane. Slower speeds need lower turn rates.
  • It is probably best to put "Animation - damage point" to 0 for all attacks, otherwise there is a risk that the unit doesn't have time to attack.

JASS:
library AirplaneSystem requires ListModule, optional AutoIndex

    /*
       .~@ FINGOLFINS AIRPLANE MOVEMENT SYSTEM @~.
   
        INSTRUCTIONS:
            - Implement AutoIndex as specified in its description
            - Create your airplane units as you would with any normal unit
            - Set the turn speed of your airplanes to something below 0.1 (use shift-click)
            - Register plane types by adding this line to the initPlaneTypes function:
           
              call registerPlaneType('xxxx')
             
              where 'xxxx' is the id of your airplane.
    */
   
    //=======================//
    //======= GLOBALS =======//
    //=======================//

    globals
        private constant real UPDATE_INTERVAL   = 0.03125 //I SUBMIT TO THIS FOLLY
        private constant real ORDER_INTERVAL    = 1 // HOW OFTEN ORDERS ARE REFRESHED
        private constant real DEFAULT_FACING    = 270
       
        private constant integer MAX_PLAYERS    = 24
       
        //LOWER NUMBERS = LESS INERTIA. SET TO ZERO TO REMOVE INERTIA COMPLETELY. THIS VALUE MUST BE LOWER THAN 1!
        private constant real AIR_FRICTION      = 0.945
       
        //A CONSTANT MULTIPILER TO THE UNIT SPEED, TO CIRCUMVENT THE HARDCODED SPEED LIMIT OF WC3.
        private constant real SPEED_FACTOR      = 1.6
       
        //MAX DISTANCE TO MAP BORDER WHERE PLANES CAN TRAVEL
        private constant real BORDER_WIDTH      = 200
        //MAP BOUNDS
        private real MAP_MAX_X
        private real MAP_MAX_Y
        private real MAP_MIN_X
        private real MAP_MIN_Y
       
        private constant integer ORDER_SMART    = OrderId("smart")
        private constant integer ORDER_MOVE     = OrderId("move")
        private constant integer ORDER_ATTACK   = OrderId("attack")
       
        private constant integer TYPES_KEY      = StringHash("Airplane Types")
        private constant integer INSTANCES_KEY  = StringHash("Airplane Instances")
       
        private hashtable Hash                  = InitHashtable()
    endglobals
   
    native UnitAlive takes unit id returns boolean
   
    //=======================//
    //===== SYSTEM CODE =====//
    //=======================//
   
    function IsUnitPlane takes unit whichunit returns boolean
        return HaveSavedInteger(Hash, TYPES_KEY, GetUnitTypeId(whichunit))
    endfunction
   
    private function RegisterPlaneType takes integer unitid returns nothing
        call SaveInteger(Hash, TYPES_KEY, unitid, 1)
    endfunction
   
    private function InitPlaneTypes takes nothing returns nothing
        //REGISTER YOUR PLANE TYPES LIKE THIS
        call RegisterPlaneType('jetf')
        call RegisterPlaneType('bomb')
        call RegisterPlaneType('rdra')
    endfunction
   
    struct Plane
   
        implement List
   
        unit plane      = null
        unit target     = null
       
        real vx         = 0
        real vy         = 0
       
        real tx         = 0
        real ty         = 0
       
        real speed      = 0
       
        integer order   = 0
       
        private static timer t1
        private static timer t2
       
        private static method get takes unit whichunit returns thistype
            return LoadInteger(Hash, INSTANCES_KEY, GetHandleId(whichunit))
        endmethod
       
        method onDestroy takes nothing returns nothing
            //DON'T GIVE ME CRAP ABOUT USING onDestroy(), IT POSES NO PERFORMANCE ISSUE IN THIS CASE.
            call .listRemove()
            call RemoveSavedInteger(Hash, INSTANCES_KEY, GetHandleId(.plane))
           
            if .count == 0 then
                call PauseTimer(.t1)
                call PauseTimer(.t2)
            endif
        endmethod
       
        private static method updateOrder takes nothing returns nothing
            local thistype this = .first
            local thistype temp
            local real x
            local real y
           
            if .count == 0 then
                call PauseTimer(.t1)
                call PauseTimer(.t2)
            endif
           
            loop
                exitwhen this == 0
                set temp = .next
               
                if GetUnitTypeId(.plane) == 0 then
                    //DESTROY THE STRUCT WHEN THE PLANE NO LONGER EXISTS
                    call .destroy()
                else
                    if .target != null then
                        if UnitAlive(.target) == false or GetUnitTypeId(.target) == 0 then
                            set .target = null
                            set .tx = GetUnitX(.plane)
                            set .ty = GetUnitY(.plane)
                            set .order = ORDER_ATTACK
                        else
                            call IssueTargetOrderById(.plane, .order, .target)
                        endif
                    else
                        if .order == ORDER_MOVE or .order == ORDER_SMART then
                            set x = GetUnitX(.plane)
                            set y = GetUnitY(.plane)
                            if (x - .tx)*(x - .tx) + (y - .ty)*(y - .ty) < 10000 then
                                //ALLOW THE UNIT TO ATTACK ONCE IT HAS REACHED ITS DESTINATION
                                set .order = ORDER_ATTACK
                            endif
                        endif
                       
                        call IssuePointOrderById(.plane, .order, .tx, .ty)
                    endif
                endif
               
                set this = temp
            endloop
        endmethod
       
        private static method updatePosition takes nothing returns nothing
            local thistype this = .first
            local real x         = 0
            local real y         = 0
            local real f         = 0
           
            loop
                exitwhen this == 0  
               
                set f = GetUnitFacing(.plane)*bj_DEGTORAD
               
                //ADD FRICTION FIRST INCASE IT IS ZEROED
                set .vx = (.vx * AIR_FRICTION) + .speed * Cos(f)
                set .vy = (.vy * AIR_FRICTION) + .speed * Sin(f)
               
                set x = GetUnitX(.plane)+.vx
                set y = GetUnitY(.plane)+.vy
               
                //ENFORCE BOUNDS
               
                if x > MAP_MAX_X then
                    set x = MAP_MAX_X
                elseif x < MAP_MIN_X then
                    set x = MAP_MIN_X
                endif
               
                if y > MAP_MAX_Y then
                    set y = MAP_MAX_Y
                elseif y < MAP_MIN_Y then
                    set y = MAP_MIN_Y
                endif
               
                call SetUnitX(.plane, x)
                call SetUnitY(.plane, y)
               
                set this = .next
            endloop
        endmethod
       
        private static method onOrderTarget takes nothing returns boolean
            local thistype this = thistype.get(GetTriggerUnit())
           
            if this == 0 then
                return false
            endif
           
            set .target = GetOrderTargetUnit()
            set .tx = GetUnitX(.target)
            set .ty = GetUnitY(.target)
            set .order = GetIssuedOrderId()
           
            return false
        endmethod
       
        private static method onOrderPoint takes nothing returns boolean
            local real x = GetOrderPointX()
            local real y = GetOrderPointY()
            local thistype this = thistype.get(GetTriggerUnit())
           
            if this == 0 then
                return false
            endif
           
            set .tx = x
            set .ty = y
            set .target = null
            set .order = GetIssuedOrderId()
           
            return false
        endmethod
       
        static method create takes unit whichunit returns thistype
            local thistype this = thistype.allocate()
           
            call .listAdd()
           
            call SaveInteger(Hash, INSTANCES_KEY, GetHandleId(whichunit), this)
           
            //=========SOME MATH TRIVIA!=========//
            //The acceleration is given by the following differential equation:
            // v(t)' = a = v(t) - v(t)*(1-R), where R = AIR_FRICTION
            //And it's solution:
            //v(t) = (a/(1-R)*(1 + e^-(1-R)t)
            //Calculating the limit when (t -> infinity) gives:
            //vmax = a/(1-R)
            //vmax*(1-R) = a (in this case 'vmax' is known and 'a' is unknown)
            //===========END OF TRIVIA===========//
           
            set .plane = whichunit
            set .speed = GetUnitDefaultMoveSpeed(.plane)*(1-AIR_FRICTION)*UPDATE_INTERVAL*SPEED_FACTOR
           
           
            set .tx = GetUnitX(.plane) + 500 * Cos(DEFAULT_FACING*bj_DEGTORAD)
            set .ty = GetUnitY(.plane) + 500 * Sin(DEFAULT_FACING*bj_DEGTORAD)
            set .order = ORDER_SMART
           
            call SetUnitMoveSpeed(.plane, 0.01)
            call SetUnitFacing(.plane, DEFAULT_FACING)
            call SetUnitFlyHeight(.plane, 0, 0)
            call SetUnitFlyHeight(.plane, GetUnitDefaultFlyHeight(.plane), GetUnitDefaultFlyHeight(.plane)/3)
           
            if .count == 1 then
                call TimerStart(.t1, UPDATE_INTERVAL, true, function thistype.updatePosition)
                call TimerStart(.t2, ORDER_INTERVAL, true, function thistype.updateOrder)
            endif
            return this
        endmethod
       
        private static method onUnitEntersMap takes nothing returns boolean
            call thistype.onIndex(GetFilterUnit())
            return false
        endmethod
       
        private static method onIndex takes unit u returns nothing
            if IsUnitPlane(u) then
                call .create(u)
            endif
        endmethod
       
        private static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger()
            local integer i = 0
            local rect r = GetWorldBounds()
            local region map = CreateRegion()
            local group g = CreateGroup()
           
            set MAP_MAX_X = GetRectMaxX(r)-BORDER_WIDTH
            set MAP_MAX_Y = GetRectMaxY(r)-BORDER_WIDTH
            set MAP_MIN_X = GetRectMinX(r)+BORDER_WIDTH
            set MAP_MIN_Y = GetRectMinY(r)+BORDER_WIDTH
           
            set Plane.t1 = CreateTimer()
            set Plane.t2 = CreateTimer()
           
            call InitPlaneTypes()
           
            loop
                exitwhen i > MAX_PLAYERS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, null)
                set i = i+1
            endloop
            call TriggerAddCondition(t, Condition(function Plane.onOrderTarget))
           
            set t = CreateTrigger()
            set i = 0
           
            loop
                exitwhen i > MAX_PLAYERS
                call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
                set i = i+1
            endloop
            call TriggerAddCondition(t, Condition(function Plane.onOrderPoint))
           
            static if (LIBRARY_AutoIndex) then
                call OnUnitIndexed(Plane.onIndex)
            else
                set t = CreateTrigger()
                call RegionAddRect(map, r)
                call TriggerRegisterEnterRegion(t, map, function Plane.onUnitEntersMap)
               
                set i = 0
                loop
                    exitwhen i > MAX_PLAYERS
                    call GroupEnumUnitsOfPlayer(g, Player(i), function Plane.onUnitEntersMap)
                    set i = i+1
                endloop
            endif
           
            call DestroyGroup(g)
            call RemoveRect(r)
            set g = null
            set r = null
            set t = null
            set map = null
        endmethod
   
    endstruct

endlibrary

UPDATE 1:
- Made most struct methods private.
- The system now uses GetUnitTypeId(whichunit) == 0 to check if a unit no longer exists.

UPDATE 2:
- The system now uses AutoIndex instead of AutoDex. Various other fixes to satisfy the moderators demands.

UPDATE 3:
- AutoIndex is now optional.
- Now supports 24 players.

UPDATE 4:
- Removed all custom models from the map, so that people won't be confused as to what to import.
- Map border width is now customizeable.

UPDATE 5:
- Various small changes to conform with moderator's requests


Keywords:
Airplane, inertia, flying, air, fighter, ship
Contents

Realistic Airplane System (Map)

Reviews
MyPad
The realistic airplane system is a resource that few users expect. As a somewhat original system (this may be changed later), this deserves some recognition on that front. It forces the units registered to the system to move in an airplane-like...
Level 3
Joined
Aug 25, 2018
Messages
29
The errors aren’t from the system. JASSHelper is no longer enabled by default because the Reforged team doesn’t know what they’re doing.

In the trigger editor navigate to the top bar and make sure JASSHelper > Enable JASSHelper as well as JASSHelper > Enable vJASS are both checked on.

Thank you so much man!!! For. Real.
 
Top