Saturday 2 March 2013

Tsukino-con Followup, Javascript, and a Useful Code Segment

Howdy interwebs!

Sorry this is a bit late, but 1 week is actually better than I normally do.

So the Strategem front end now has most of the functions I wanted to get in for the extremely basic prototype.  It distinguishes which units are yours and which are not, you can save orders in terms of angles.  The on screen pathfinding still has a bug when it switches cases, but as of now it isn't worth hunting down.  On the bright side, aside from the bug, the pathfinding system works just like I wanted.  

The architecture is in a somewhat unmanageable state as all the work has been done in a single javascript file, which is now up to 600 lines.  Yikes. There is a discernible model-view-controller split so the next version should be able to put them in different files and either assemble them with the builder, or just reference them on the server.  Web architecture is still rather alien to me, although javascript itself is less so now. Prior to this work all I had done with the language was a few scripts running in a django system, (that's a python server system, not a slave system rising up against the oppressive admin that stole its wife.)  and some short scripts for a security course.  

Javascript architecture lives in the strange domain of the web script, where there isn't a clear main and scope is an eternal mystery.  Depending on how you look at it, scripts are either run in a space with several global variables, or in a class with several member variables that can be accessed throughout.  Either way, I found that the singleton pattern to be both incredibly useful and easy to set up.  

var Singleton = new function Singleton(){
   //Add variables here
}

I realize it's debatable wether this is genuinely a singleton, or just a global object in which I can put several other variables and can be reliably retrieved with a simple "Game.x", but in the end, with everything being the same script, it had the same effect.  The example I was using online also had the usual getInstance() method, but the Game. syntax was shorter and worked well enough. 

This doesn't detract from my larger point, that in javascript, since document is floating around anyway, having game next to it doesn't seem out of place, making javascript an unusual place where global variables are not a mortal sin.

You know, it dawns on me that I very rarely post code on this site.  I should fix that, and I know just how.  Behold:

function getAngle(xo, yo, xd, yd)
{
    var x = xd - xo;
    var y = yd - yo;
    var angle = 0;

    if(x == 0)
    {
     if(y > 0){ angle = 270;}
     else if(y < 0){ angle = 90; }
    }
    else if (y == 0)
    {
     if(x > 0){ angle = 0;}
     else if(x < 0){angle = 180; }
    }
    else
    {
     //var tmp_angle;
     var tmp_angle = Math.atan(Math.abs(y)/Math.abs(x)) * 180 /      Math.PI;

     var rem = tmp_angle % 45;
     if(Math.floor(rem/22) < 1)
     {
            tmp_angle -= rem;
     }else{
            tmp_angle += (45-rem);
     }

     //var switch_var;
     switch_var = 0;
     if(x>0){ switch_var+=1;}
     if(y>0){ switch_var+=2;}
     switch(switch_var)
     {
      case 0:          //quadrant 2
           angle = 90 + tmp_angle;
           break;
      case 1:          //quadrant 1
           angle = 0 +  tmp_angle;
           break;
      case 2:          //quadrant 3
           angle = 180 +  tmp_angle;
           break;
      case 3:          //quadrant 4
           angle = 270 + tmp_angle;
           break;
     }
    }
    return angle;
    
}

This is a simple function I wrote for Jeubble to make the bubble shooter rotate to face the mouse, butwhen testers found the controls too touchy, I added in this section:

var rem = tmp_angle % 45;
if(Math.floor(rem/22) < 1)
{
     tmp_angle -= rem;
}else{
     tmp_angle += (45-rem);
}

Basically it separates the angle into 45 degree increments.  The original Jeubble version used 15 degree increments, but Stratagem uses 45 degrees to differentiate between the 8 possible1 space moves from a given square.  I also recently used this function in the NEPTUNE game to deal with the different directions available from hex and takes th increment as a paramater instead.  It's probably the best version of this, but it's on my other computer and I don't want to dig through github right now.  Anyway, when you find you've used the same code 3 times, its time to save it somewhere.

Let's break it down.

This first part assigns the x and y components and initializes the output angle to zero.  Nothing fancy yet so I didn't copy it.  But the next part is where things get interesting.


    if(x == 0)
    {
     if(y > 0){ angle = 270;}
     else if(y < 0){ angle = 90; }
    }
    else if (y == 0)
    {
     if(x > 0){ angle = 0;}
     else if(x < 0){angle = 180; }
    }
    else
    {
     //var tmp_angle;
     var tmp_angle = Math.atan(Math.abs(y)/Math.abs(x)) * 180 /      Math.PI;


Typical use of a tan function has issues about where it works.  Firstly, at 0,90,180, or 270,  there is often some sort of asymptotic bug.  Game Maker has probably the funniest version of this where rather than crashing, the angle just grows, so the object will slowly rotate forever.  Signs also don't match in most cases, because screen coordinates are reflected in the x axis (positive y is down).  Lastly, atan is only defined over a 180 degree section between 2 asymptotes.  I've found this varies from implementation to implementation, but the one thing you can always count on is the first quadrant, 0 to 90 degrees, being accurate.

To deal with these issues, my function starts by catching all 4 asymptotes.  First it checks if one of the coordinates is 0, then checks if the other is above or below zero.  The double zero case kind of slips harmlessly through the cracks.  It takes the first if because x==0, but it doesn't take either of the followups, and instead jumps to the end of the function without changing the initial value of angle.  So in the end, the method returns 0, which while not technically correct, isn't detrimental to an place I've used this program.

The else section takes the arctan of the ratio of the 2 absolute coordinates, giving an angle 1-89 degrees above the positive x axis.  The next step is the truncation bit we talked about earlier:


var rem = tmp_angle % 45;
if(Math.floor(rem/22) < 1)
{
     tmp_angle -= rem;
}else{
     tmp_angle += (45-rem);
}


It takes the remainder of the angle retrieved in the else and rounds it to the nearest multiple of 45.  So now we have an angle of 0, 45 or 90.  Obviously there are 5 other possibile angles which aren't represented, so we need to take our answer out of the 1st quadrant and translate it to the correct one.


     //var switch_var;
     switch_var = 0;
     if(x>0){ switch_var+=1;}
     if(y>0){ switch_var+=2;}
     switch(switch_var)
     {
      case 0:          //quadrant 2
           angle = 90 + tmp_angle;
           break;
      case 1:          //quadrant 1
           angle = 0 +  tmp_angle;
           break;
      case 2:          //quadrant 3
           angle = 180 +  tmp_angle;
           break;
      case 3:          //quadrant 4
           angle = 270 + tmp_angle;
           break;
     }
    }
    return angle;

Ah switch case manipulation, how I love it.  Switch cases are in the same camp as for loops in the school of constructs created to quickly perform a simple common operation that can be subtly reworked if you know what you're doing, and made to perform a more complex, but fundamentally similar operation.  This isn't too much of a hack, but it does demonstrate creative use of the switch variable. 

First let me stress that this is done for screen coordinates rather than normal coordinates, so positive y is down.  So we once again start by checking the x and y coordinates against 0.  We can think of our 4 quadrants as hypercube in 2 dimensions.  I'm using x as the bit 0 value and y for the bit 1.  In an integer, these correspond to adding 1 and adding 2 respectively.  This creates a number between 0 and 3 that will correspond to the quadrant.  In each quadrant, we assign angle to be the tmp_angle is between angle and the x-axis.

And then we're done.  So the function isn't amazing or super insightful or even as general as it could be,  but it is useful and it has prompted me to start a pseudocode list for UVic Gamdev of useful functions like this one that can be dropped into any program and modified to fit the language.

Well I hope you find this useful random reader, and don't be afraid to tell me how I could make it better, or how much it sucks, or how it was the missing piece that made your most difficult problem suddenly go away.  I'm not realistically expecting that last one, but hey, a guy can dream.