Ready. Aim. FIRE!

If you've ever programmed a game, then you know that physics is critical to realistic play.

No, I am not going to explain how to build a physics engine. I just started with the basics.

How to Launch Things

Whether it is catapults, tanks or worms with slingshots, the most important thing is: whatever goes up must come down... specifically in a parabolic trajectory.

Now, computers are smart, but they're also dumb. It would be nice if we could just tell it a speed and an angle and have it determine everything else. OR give it an x,y coordinate and have it calculate the speed and angle. But computers don't solve algebra equations unless we tell them how. They just plug-n-chug input and give output.

That means we have to do the algebra ourselves. *Sigh*

Programming basic kinematics is the easy part. We provide the initial position, speed, angle and time since "launch" and we can determine the speed, direction and position. Do this 60 times a second and we can animate it. If you need a refresher on the big four, I recommend Khan Academy.

The only real challenge making this app was solving the algebra to adjust the angle of launch based on a selected (arbitrary) target. If you plug the final position into the big four and solve for the angle, you get something pretty ugly. A few trigonometric identities and a quadratic formula later we get this code:
//global declarations
   var g = -9.80665;
   var time = 0; //time
   var v0 = 10;//initial velocity (muzzle)
   var theta = Math.PI/4;//trajectory angle
   var x0 = 0;//initial position
   var y0 = 0;
   var target = {"X":0,"y":0,"active":false};

//x and y are the coordinates that were clicked by the user
function defineTrajectory(x,y){
    tanTheta1 = quadraticFormula(g*x*x/(2*v0*v0),x,-(y-y0)+(g*x*x/(2*v0*v0)),1);
    tanTheta2 = quadraticFormula(g*x*x/(2*v0*v0),x,-(y-y0)+(g*x*x/(2*v0*v0)),-1);
    //two solutions, so we pick positive first
    if (tanTheta1 > 0) theta = Math.atan(tanTheta1);
    else theta = Math.atan(tanTheta2);
    //this updates the visual layout to reflect the angle
    $('#angle').val(theta*180/Math.PI);
    //this tells us where to draw the big red target
    target.x = x;
    target.y = y;
    target.active = true;
    //then we calculate the rest of the trajectory
    //using the big four and then draw the scene
    calcValues();
    drawScene();
    drawTrajectory();
}
function quadraticFormula(a,b,c,sign){
    if (b*b-(4*a*c) < 0) return false;//imaginary solution
    return (-b+(sign*Math.sqrt(b*b-(4*a*c))))/(2*a);
}

What happens if there isn't enough speed to reach the selected point, no matter the angle? I'll leave that for someone else to decide. On to the next project! Orbital trajectories!

What's your vector, Victor?

If you have studied physics in 3 dimensions, then you know what a pain it is to do vector math. It's abstract, it's tricky to draw on paper...


So I devised a little aid. It's much easier to visualize the sum of Force and Moment (torque) vectors when you can actually turn it around, and compare at different angles.

With a little help from WebGL, it wasn't too hard to draw some axes, planes and arrows (although adding text was a bit trickier).

This little script lets you create up to 6 forces (or moments) and it will sum them into a resultant force and moment. If the resultants are 0, then the forces are in equilibrium. This is very handy if you need to check your homework, of course. However, it won't solve it for you... unless you are looking for a single force to cancel the existing ones!

Features so far:
  • add up to 6 colored vectors
  • rotate the graph 90 degrees in each direction (click the graph once to unlock, again to lock)
  • scale the graph
  • calculates resultant force and moment vector displayed in white
Planned features:
  • adding larger number of vector inputs
  • animated vectors
  • unit adjustments, degrees and radians
  • vector solver...? (to determine unknown value of a vector with known direction)
The app currently runs on glMatrix 0.9.5. Next version will likely be updated to 2.0 (more info on http://glmatrix.net). It is a nice library already optimized for use with WebGL.

Enjoy the app. I use it to check my homework...

Feel free to fork my repo on GitHub and let me know if you have any feature suggestions or bugs!

A Random Post

It's been awhile so I figured: it is time for a random post. Seriously, this is a post about random numbers.

If you code in Javascript, then Math.random() is probably already familiar to you. It does a nice job of generating (pretty) uniform variables from 0 to 1. A little multiplying and rounding and you can get integers from 0 to x, or -x to x, or whatever your needs.

But what if you want NON-uniform randoms? For statistics freaks: what if you need normal distribution? or Gamma? or Beta? Exponential? Chi-squared?

Well you are in luck! I found myself in need of some common distributions for a little side game I'm making, and went the extra mile to read some thesis papers about how to generate these useful distributions.

Rather than bore you with how it all works, here is the code for a RANDOM object you can insert into your code. I'm still working on Betas, they are bit trickier.

RANDOM.norm(n) 

This returns an (approximately) normal bell curve centered on mean 0.5. The parameter "n" can be between 2 and 5 (default 2). Higher values of "n" makes the bell curve more narrow. The value returned is between 0 and 1.

RANDOM.inorm(n) 

This returns an inverse bell curve centered on mean 0.5. The parameter "n" can be between 2 and 5 (default 2). Higher n makes output more likely to be near 0 or 1.

RANDOM.exp(n) 

A nice easy exponential variable from 0 to 1. Increasing "n" makes the graph skewed towards 0. Default value for "n" is 2.

RANDOM.invexp(n) 

This is just RANDOM.exp(n) times -1.

RANDOM.gamma(alpha,beta) 

This is a conglomeration of a few generators. For alpha > 1 and < 0, it uses an algorithm developed by Kundu and Gupta. No beta is used for this algorithm.

For alpha >= 1 it uses the algorithms proposed by Martino and Luengo. It generates very quick results due to a low rejection rate. Default values are 1 and 1.

I could not have completed this without those great algorithms! If you use this code, please leave the credits they deserve.

TIP: RANDOM.gamma(2,2) returns a very nice gamma distribution. Be aware: the values returned may be higher than 1 (especially for high alpha and beta values). Alpha values below 1 resemble exponential distributions.

RANDOM.igamma(alpha,beta) 

Simply RANDOM.gamma() times -1.

RANDOM.chi2(k) 

A commonly used distribution resembling gamma distributions (I use this one a lot). Again, returned values may be higher than 1, especially for high "k".

RANDOM.coinFlip(weight) 

A handy weighted coin flip that returns 0 or 1. Default weight is 2 (50/50) and MUST be > 1. Weights over 2 favor a positive outcome (i.e. weight of 3 will have 2:3 odds of returning 1).

The code:

As usual, the code is free to use under the GNU. Please give credit though, and I'd love to hear how this has been useful for anyone! Also, feel free to play around with the fiddle! (The code is adjusted slightly for display).

I'll post a follow-up if I ever get around to adding Beta randoms too!

var RANDOM = { //returns random between 0 and 1, normal distribution centered on 0.5
    "norm": function(n) {
        if (!(n > 2 && n <= 5)) n = 2;
        var nrand = 0;
        n = Math.floor(n);
        for (var i = 1;i<=n*2;i++){
            nrand += Math.random();
        }
        return nrand/(2*n);
    },
    "inorm": function(n) { //returns random between 0 and 1
        if (!(n > 2 && n <= 5)) n = 2;
        var nrand = 0;
        n = Math.floor(n);
        for (var i = 1;i<=n*2;i++){
            nrand += Math.random();
        }
        return ((1 - Math.abs((nrand-n) / n))*(Math.abs(nrand-n)/(nrand-n)) + 1)/2;
    },
    "exp": function(n) { //returns greater than 0
        if (!(n > 2 && n <= 5)) n = 2;
        var nrand = Math.random();
        for (var i = 2;i<=n;i++){
            nrand *= Math.random();
        }
        return 2*nrand;
    },
    "invexp": function(n) { //returns less than 0
        return -RANDOM.exp(n);
    },
    "gamma3": function(alpha) { //Kundu and Gupta algorithm 3 http://home.iitk.ac.in/~kundu/paper120.pdf
        if (!alpha || Math.abs(alpha) > 1) alpha = 1; //alpha between 0 and 1
        var d = 1.0334 - (0.0766*Math.exp(2.2942*alpha));
        var a = Math.pow(2,alpha)*Math.pow(1-Math.exp(-d/2),alpha);
        var b = alpha*Math.pow(d,alpha-1)*Math.exp(-d);
        var c = a + b;
        var U = Math.random();
        var X = (U <= a/(a+b)) ? -2*Math.log(1-(Math.pow(c*U,1/alpha)/2)) : -Math.log(c*(1-U)/(alpha*Math.pow(d,alpha-1)));
        var V = Math.random();
        if (X <= d) {
            var mess = (Math.pow(X,alpha-1)*Math.exp(-X/2))/(Math.pow(2,alpha-1)*Math.pow(1-Math.exp(-X/2),alpha-1));
            if (V <= mess) return X;
            else return this.gamma3(alpha);
        } else { //X > d
            if (V <= Math.pow(d/X,1-alpha)) return X;
            else return this.gamma3(alpha);
        }
        //output is > 0 and possibly > 1
    },
    "gamma": function(alpha,beta) { //Martino and Luengo http://arxiv.org/pdf/1304.3800.pdf luca@tsc.uc3m.es luengod@ieee.org
        if (!alpha || alpha <= 0) alpha = 1; //alpha >= 1 if negative or 0
        if (alpha > 0 && alpha < 1) return this.gamma3(alpha); // use different algorithm
        if (!beta || beta <= 0) beta = 1; //beta > 0
        var alphap = Math.floor(alpha);
        var X = Math.random();
        for (var i=2;i<=alphap;i++){
            X *= Math.random();
        } 
        var betap = (alpha < 2) ? beta/alpha : beta*(alphap-1)/(alpha-1);
        X = -Math.log(X)/betap;
        var Kp = (alpha < 2) ? Math.exp(1-alpha)*Math.pow(alpha/beta,alpha-1) : Math.exp(alphap-alpha)*Math.pow((alpha-1)/beta,alpha-alphap);
        //then accept with prob p(X)/pi(X)
        if (alphap >= 2) {
            if (Kp*Math.pow(X,alphap-1)*Math.exp(-betap*X) >= Math.pow(X,alpha-1)*Math.exp(-beta*X)) return X/alpha;
                else return this.gamma(alpha,beta);
            }
        else if (alphap < 2) {
            if (Kp*Math.exp(-betap*X) >= Math.pow(X,alpha-1)*Math.exp(-beta*X)) return X/alpha;
            else return this.gamma(alpha,beta);
        }
    },
    "igamma": function(alpha,beta) { // returns less than 0
        return -RANDOM.gamma(alpha,beta);
    },
    "chi2": function(k) { // returns greater than 0
        var nrand = RANDOM.norm(2);
        nrand = nrand*nrand;
        if (!k || k <= 1) return nrand;
        for (var i=2;i<=k;i++){
            var krand = RANDOM.norm(2);
            krand = krand*krand;
            nrand += krand;
        }
        return nrand;
    },
    "coinFlip": function(weight){
        if (!weight || weight < 1) weight = 2;
        if (Math.random() > 1/weight) return 1;
        else return 0;
    }
};
//Copyright 2016 Trevor Gast: codeandcompose.com
//RANDOM, non-uniform random generators (5 May 2016)
//GNU General Public License

Dice gif courtesy of http://bestanimations.com/Games/Dice/Dice.html

Take to the skies! Take the skies... recursively.

If the game has been too much fun for you...

I kid. I'll finish it someday. Probably.

In the meantime, I've been taking an Introduction to Aeronautical Engineering course on edx.org. It's a great course (and free) from Delft Technical University. If you are interested in Aerospace, start there. It'll help you determine if you like math enough...

It turns out: I do like math.

So much so, that I programmed this International Standard Atmosphere Calculator. Using the 1976 international standard (which apparently hasn't changed since) you can plug in your geopotential altitude and get the air density and pressure. Very handy if you want check your flight altitude to make sure you won't collide with any other airplanes. Or if you need to do your Aerospace homework.

I added a nice graph, so you can just click the altitude rather than typing it. I hope to add unit conversions and also altitude prediction based on pressure or air density. One thing at a time.

Programming this little job also gave me a good excuse to use a recursive function.

Re...CURSES!

If you have ever taken a programming course, you've been taught about recursion. And then promptly forgotten it thinking, "that's too confusing! I'll just do it a different way..."

And you can solve most problems without ever using recursion. So why bother right?

Because it is so much TIDIER!

In the case of ISA, we have a piece-wise function that stacks on more function pieces as we progress to higher altitudes. Lapse rates change and we glide through isothermal layers... nevermind. Back to recursion!

Without getting too into depth on ISA calculations, let's just say this:
If we want to calculate the air density for an altitude of 40 kilometers, we need to first calculate it for 11 using one equation, then up to 20 with another, then up to 32; all based on the calculations made for lower layers.
If we want to calculate at higher altitudes, we need even MORE equations dependent on all those below.

So how does recursion help?

Recursion allows us to call one function for any altitude, that calls itself for the lower calculations. Then it spits out just the answer we need.

Without recursion, we'd have to write separate if.. then... statements for each case (retyping a lot of the same code). OR (as some ISA calculators do) we could cheat and store the key values for each layer in an array. But what's the fun in that?

Here's the commented code:

//this function takes two parameters:
//alt is altitude in meters
//To is temperature offset in Kelvin
function getAtmosphere(alt,To) {
    // constants!
    var R = 287.058; //gas constant
    var TEMP = 288.15; //in Kelvin
    var ATM1 = 101325; //in Pascals
    var DENSITY = 1.225; //in kg/cubic meter

    // range check (altitude in meters)
    if (alt > 84852) alt = 84852;
    if (alt < 0) alt = 0;

    //this is our OUT. no infinite recursion!
    //it returns 1 standard atmosphere at sea level in an object
    if (alt === 0) { //sea level
        return {"alt":alt,"T":TEMP+To,"To":To,"p":ATM1,"rho":DENSITY};
    } 

    // THIS IS WHERE THE MAGIC HAPPENS!
    // the function calls ITSELF...
    var atm0 = getAtmosphere(getAltRange(alt),To);
    // getAltRange() is a simple function that returns the altitude for
    // the "layer" beneath. This is used to find the correct lapse rate
    // and equation below...

    // lapseRate() returns the rate used to calculate temp for a given layer
    // here is where the calculation is done.
    // notice it uses output from itself (atm0)
    if (lapseRate(alt) != 0) {
        var T1 = atm0.T + lapseRate(alt)*(alt-getAltRange(alt));
        var rho1 = atm0.rho*Math.pow(T1/atm0.T,(-9.80665/(lapseRate(alt)*R)-1));
        var p1 = atm0.p*Math.pow(T1/atm0.T,-9.80665/(lapseRate(alt)*R)); 
    } else { // lapseRate = 0
        var con = Math.pow(Math.E,-9.80665/(R*atm0.T)*(alt-getAltRange(alt)));
        var T1 = atm0.T
        var p1 = atm0.p * con;
        var rho1 = atm0.rho * con;
    }

    // calculations "complete" it returns the data as an object
    return {"alt":alt,"T":T1,"To":To,"p":p1,"rho":rho1};
}
So wait? How can a function call itself and then use its own output to calculate. If it doesn't have the input... how can it give output... wouldn't it fail or return 'undefined'??

Well, no. Let's look at an example:

Let's say we call getAtmosphere(25000).

The function needs input from getAtmosphere(20000), so it calls that and waits...

THAT call needs input from getAtmosphere(11000), so it calls that and waits...

THAT call needs input from getAtmosphere(0), so it calls it and... WAIT! That's the out!

Once it calls that, it gets data returned! This is the KEY to recursion. It hits an end, now it can finish!

Then, getAtmosphere(11000) takes the input from (0) and runs the calculation. It passes that to (20000) which calculates and passes to (25000). And like magic, we get our data.

And no matter what altitude we call it on, it knows just how many times to call itself to get the right answer! Brilliant!

I think I've waited 15 years to have a need for recursion. And I'm proud to say I only froze my browser once...

Don't forget the OUT!

EDIT:

I have updated the calculator to properly return the density when there is a temperature offset. You can now change units and get the temperature up to 1000 km. A few other minor adjustments for accuracy too. Coming soon: density and pressure over 86 km!

Also, feel free to fork on GitHub!

Comedy is all about... timing.

In the case of my game concept, so is user input.

"So what is this crazy game concept you keep talking about?"

Here it is:

Do you remember Super Mario RPG and timed hits? It was a fun addition to the usual turn-based RPG style game that added an extra layer of player interaction.

It was a nice touch that made the game very unique, in my opinion. I was surprised that it didn't seem to catch on. But it got me thinking...

I'm a music composer and sound designer; why not combine those skills with the concept of timed hits?

For this demo, I (quickly) made some sound effects using musical sounds. There is a background layer to keep time -- a simple 4/4 beat. When you send a character to attack, he first must arrive close enough to attack, and then he begins "priming".

During this "priming" phase, the character will pulse in time with the music. Then you click on the target and if you are on the beat, you get an extra power boost. Yay!

Depending on other factors, there will be other ways to maximize attacks. However be aware, in this demo, the enemies can fight back if you get too close!

Music, Audio and Timing


Before we can time our hits, we need to time our audio! For this we use the Web Audio API. I learned from this great information here.

For those of you into instant gratification: here is the code used to schedule the music.

var lastBeatTime = null;
var lastBeat = null;
var nextBeatTime = null; // when will the next beat occur
var nextBeat = null; // 32 total (8 bars of 4)
var nextFrameTime = null; // when will the next frame draw?
// tempo (in beats per minute)
var tempo = 125.0;
// these must be recalculated if the tempo changes 
var secondsPerBeat = 60.0 / tempo;
var framesPerBeat = 3600 / tempo;

var calibration = -0.001; 
// use this to fudge the click time back for accuracy (clicking late "sounds" correct)

window.onload = function (){
 
    //make sure the webkit Audio API is available
    try {   // Fix up for prefixing
        window.AudioContext = window.AudioContext||window.webkitAudioContext;
        audioctx = new AudioContext();
    } catch(e) {
        alert('Web Audio API is not supported in this browser');
    }

    //load the audio (code for this class is below)
    gMusic.parseAudioFile();
}

function startMusic(){
    secondsPerBeat = 60.0 / tempo;
    framesPerBeat = 3600 / tempo;
    nextBeat = 32;
    lastBeat = 31;
    nextBeatTime = audioctx.currentTime;
    lastBeatTime = nextBeatTime - secondsPerBeat;

}

function scheduler(time){
    //schedule the next beat
    lastBeat = nextBeat;
    lastBeatTime = nextBeatTime;
    if (nextBeat === 16) nextBeat = 1;
        else nextBeat++;
    nextBeatTime += 60.0 / tempo; // schedule the next quarter note
    scheduleSound(nextBeat,nextBeatTime);
}

function scheduleSound(beatNumber,time){
    // create an oscillator -- it makes the beep
    var osc = audioctx.createOscillator();
    osc.connect( audioctx.destination );
    if (beatNumber % 32 === 1) {
        gMusic.playSound("Pad1.wav",time);
  }
    if (beatNumber % 16 === 1) {
        gMusic.playSound("Beat1.wav",time);
        gMusic.playSound("Drums3.5.wav",time + (secondsPerBeat*2.5));
    }
}

function animate(){
    var NOW = audioctx.currentTime; //gets the time of the last draw...when is now?
    nextFrameTime = NOW + (1 / 60); // calculate when this frame draw will occur
    if (nextBeatTime <= nextFrameTime) scheduler(nextFrameTime);
    //calculate the part of the beat the frame will draw at (as a percentage)
    var bp = Math.round((nextFrameTime - lastBeatTime) / (nextBeatTime - lastBeatTime) * 100);
      
    // ALL OTHER ANIMATION OCCURS
}

gMusic is an instance of an AudioFontClass I created. More on that later, but this is the playSound function it uses when scheduling audio.

playSound: function (name,time) {
    //first find the buffer
    var b = null;
    for(var i = this.sounds.length - 1; i >=0; i--) {
        // find the buffer for the name
        if(this.sounds[i].name === name) b = this.sounds[i].buffer;
    }
    // if we don't find the sound, do nothing
    if (!b) return;

    var soundSource = audioctx.createBufferSource();
    //schedule the sound
    soundSource.buffer = b;
    soundSource.connect(audioctx.destination);
    soundSource.start(time);
} 

How does it work?

Simple: each animation frame checks to see if the next frame will happen after the current beat. If it will, it calls the scheduler to schedule the audio for the next beat. The scheduled keeps track of which beat it is on, and loops the music every 4 bars (or 1 or 2).

The nice thing about the Web Audio API is that you can schedule audio way ahead of time, so nothing plays late.

However, one of the future challenges in making this web game will be keeping the load time down. Too much audio means longer load times.

For now, try the demo here! Enjoy the terrible artwork!

Controls are as follows:

Click and drag from a character to aim your "wave attack".

A single click will select him. If you release on the character, you can click elsewhere to move, or an enemy to attack. (Don't forget to click the enemy when you arrive for a "timed" attack! On the beat gives you a boost!)

Once selected, Z and X cycle through the colors of the "wave attack". Different waves will have a different effect on the enemies.

A second click on a selected character will bring up a menu.

You can choose a formation: some formations require a certain arrangement to already exist. Try moving characters around first. Be aware that the enemies react to formation changes.

Taunting an enemy or protecting an ally can help keep the enemies away from wounded allies.

And escape... well, you don't need that.