Artillery

From Second Life Wiki
Jump to: navigation, search

Created by Kira Komarov.

Motivation

llMoveToTarget implies a continuous dampening force that propels an object towards its destination. That got me thinking about simulating a typical artillery gun using simple physics freefall mechanics motion equations. By creating an artillery gun, the whole simulation is entirely correct and does not rely on continuous applications of forces, such as dampening and propulsion.

Furthermore, this sort of artillery gun might be familiar to you from games such as Worms or Tank Wars where the objective is given to destroy the enemy artillery gun. In those games, the skill relies in intuitively guessing the velocity and angle at which a projectile must be fired in order to impact the target. This script however, performs all the calculations and there is no skill requirement. It is, of course, possible to implement an artillery firing game based on this script, but that is out of the scope of this article.

Usage

  1. Create a primitive which will play the role of the artillery gun. More precisely, this primitive will be the nozzle or the point of departure for the fired projectile. You may also attach this primitive to yourself and make yourself a mobile artillery gun - the script recalculates dynamically on each fired round making it suitable for a mobile application.
  2. Create some projectiles. These can be primitives (including sculpts) which will be placed inside the artillery gun you created at step 1. There are no restrictions here and you can create as many projectiles as you wish because the script will read the contents of the inventory and allow you to browse between them. When creating projectiles, keep in mind that the chosen projectile parameters (such as size) will influence the projectile flight, as well as the impact force against the target. More precisely, when the projectile descends, it is influenced by the gravitational force, which is a multiplicative combination between mass and velocity. The greater the mass of the projectile, the greater the impact force on your designated target. After you have designed your projectiles, take them to inventory and drop them inside the artillery gun.
  3. Touch the gun and go through the firing configuration formalities and the artillery gun will fire your chosen projectile based on your choices.

Features and Clarifications

The gun asks for:

  1. The projectile name to be fired from the artillery gun's inventory. Endless menu based, allowing finitely many projectiles to be loaded into the artillery gun.
  2. The target avatar that the projectile should hit. Sensor scan based, up to 96m.
  3. The amount of charges to fire. Supporting continuous fire. The firing interval is given by the last fired projectiles estimated flight time, plus the region time dilation.
  4. The firing velocity in meters per second.
  5. A choice between the two complementary theta angles. These two angles will both hit the target, provided that the velocity is sufficient. However, the "High" theta angle might be useful to hit a target in case there are obstacles between the artillery gun and the designated target. A "Low" theta angle will attempt a direct, gravity compensated, shot at the designated target.
  • The projectile, being physical, is susceptible to wind which will induce an error and might deviate the projectile's trajectory. If your designed projectiles have a low mass (directly given by the object's size), then this might be an issue. To counter wind effects, increase the object's mass and/or increase the firing velocity.

Background

Calculating the grid-distance

In order to calculate the distance between the artillery gun and the designated target avatar, we must factor out the elevation differential and then apply llVectDist() to the two positional vectors. Formally, this is done like so:

float dΔ=llVecDist(<target.x,target.y,0>, <origin.x,origin.y,0>);

that is, the grid-distance is given by the x,y component differential between the target and the origin.

Calculating the maximum height of the trajectory

From the basic motion equation:

y = v_y * t

we know that the average vertical speed is half of the vertical speed:

v_y = v_0y / 2

When the projectile reaches the apex of the flight path, its vertical speed will be 0. Makes sense no? If it has reached its maximum height, it will not have any upward speed anymore. We take thus the second equation and set v_y = 0, thus:

v_0y - g * t_peak = 0

solving for t_peak, we have:

t_peak = v_0y / g

substituting in the basic motion equation, we obtain:

h_max = y_peak = v_0y^2/(2 * g)

Calculating the required firing angle based on the chosen velocity

From the general motion equations, we have:

x = v_0x * t y = v_0y * t - gt^2/2

where v_0x, v_0y represent the velocity on the x, respectively y axis. t represents the time and g the linden gravitational force which is currently 9.81 m/s.

Since the fired projectile's flight path will be represented by a curve, we know that the total time of flight will be equal to twice the time at the peak. Thus, we have that:

t_range = 2*t_peak = 2v_0y/g

we substitute into the first equation and obtain the distance as:

dΔ = 2 * v_0x * v_0y / g

since v_0x and v_0y are vectors, we use the vectorial product and push it further to:

dΔ = 2 * v_0^2 * sinΘcosΘ / g

the trigonometric identity tells us that sin2Θ = 2sinΘcosΘ, thus we finally obtain that the range equation is:

dΔ = 2 * v_0^2 * sin2Θ / g

Now, we know the range dΔ since we have already calculated it as the distance between the artillery gun and the designated target avatar. We also know g, the gravitational force since it is given by linden to be 9.81 m/s. The user is also prompted to choose the velocity v0, so we will know that too. This leaves us to extract the theta Θ angle which is required for the projectile to hit the target. In order to do that, we reverse the equation:

dΔ = 2 * v_0^2 * sin2Θ / g

switching over the equal sign, we obtain:

sin2Θ = dΔ * g / v_0^2

which means that:

2Θ = arcsin(dΔ * g / v_0^2)

thus,

Θ = arcsin(dΔ * g / v_0^2)/2

Now, we know everything on the right hand side. By substituting all the parameters, we will obtain the first theta angle required to hit the designated target avatar (in my improvised terminology, the "Low" angle). To obtain the high angle, since it is complementary to 45 deg. secant, we simply subtract the theta "Low" angle from 90 degrees and careful to convert the obtained radian angles to degrees. Thus:

low Θ = arcsin(dΔ * g / v_0^2)/2 * RAD_TO_DEG

and

high Θ = 90 - arcsin(dΔ * g / v_0^2)/2 * RAD_TO_DEG

are the two possible angles required to hit the target. Why? Think about it intuitively, you can throw a projectile directly at a target, but you can also throw it towards the sky and wait for it to descend and hit the target as well. Thus, there are two possible angles at which you might throw a projectile. What's the difference? Well, it depends on your intentions.

Suppose that you have a gun and the you shoot towards some target. Now suppose that you have a resistance force on the projectile's firing direction and working in reverse. For example, there some wind source that tries to slow down your projectile. That wind force would alter the velocity of your bullet. However, a gun's blast power is greatly superior to whatever wind force may act against the bullet. Thus, it would not be necessary to chose the "High" angle - in fact, it would be intuitively a bad choice since friction will, in fact slow down the bullet given its small mass.

On the other hand, if you are a pirate and you want to sink a ship at a distance, given a heavy cannonball, you might want to chose the "High" angle and benefit from gravity to accelerate your projectile. I have not calculated the impact force in the script, however if you wish to do so:

F_impact = mass_of_projectile * velocity_at_impact ^2 / 2

where:

velocity_at_impact = SquareRoot( 2 * g * h_max);

Which basically represents the kinetic force (given that mass is measured in lindograms, the force would be measured in lindograms-force) with which the projectile will impact the target. As you can see, the impact force F_impact exponentially increases with the velocity at impact and the mass of a projectile. For a bullet, the mass is small, the velocity great. For a pirate cannon, gunpowder wasn't able to deal too much of a velocity to a cannonball, however a cannonball was rather heavy so it benefited more from freefall than from velocity.

Can I always hit a target?

Nope. Of course not, since you might not have sufficient velocity. Here's formally why. Recall the two-theta angle:

2Θ = arcsin(dΔ * g / v_0^2)

The sinus function's value domain is between -1 and 1. That means, that sin(x), for an arbitrary chosen x will give you a value that is between -1 and 1 (inclusive). Whatever you plug as x into sin(x), you will always get something that is in that range. Thus, if you take angle = arcsin(y), that means that if y is not between -1 and 1, then it is not in the domain of values of the sinus function and there is no possible angle at which the projectile can be fired in order to hit the target.

Intuitively (and hilariously), it's like saying, you only wear red, yellow and blue shoes. You are now wearing something green on your feet. However, via the hypothesis, since you only wear red, yellow and blue shoes, then that green stuff on your feet are not shoes.

Back to formalism:

value_domain(dΔ * g / v_0^2) = [-1, 1]

for your projectile to hit the target.

Projectile Considerations

  • As mentioned previously, the larger and thus heavier the projectile, the heavier the impact.
  • You might want to consider making the projectiles temporary so they will de-rez after a while.

Code

//////////////////////////////////////////////////////////
// [K] Kira Komarov - 2011, License: GPLv3              //
// Please see: http://www.gnu.org/licenses/gpl.html     //
// for legal details, rights of fair usage and          //
// the disclaimer and warranty conditions.              //
//////////////////////////////////////////////////////////
 
//////////////////////////////////////////////////////////
//                     INTERNALS                        //
//////////////////////////////////////////////////////////
 
string object = "";
integer person = 0;
list peopleK = [];
list peopleN = [];
integer comHandle = 0;
list menu_items = [];
integer mitra = 0;
list cList = [];
integer numObjects = 0;
float V0 = 0;
string angle = "";
 
list mFwd() {
    if(mitra+1>llGetListLength(menu_items)) return cList;
    cList = llListInsertList(llListInsertList(llList2List(menu_items, ++mitra, (mitra+=9)), ["<= Back"], 0), ["Next =>"], 2);
    return cList;
} 
 
list mBwd() {
    if(mitra-19<0) return cList;
    cList = llListInsertList(llListInsertList(llList2List(menu_items, (mitra-=19), (mitra+=9)), ["<= Back"], 0), ["Next =>"], 2);
    return cList;
}
 
default
{   
    touch_start(integer total_number)
    {
        if(llDetectedKey(0) != llGetOwner()) return;
        integer itra;
        list objects = [];
        for(itra=0, person=0, object="", objects=[], cList=[], menu_items=[], mitra=0; itra<llGetInventoryNumber(INVENTORY_OBJECT); ++itra) {
            menu_items += llGetInventoryName(INVENTORY_OBJECT, itra);
        }
        cList = llListInsertList(llListInsertList(llList2List(menu_items, mitra, (mitra+=9)), ["<= Back"], 0), ["Next =>"], 2);
        integer comChannel = ((integer)("0x"+llGetSubString((string)llGetOwner(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        comHandle = llListen(comChannel, "", llGetOwner(), "");
        llDialog(llGetOwner(), "Please choose an object from the inventory:", cList, comChannel);
    }
 
    changed(integer change)
    {
        if (change & CHANGED_OWNER)
            llResetScript();
    }
 
    listen(integer channel, string name, key id, string message) {
        if(message == "<= Back") {
            llDialog(id, "Please browse and select:\n", mBwd(), channel);
            return;
        }
        if(message == "Next =>") {
            llDialog(id, "Please browse and select:\n", mFwd(), channel);
            return;
        }
        integer comChannel = ((integer)("0x"+llGetSubString((string)llGetOwner(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        if(channel == comChannel) {
            object = message;
            llSensor("", "", AGENT, 96, TWO_PI);
        }
        if(channel == comChannel+1) {
            person = (integer)message;
            list am = [];
            integer itra;
            for(itra=1; itra<12; ++itra) {
                am += (string)itra;
            }
            am += "Continuous";
            llListenRemove(comHandle);
            comHandle = llListen(comChannel+2, "", llGetOwner(), "");
            llDialog(llGetOwner(), "Please select an amount of charges to fire:\n", am, comChannel+2);
            return;
        }
        if(channel == comChannel+2) {
            if(message == "Continuous")
                numObjects = -1;
            else
                numObjects = (integer)message;
            integer itra;
            list am = [];
            for(itra=1, cList=[], menu_items=[], mitra=0; itra<1200; ++itra) {
                menu_items += (string)itra;
            }
            cList = llListInsertList(llListInsertList(llList2List(menu_items, mitra, (mitra+=9)), ["<= Back"], 0), ["Next =>"], 2);
            llListenRemove(comHandle);
            comHandle = llListen(comChannel+3, "", llGetOwner(), "");
            llDialog(llGetOwner(), "Please select the velocity in meters/second:\n", cList, comChannel+3);
            return;
        }
        if(channel == comChannel+3) {
            V0 = (integer) message;
            llListenRemove(comHandle);
            comHandle = llListen(comChannel+4, "", llGetOwner(), "");
            llDialog(llGetOwner(), "Please the possible angles:", ["High", "Low"], comChannel+4);
            return;
        }
        if(channel == comChannel+4) {
            angle = message;
            state fire;
        }
        llListenRemove(comHandle);
    }
 
    sensor(integer num) {
        integer itra;
        for(itra=0, peopleK=[], peopleN=[]; itra<13; ++itra) {
            if(llDetectedKey(itra) != NULL_KEY) {
                peopleK += llDetectedKey(itra);
                peopleN += (string)itra + ".) " + llDetectedName(itra);
            }
        }
        integer comChannel = ((integer)("0x"+llGetSubString((string)llGetOwner(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        comHandle = llListen(comChannel+1, "", llGetOwner(), "");
        list val = [];
        for(itra=0; itra<llGetListLength(peopleN); ++itra) {
            val += (string) itra;
        }
        llDialog(llGetOwner(), "Please select a target:\n"+llDumpList2String(peopleN, "\n"), val, comChannel+1);
    }
}
 
state fire {
    state_entry() {
        llSetTimerEvent(1.0);
    }
    timer() {
        vector target = llList2Vector(llGetObjectDetails(llList2Key(peopleK, person), [OBJECT_POS]), 0);
        vector origin = llGetLocalPos();
        float=llVecDist(<target.x,target.y,0>,<origin.x,origin.y,0>);
        float valSin = 9.81*/llPow(V0, 2);
        if(valSin < -1 || valSin > 1) {
            llSetTimerEvent(0);
            llOwnerSay("Not enough velocity to reach target, please increase velocity.");
            state default;
        }
        float low_Θ = RAD_TO_DEG*llAsin(valSin)/2;
        float high_Θ = 90-low_Θ;
        rotation= ZERO_ROTATION;
        if(angle == "High")= llRotBetween(<1,0,0>,llVecNorm(<target.x-origin.x,target.y-origin.y, dΔ*llTan(high_Θ * DEG_TO_RAD) + llFabs(target.z-origin.z)>));
        else= llRotBetween(<1,0,0>,llVecNorm(<target.x-origin.x,target.y-origin.y, dΔ*llTan(low_Θ * DEG_TO_RAD) + target.z-origin.z>));
        float t = 2*V0/9.81;
        float h_max = llPow(V0, 2)*llPow(llSin(high_Θ), 2)/(2*9.81);
        llOwnerSay("---------- FIRE ROUND START ----------");
        llOwnerSay("+Distance to target: " + (string)+ "m");
        llOwnerSay("+Required angle delta to hit: " + (string)low_Θ + "˚");
        llOwnerSay("+Orientation detlta to target: " + (string)+ "m");
        llOwnerSay("+Time to impact: " + (string)t + "s");
        llOwnerSay("+Maximum flight height: " + (string)h_max + "m");
        llOwnerSay("----------- FIRE ROUND END -----------");
        llRezObject(object, llGetLocalPos(), llVecNorm(<1,0,0>*)*V0, qΔ, 0);
        if(~numObjects != 0) {
            if(--numObjects == 0) {
                llSetTimerEvent(0);
                return;
            }
        }
        llSetTimerEvent(t + llGetRegionTimeDilation());
    }
    touch_start(integer num) {
        if(llDetectedKey(0) != llGetOwner()) return;
        llOwnerSay("Firing ceased, touch to reconfigure.");
        state default;
    }
}