tag:blogger.com,1999:blog-66588440846247001422024-03-13T20:01:27.674-07:00code and composeAnonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.comBlogger14125tag:blogger.com,1999:blog-6658844084624700142.post-4139252524723236432017-02-25T00:58:00.001-08:002017-02-25T00:59:39.254-08:00Ready. Aim. FIRE!If you've ever programmed a game, then you know that physics is critical to realistic play.<br />
<br />
No, I am not going to explain how to build a physics engine. I just started with the basics.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://games.codeandcompose.com/trajectory.html" target="_blank"><img border="0" height="213" src="https://1.bp.blogspot.com/-8_NRLBKrFIA/WLFGHVkh7ZI/AAAAAAAAAtE/28W734LQQQc_K5noPxgp8dFSOOfW09VagCLcB/s320/Screen%2BShot%2B2017-02-15%2Bat%2B13.19.38.png" width="320" /></a></div>
<h4>
How to Launch Things</h4>
<div>
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.</div>
<div>
<br /></div>
<div>
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.</div>
<div>
<br /></div>
<div>
That means we have to do the algebra ourselves. *Sigh*</div>
<div>
<br /></div>
<div>
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 <a href="https://www.khanacademy.org/science/physics/one-dimensional-motion/kinematic-formulas/a/what-are-the-kinematic-formulas" target="_blank">big four, I recommend Khan Academy</a>.</div>
<div>
<br /></div>
<div>
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:</div>
<div>
<pre class="prettyprint">//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);
}</pre>
<br /></div>
<div>
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!</div>
Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-47341897759773098262016-10-03T03:58:00.001-07:002016-10-03T03:58:48.435-07:00What'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...<br />
<div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://games.codeandcompose.com/3dvmv/" target="_blank"><img border="0" height="267" src="https://1.bp.blogspot.com/-wgTrs6y9s1Y/V_I5cT_gsHI/AAAAAAAAArk/K82sXZifVKwKGnYsRZEkNVjVsTYOpDFYgCLcB/s400/Screen%2BShot%2B2016-10-03%2Bat%2B12.56.24.png" width="400" /></a></div>
<br />
So I devised a <a href="http://games.codeandcompose.com/3dvmv/">little aid</a>. 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.</div>
<div>
<br /></div>
<div>
With a little help from WebGL, it wasn't too hard to draw some axes, planes and arrows (although adding text was <a href="http://webglfundamentals.org/webgl/lessons/webgl-text-texture.html" target="_blank">a bit trickier</a>).<br />
<br />
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!<br />
<br />
Features so far:<br />
<ul>
<li>add up to 6 colored vectors</li>
<li>rotate the graph 90 degrees in each direction (click the graph once to unlock, again to lock)</li>
<li>scale the graph</li>
<li>calculates resultant force and moment vector displayed in white</li>
</ul>
<div>
Planned features:</div>
</div>
<div>
<ul>
<li>adding larger number of vector inputs</li>
<li>animated vectors</li>
<li>unit adjustments, degrees and radians</li>
<li>vector solver...? (to determine unknown value of a vector with known direction)</li>
</ul>
<div>
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.</div>
</div>
<div>
<br /></div>
<div>
Enjoy the app. I use it to check my homework...</div>
<div>
<br /></div>
<div>
Feel free to fork <a href="https://github.com/Shytor/3dvmv" target="_blank">my repo on GitHub</a> and let me know if you have any feature suggestions or bugs!</div>
Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-34497089464929230762016-05-05T11:03:00.003-07:002016-05-05T11:03:51.767-07:00A Random PostIt's been awhile so I figured: it is time for a random post. Seriously, this is a post about random numbers.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://2.bp.blogspot.com/-JtRHRPpYaEQ/VyuK9HaFk5I/AAAAAAAAAkw/UFtdcjYoVhw-FAplrvxDkaY1bj9UcG0oQCLcB/s1600/rolling-dice-gif-3.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://2.bp.blogspot.com/-JtRHRPpYaEQ/VyuK9HaFk5I/AAAAAAAAAkw/UFtdcjYoVhw-FAplrvxDkaY1bj9UcG0oQCLcB/s1600/rolling-dice-gif-3.gif" /></a></div>
<br />
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.<br />
<br />
But what if you want NON-uniform randoms? For statistics freaks: what if you need normal distribution? or Gamma? or Beta? Exponential? Chi-squared?<br />
<br />
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.<br />
<br />
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.<br />
<h4>
<span style="font-family: "courier new" , "courier" , monospace;">RANDOM.norm(n) </span></h4>
<span style="font-family: inherit;">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.</span><br />
<h4>
<span style="font-family: "courier new" , "courier" , monospace;">RANDOM.inorm(n) </span></h4>
<span style="font-family: inherit;">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.</span><br />
<h4>
<span style="font-family: "courier new" , "courier" , monospace;">RANDOM.exp(n) </span></h4>
<span style="font-family: inherit;">A nice easy exponential variable from 0 to 1. Increasing "n" makes the graph skewed towards 0. Default value for "n" is 2.</span><br />
<h4>
<span style="font-family: "courier new" , "courier" , monospace;">RANDOM.invexp(n) </span></h4>
<span style="font-family: inherit;">This is just RANDOM.exp(n) times -1.</span><br />
<h4>
<span style="font-family: "courier new" , "courier" , monospace;">RANDOM.gamma(alpha,beta) </span></h4>
<span style="font-family: inherit;">This is a conglomeration of a few generators. For alpha > 1 and < 0, it uses an algorithm <a href="http://home.iitk.ac.in/~kundu/paper120.pdf" target="_blank">developed by Kundu and Gupta</a>. No beta is used for this algorithm.</span><br />
<br />
<span style="font-family: inherit;">For alpha >= 1 it uses the <a href="http://arxiv.org/pdf/1304.3800.pdf" target="_blank">algorithms proposed by Martino and Luengo</a></span>. It generates very quick results due to a low rejection rate. Default values are 1 and 1.<br />
<br />
I could not have completed this without those great algorithms! If you use this code, please leave the credits they deserve.<br />
<br />
<b>TIP:</b> 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.<br />
<h4>
<span style="font-family: "courier new" , "courier" , monospace;">RANDOM.igamma(alpha,beta) </span></h4>
<span style="font-family: inherit;">Simply RANDOM.gamma() times -1.</span><br />
<h4>
<span style="font-family: "courier new" , "courier" , monospace;">RANDOM.chi2(k) </span></h4>
<span style="font-family: inherit;">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".</span><br />
<h4>
<span style="font-family: "courier new" , "courier" , monospace;">RANDOM.coinFlip(weight) </span></h4>
<span style="font-family: inherit;">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).</span><br />
<h3>
<span style="font-family: inherit;">The code:</span></h3>
<span style="font-family: inherit;">As usual, the code is free to use under the GNU. Please give credit though, and I'd <b>love</b> to hear how this has been useful for anyone! Also, feel free to play around with the <a href="http://jsfiddle.net/pb086em6/5/" target="_blank">fiddle</a>! (The code is adjusted slightly for display).</span><br />
<span style="font-family: inherit;"><br /></span>
<span style="font-family: inherit;">I'll post a follow-up if <span style="font-family: inherit;">I <span style="font-family: inherit;">ever get around to adding Beta randoms too!</span></span></span><br />
<br />
<pre class="prettyprint">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
</pre>
<br />
Dice gif courtesy of <a href="http://bestanimations.com/Games/Dice/Dice.html">http://bestanimations.com/Games/Dice/Dice.html</a>Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-90448316572982376172015-11-21T21:24:00.000-08:002015-12-02T10:58:59.618-08:00Take to the skies! Take the skies... recursively.If the game has been too much fun for you...<br />
<br />
I kid. I'll finish it someday. Probably.<br />
<br />
In the meantime, I've been taking an <a href="https://www.edx.org/course/introduction-aeronautical-engineering-delftx-ae1110x-0" target="_blank">Introduction to Aeronautical Engineering course on edx.org</a>. 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...<br />
<br />
It turns out: I do like math.<br />
<br />
So much so, that I programmed this <a href="http://games.codeandcompose.com/ISAcalculator.html" target="_blank">International Standard Atmosphere Calculator</a>. 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 <a href="http://codeandcompose.blogspot.com/2014/11/point-to-point-b-part-4.html">collide</a> with any other airplanes. Or if you need to do your Aerospace homework.<br />
<br />
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.<br />
<br />
Programming this little job also gave me a good excuse to use a recursive function.<br />
<h3>
Re...CURSES!</h3>
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..."<br />
<br />
And you can solve most problems without ever using recursion. So why bother right?<br />
<br />
Because it is so much TIDIER!<br />
<br />
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!<br />
<br />
Without getting too into depth on ISA calculations, let's just say this:<br />
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.<br />
If we want to calculate at higher altitudes, we need even MORE equations dependent on all those below.<br />
<br />
So how does recursion help?<br />
<br />
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.<br />
<br />
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?<br />
<br />
Here's the commented code:<br />
<br />
<pre class="prettyprint">//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};
}
</pre>
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'??<br />
<br />
Well, no. Let's look at an example:<br />
<br />
Let's say we call getAtmosphere(25000).<br />
<br />
The function needs input from getAtmosphere(20000), so it calls that and waits...<br />
<br />
THAT call needs input from getAtmosphere(11000), so it calls that and waits...<br />
<br />
THAT call needs input from getAtmosphere(0), so it calls it and... WAIT! That's the out!<br />
<br />
Once it calls that, it gets data returned! This is the KEY to recursion. It hits an end, now it can finish!<br />
<br />
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.<br />
<br />
And no matter what altitude we call it on, it knows just how many times to call itself to get the right answer! Brilliant!<br />
<br />
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...<br />
<br />
Don't forget the OUT!<br />
<br />
EDIT:<br />
<br />
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!<br />
<br />
Also, feel free to <a href="https://github.com/Shytor/ISAcalculator" target="_blank">fork on GitHub</a>!<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-WvVU2pxn9ig/VlFTGKw6S5I/AAAAAAAAAgo/dOLaUeTvs34/s1600/clouds_in_earths_atmosphere_m.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-WvVU2pxn9ig/VlFTGKw6S5I/AAAAAAAAAgo/dOLaUeTvs34/s1600/clouds_in_earths_atmosphere_m.jpg" /></a></div>
Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-2405755720883257742015-10-15T20:51:00.001-07:002015-10-15T20:54:10.765-07:00Comedy is all about... timing.In the case of my game concept, so is user input.<br />
<br />
"So what is this crazy game concept you keep talking about?"<br />
<br />
Here it is:<br />
<br />
Do you remember <a href="https://www.youtube.com/watch?v=uTVL-nIfw1k" target="_blank">Super Mario RPG and timed hits</a>? It was a fun addition to the usual turn-based RPG style game that added an extra layer of player interaction.<br />
<br />
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...<br />
<br />
I'm a music composer and sound designer; why not combine those skills with the concept of timed hits?<br />
<br />
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".<br />
<br />
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!<br />
<br />
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!<br />
<br />
<h3>
Music, Audio and Timing</h3>
<br />
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 <a href="http://www.html5rocks.com/en/tutorials/audio/scheduling/" target="_blank">information here</a>.<br />
<br />
For those of you into instant gratification: here is the code used to schedule the music.<br />
<br />
<pre class="prettyprint">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
}
</pre>
<br />
gMusic is an instance of an AudioFontClass I created. More on that later, but this is the playSound function it uses when scheduling audio.<br />
<br />
<pre class="prettyprint">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);
} </pre>
<br />
How does it work?<br />
<br />
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).<br />
<br />
The nice thing about the Web Audio API is that you can schedule audio way ahead of time, so nothing plays late.<br />
<br />
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.<br />
<br />
For now, <a href="http://games.codeandcompose.com/gamedemo1/" target="_blank">try the demo here!</a> Enjoy the terrible artwork!<br />
<br />
<b>Controls are as follows:</b><br />
<br />
Click and drag from a character to aim your "wave attack".<br />
<br />
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!)<br />
<br />
Once selected, Z and X cycle through the colors of the "wave attack". Different waves will have a different effect on the enemies.<br />
<br />
A second click on a selected character will bring up a menu.<br />
<br />
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.<br />
<br />
Taunting an enemy or protecting an ally can help keep the enemies away from wounded allies.<br />
<br />
And escape... well, you don't need that.Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-31117261283285220822015-06-28T15:50:00.005-07:002015-06-28T15:50:48.796-07:00Enough Fun and GamesOk, So I am taking a short break from the game to do some real work. Sort of.<br />
<br />
In learning Javascript, I decided it would be worth my while to learn some jQuery as well. Turns out it is super-easy and useful for making simple applications that have "real work" uses.<br />
<br />
I developed two small tools that could be used in a number of ways. One is just a <a href="http://games.codeandcompose.com/WorkApps/Contents/Contents.html" target="_blank">big-old table of contents style drop down, complete with link descriptions</a>.<br />
<br />
The other is what I call a "workflow" that could be used for giving instructions or even trouble-shooting common issues. <a href="http://games.codeandcompose.com/WorkApps/Workflow/Workflow.html" target="_blank">Check it out here</a>.<br />
<br />
Both are easily adaptable and open source. So feel free to <a href="http://games.codeandcompose.com/WorkApps/sourcecode.zip">pilfer the code</a>. Feel free to ask me questions if you need help using it.<br />
<br />
<h3>
Table of Contents</h3>
For this little app, I just needed a list that I could easily add links to and would fold itself up, so there wouldn't be too many on a page to look at at once. You can just look at the relevant section and ignore the rest.<br />
<br />
First, each "category" is listed inside list tag with class "slide". Then underneath, we create a list of links with class "articles". You can make unlimited categories.
<br />
<pre class="prettyprint">
<ul class='slide'>
<li><h3>Category 1</H3></li>
<ul class="articles">
+Each line is split at the plus
+type "#plus" if you want to actually see a plus sign as text
+It automatically parses addresses as links that are listed with http:
+the script parses out the part after "Description:" for the mouseover
+download the code here! http://games.codeandcompose.com/WorkApps/sourcecode.zip
Description:The best code ever written! Or not.
</ul>
... </pre>
<br />
The app splits each line in the "articles" list by the +. Then it creates a link with an address (if there is one starting with http:// or https://). If there is a description (split at "Description:"), that will show up when you hover.<br />
<br />
There is a tiny cosmetic glitch with the hover descriptions. I haven't yet come up with a great fix, but it's not too noticeable.<br />
<br />
The string parsing was pretty simple. It uses some <a href="http://regexr.com/" target="_blank">regular expressions</a> and Javascript string.replace() function. After breaking the strings, it puts them into list elements and links (when used).<br />
<br />
<pre class="prettyprint">
$.fn.extend({
stringSplitToList: function() //this function parses the html body into a list for the jquery
{
var htmlList = '', aclass = 'none';
$.each($(this).html().split('+'), function(i, v) {
v = v.replace(/#plus/g,"+");//put plusses back in
var tempSplit = v.split("Description:");
v = tempSplit[0];
//makes sure the description not undefined.
var description = (tempSplit[1]) ? tempSplit[1] : "No description.";
if (v.match(/(https:\/\/|http:\/\/)\S+/igm)) {
var alink = v.match(/(https:\/\/|http:\/\/)\S+/igm)[0];
var title = v.replace(alink,"");
if (title.match(/\*/gm)){
title = title.replace("*","");
aclass = 'internal';
} else if (title.match(/\^/gm)){
title = title.replace("^","");
aclass = 'archive';
} else {
aclass = 'none';
}
htmlList += '<li><a href="' + alink + '" class="' + aclass + '">' + title + '</a><div class="des">' + description + '</div></li>';
//handle categories header
} else if (v.match(/(href=)\S+/igm)) {
var alink = v.match(/(href=)\S+/igm)[0];
var title = v.replace(alink,"");
alink = alink.replace("href=","");
if (title.match(/\*/gm)){
title = title.replace("*","");
aclass = 'internal';
} else {
aclass = 'none';
}
htmlList += '<li><a href="' + alink + '" class="' + aclass + '">' + title + '</a><div class="des">' + description + '</div></li>';
//handle categories header
} else {
htmlList += '<li><div class="category">' + v + '</div></li>';
}
});
$(this).html(htmlList);
}
});</pre>
<br />
Each link can also be marked as "internal" with a * or "archived" with a ^. You can even use internal links marked by href=<br />
<br />
Just call this line once the page loads and it parses the text for you.<br />
<br />
<pre class="prettyprint">$('.articles').each(function(index){$(this).stringSplitToList();});
</pre>
<br />
<h3>
Workflow App</h3>
The workflow app can take very complex paths. By following the formula in each js file, you can add unlimited questions and answers. As the user chooses their answers, links, pictures and tips are given to help them out. The logic can be created to direct to a certain path based on previous answers form one or multiple questions.<br />
<br />
I used it to make a wi-fi router trouble-shooting guide. The example given on this website is just for fun though.<br />
<br />
Here is a graffle showing how the questions are programmed.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-CF_yR0zDzjw/VZBsVRCebeI/AAAAAAAAAU8/iyBXsB5bkz0/s1600/colorgraffle.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/-CF_yR0zDzjw/VZBsVRCebeI/AAAAAAAAAU8/iyBXsB5bkz0/s1600/colorgraffle.jpg" /></a></div>
<br />
<br />
Each question is loaded in the following format:<br />
<br />
<pre class="prettyprint">addQuestion(1, //the question number
//the question
"This is workflow 1. Use this workflow?",
//answer options (as many as you want)
["Maybe","No","Yes"],
//answer direction, either a question number or LOGIC or RESET
[1,"LOGIC",1],1, // the extra number is default
//tips text
"This area will show helpful tips. Or not...",
//image urls (as many as you want) or null
["image1.png","image2.png"],
// url and text for helpful links
[{"url":"http://tagsounds.com","title":"A Link to TAGSOUNDS.COM"}],
// callback function for when the direction of an answer is LOGIC
function(){
if (AA[0] === "Red"){
nextQuestion(201);
return;
} else loadWorkflow("workflow2.js");
}
);
</pre>
<br />
If you like it, or would like to know more about how to adapt it for your needs, please contact me using the <a href="http://tagsounds.com/contact/" target="_blank">contact form at tagsounds.com</a>.<br />
<br />
<br />Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-78803174270335365742015-04-28T11:10:00.000-07:002015-04-28T11:10:12.571-07:00INPUT (Click me! Click me!)<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-cz-up02DC2k/VT7p4FdY8oI/AAAAAAAAAR4/lkbqBAw_LuE/s1600/clickme.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/-cz-up02DC2k/VT7p4FdY8oI/AAAAAAAAAR4/lkbqBAw_LuE/s1600/clickme.gif" /></a></div>
One of the most important elements of game-play is user interaction.<br />
<br />
So many RPG games are great tactical games, but lose the sense of real-time enjoyment while bouncing through menus. An early prototype of my game was menu centric. But as I make some mini demos, I'm trying to limit the amount of time spent in the menus in favor of faster input choices.<br />
<br />
For this demo, there is no need for a menu. One click on a character selects him and then click the enemy and he attacks. OR, click and drag to aim the projectile, release to fire. Click twice on the character (not double-click for reasons I will explain) and then click an empty space to move them.<br />
<br />
I'd like to keep it setup in a way that may be ported to touch screen devices. I imagine that will introduce new challenges. Perhaps I'll learn Objective-C first and make an app... ?<br />
<br />
Anyway. Step one is capturing the click. Since everything is done inside the canvas, we use the following code:<br />
<br />
<pre class="prettyprint">window.onload = function (){
var canvas = document.getElementById("gamewindow");
document.getElementById('gamewindow').onmousemove = moveHandler;
document.getElementById('gamewindow').onmousedown = clickHandler; //onclick is too slow for timing
document.getElementById('gamewindow').onmouseup = clickReleaseHandler;
document.onkeypress = onkeypressHandler;
}
function moveHandler(event){}
function clickHandler(event){}
function clickReleaseHandler(event){}
function onkeypressHandler(event){}
</pre>
Notice that I used mousedown instead of onclick. This is important for calling functions quickly so that when the player tries to synchronize with the music, it is more accurate. Plus we can catch the mouseup event separately. The downside, is that differentiating click from double click is harder, so we just don't do it!<br />
<br />
Each event listener is directed to a handler function. Now we have to do, you know, logic.<br />
<br />
Unlike normal interactive elements, there are a few challenges to overcame when dealing with a live game environment. Starting with the fact that everything moves. Thus, step one is finding out what was clicked.<br />
<br />
Or as I quickly learned, the FIRST step is to offset your click location to account for the placement of the canvas and scrolling...<br />
<br />
<pre class="prerttyprint">function clickHandler(event){
// cache the time/location of the click
clickTime = audioctx.currentTime;
event = event || window.event;
var scrollX = window.pageXOffset || document.body.scrollLeft;
var scrollY = window.pageYOffset || document.body.scrollTop;
clickX = event.clientX + scrollX - xoffset;
clickY = event.clientY + scrollY - yoffset;
var gclick = guyClicked(clickX,clickY,"Allies",true);
}
function guyClicked(X,Y,dic,mustBeReady){
var dict = activeGuyDict[dic];
for (var guy in dict){
if (!(X < dict[guy].pos.x + dict[guy].bounding.x || X > dict[guy].pos.x + dict[guy].bounding.x + dict[guy].bounding.w || Y < dict[guy].pos.y + dict[guy].bounding.y || Y > dict[guy].pos.y + dict[guy].bounding.y + dict[guy].bounding.h)){
if ((mustBeReady && dict[guy].ready) || !mustBeReady) return guy;
//character must be ready if "mustBeReady" is true
else return null;
}
}
//no guy was found
return null;
}
</pre>
If you don't feel like ripping through that IF statement in guyClicked, here's the rough explanation. It's a beautiful use of NOT OR logic.
We need to see if the click occurred INSIDE the bounding box for the character. If the box is 50x100 at point (200,130), we see if the x was between 200 and 250 and if y was between 130 and 230, right?<br />
<br />
We could setup the logic to see if x > 200 AND x < 250 AND y > 130 AND y < 230...<br />
<br />
OR<br />
<br />
... we could check if the click falls OUTSIDE the parameters, and use NOT OR. Only one condition must be true for the whole thing to fail. Not sure if it matters here, but this is useful if you are comparing BOXES to look for overlap.<br />
<br />
For my game, we also check to see if the character is "ready" before selecting. Now onto the real tricky logic.<br />
<br />
First click down gets the aiming out. Then release to choose an enemy. Then click an enemy or click the good guy again. Or click another guy to change the selection. Then click a spot to move. Or if an enemy was selected, click again to time the attack. Or, um... what else can you do with just a click?
It gets complicated fast.<br />
<br />
I found that (although counter-intuitive) it works best to write the logic backwards. Start with the most specific scenario first and work backwards through input order. That way, the handler can check for the specific scenarios and bail, or move on.<br />
<br />
In plain text it looks like this:<br />
<div class="mylist">
<ul>
<li>Check to see if a selected character is waiting to move.</li>
<ul>
<li>If a character was clicked, change focus</li>
<li>If an empty space was clicked, move </li>
</ul>
<li>Check to see if an attacking character is "PRIMING".</li>
<ul>
<li>Was the appropriate enemy clicked?</li>
</ul>
<li>Check to see if an enemy must be selected to attack.</li>
<ul>
<li>Did you click an enemy?</li>
<li>If a character was clicked change focus</li>
</ul>
<li>If none of those criteria are met, select the clicked character.
</li>
</ul>
</div>
<pre class="prettyprint">
for (var pg in activeGuyDict.Allies){
// check to see if a character needs to move
if (activeGuyDict.Allies[pg].moveForm){
activeGuyDict.Allies[pg].moveForm = false;
activeGuyDict.Allies[pg].selected = false;
colorWheel.reset();
//need to check if the space is occupied by the other character
if (guyClicked(clickX,clickY,"Allies")) continue;
activeGuyDict.Allies[pg].formation = {"name":"Formation","pos":{cx:clickX,cy:clickY},"halo":{cr:0,r:5}};
activeGuyDict.Allies[pg].action = "FORMATION";
activeGuyDict.Allies[pg].target = activeGuyDict.Allies[pg].formation;
return;
}
if (activeGuyDict.Allies[pg].animation.name != 'PRIME') continue;
if (!activeGuyDict.Allies[pg].attackTarget) continue;
else var primeClick = guyClicked(clickX,clickY,"Enemies");
if (!primeClick) continue;
if (primeClick === activeGuyDict.Allies[pg].attackTarget){
var cT = clickTime; //un-cache the click time
activeGuyDict.Allies[pg].setAnim('ATTACK',cT);
//this code determines the power, based on the timing
var sync = 0;
var hbl = secondsPerBeat / 2; //half a beat length
if (cT - lastBeatTime < nextBeatTime - cT) sync = Math.round((cT - lastBeatTime)/hbl * 50);
else sync = Math.round((nextBeatTime - cT)/hbl * -50);
//calculate the power/damage here
var power = activeGuyDict.Allies[pg].power(sync);
activeGuyDict.Enemies[activeGuyDict.Allies[pg].attackTarget].takeHit(power,pg);//determine the damage taken from the hit
clickTime = null;
return; //don't do anything else with the click
}
}//end FOR loop
if (selectThis === "ENEMY"){//picking an enemy to attack
var gc = guyClicked(clickX,clickY,"Allies",true);
var g = findSelectedChar();
if (gc) { // a good guy was clicked... handle it
if (gc != g){ //a different ready character was selected..switches focus
selectThis = "";
activeGuyDict.Allies[g].selected = false;
activeGuyDict.Allies[g].action = '';
var cT = clickTime;
activeGuyDict.Allies[gc].selected = true;
colorWheel.newSC(gc);
selectThis = "AIM";
activeGuyDict.Allies[gc].setAnim('AIM',cT);
activeGuyDict.Allies[gc].action = 'ATTACK';
mouseDown = true;//keep track of the mouse state
return;
} else if (gc === g) { //same character was clicked... moveForm
activeGuyDict.Allies[g].moveForm = true;
//cancel ATTACK action
selectThis = "";
activeGuyDict.Allies[g].action = "";
return;
}
}
var e = guyClicked(clickX,clickY,"Enemies");
if (!e) return; //make sure an enemy was clicked
if (activeGuyDict.Allies[g].action === 'ATTACK'){
activeGuyDict.Allies[g].ready = false;
activeGuyDict.Allies[g].selected = false;
colorWheel.reset();
activeGuyDict.Allies[g].target = {"name":e,"pos":activeGuyDict.Enemies[e].pos,"halo":activeGuyDict.Enemies[e].halo};
selectThis = "";
colorWheel.reset();//close the color wheel
} else if (activeGuyDict.Allies[g].action === 'TAUNT'){
activeGuyDict.Enemies[e].taunted(activeGuyDict.Allies[g].pos,activeGuyDict.Allies[g].halo);
activeGuyDict.Allies[g].animation.name = 'TAUNT';
activeGuyDict.Allies[g].selected = false;
colorWheel.reset();
selectThis = "";
}
return;
}
//see if a ready guy was clicked
var gclick = guyClicked(clickX,clickY,"Allies",true);
if (gclick){
var cT = clickTime;
activeGuyDict.Allies[gclick].selected = true;
colorWheel.newSC(gclick);
selectThis = "AIM";
activeGuyDict.Allies[gclick].setAnim('AIM',cT);
activeGuyDict.Allies[gclick].action = 'ATTACK';
mouseDown = true;//keep track of the mouse state
}</pre>
This is the mouseup handler. It only pays attention if the player is holding the mouse down to "AIM". Thus avoiding unnecessary event functions.
<br />
<pre class="prettyprint">function clickReleaseHandler(event){
// cache the time/location of the click
if (selectThis != "AIM") return;
clickTime = audioctx.currentTime;
event = event || window.event;
var scrollX = window.pageXOffset || document.body.scrollLeft;
var scrollY = window.pageYOffset || document.body.scrollTop;
clickX = event.clientX + scrollX - xoffset;
clickY = event.clientY + scrollY - yoffset;
var g = findSelectedChar();
var gc = guyClicked(clickX,clickY,"Allies");
if (gc === g) {
selectThis = "ENEMY";
activeGuyDict.Allies[g].clearAnim();
mouseDown = false;
return; //bail
} else {
//they released on the aimed area
var cT = clickTime;
var sync = 0;
var hbl = secondsPerBeat / 2; //half a beat length
//click is closer to last beat
if (cT - lastBeatTime < nextBeatTime - cT) sync = Math.round((cT - lastBeatTime)/hbl * 50);
else sync = Math.round((nextBeatTime - cT)/hbl * -50);//click is closer to next beat
//click was before last beat (in case the beat changes between click and frame)
//if (cT < lastBeatTime) sync = 50 - Math.round((lastBeatTime - cT)/hbl * 50);
var power = activeGuyDict.Allies[g].power(sync);
clickTime = null;
//create the projectile
projectiles.add("wave",activeGuyDict.Allies[g].pos,clickX,clickY,power,8,500,activeGuyDict.Allies[g].color);
selectThis = "";
activeGuyDict.Allies[g].ready = false;
activeGuyDict.Allies[g].selected = false;
activeGuyDict.Allies[g].setAnim('ATTACK',cT);
activeGuyDict.Allies[g].target = null;
mouseDown = false;
return;
}
}</pre>
As always, feel free to steal my code. Just let me know how you are using it! I'm curious to know how and if anyone finds this code useful!
<br />
<br />
And as promised, here's a <a href="http://games.codeandcompose.com/projectilegame.html" target="_blank">link to the demo</a>!<br />
<br />
Instructions:<br />
<div class="mylist">
<ul>
<li>CLICK and DRAG from a character to shoot a projectile. They will not do damage, but different colors will have different effects,</li>
<li>PRESS Z or X to change the color of a character after he is selected.</li>
<li>Just CLICK a character once to select him, and then click an enemy to attack. The character will chase the enemy. You MUST CLICK the enemy once he has arrived in order to initiate the attack. Try to time it with the pulsing to do extra damage.</li>
<li>CLICK the selected character again, and then click an empty space to move him.</li>
<li>No damage indication is given, just keep attacking, and the enemies will eventually disappear.</li>
</ul>
</div>
<br />Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-86886741887844073032015-04-17T19:50:00.000-07:002015-04-17T19:50:06.088-07:00Color and DissonanceSo what exactly is this game concept I keep mumbling about?<br />
<br />
Well, here's part of the picture: You are immersed in a world with a mysterious energy force. Not much is known about it except two things. First, it's great for making powerful machines and weapons. And when it is harvested, horrifying and dangerous spirits are released.<br />
<br />
Naturally, along your quest, you must face these spirits to survive. But you can't just beat them to oblivion like normal RPG monsters or zombies. They have to be "in phase".<br />
<br />
As a composer, I decided that I should make a game that fit my specific knowledge and skill set. Thus, music and sound had to be solidly integrated into the game play. I'll get more into the music side later, but there is an underlying function of music in the battles as well.<br />
<br />
Imagine each spirit is assigned a "note" or frequency. In addition to fighting them, you can also blast them with another "note" using the <a href="http://codeandcompose.blogspot.com/2015/04/projectiles-as-javascript-objects.html">wave projectiles we created here</a>. Depending on the "interval" between the notes, a different effect is achieved.<br />
<br />
For those of you not familiar with music theory, this gets a little intense. And since I want the game play to be inclusive, I had to come up with a visual representation. So why not color each note?<br />
<br />
This presented me with more of a challenge than I had expected. HOW do you color all 12 notes?<br />
<br />
Many people have tried to color the scale for educational purposes, but usually focus only on a 7 note scale. With Roy G Biv, that works out nicely. But I had to get a little more creative.<br />
<br />
Since the effect of the interval will be based on its "dissonance" or "consonance", I decided to match a color wheel using the Circle of Fifths. If you know what I'm talking about, see the diagram. If not... um, well... take a music class.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-xV8JKu5pL7U/VTEWVNXcDrI/AAAAAAAAARk/PEx6rSrOSkM/s1600/COFcolorwheel.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-xV8JKu5pL7U/VTEWVNXcDrI/AAAAAAAAARk/PEx6rSrOSkM/s1600/COFcolorwheel.jpg" /></a></div>
The adjacent notes are all an interval of a 5th. Which is consonant. As you move around the circle, away from one note, the dissonance increases. For example, C is a semi-tone from B and a tri-tone from F#... very dissonant. This, similar colors blend, opposites, not so much.<br />
<br />
For game play purposes I grouped them like this:<br />
Unison (same note) will heal the enemy.<br />
5th is consonant, so no effect. <br />
2nd (whole-tone) will have a disruptive effect, like stopping the enemy for a moment.<br />
Minor 3rd will have a positive effect, like speeding up the enemy.<br />
Major 3rd will have a negative effect, like slowing.<br />
Semi-tone and tri-tones are so disruptive they will actually change the note of the enemy.<br />
<br />
The math for this works out really well too. The HSL color mode has 360 hues. That means the intervals can be determined by subtracting the color hues. For example, if C=150 and A=240, the difference, 90, represents a minor 3rd.<br />
<br />
Now, it would be no fun to let each character have access to all 12 notes. So they get a "key palette". Basically, a set of notes that are available during battle.<br />
<br />
I had to add some properties to my good guy object:<br />
<pre class="prettyprint">this.color = 150;//may be unnecessary = this.palette[0];
this.palette = [150,210,270,120,180,240,300];//an array of the colors available
this.changeStep = function(step){
this.palette.rotate(step)
this.color = this.palette[0];
//return this.color;
};</pre>
The changeStep() function is called with an interval so the character can move through his palette (currently by pressing Z or X). What is palette.rotate()? A handy little method added to the Array.prototype that pushes items around the array in order. There are <a href="http://jsperf.com/js-rotate-array/14" target="_blank">other methods of doing this here</a>... but I liked this the best.<br />
<pre class="prettyprint">Array.prototype.rotate = (function() {
return function(inc) {
for (var l = this.length, inc = (Math.abs(inc) >= l && (inc %= l), inc < 0 && (inc += l), inc), i, x; inc; inc = (Math.ceil(l / inc) - 1) * inc - l + (l = inc))
for (i = l; i > inc; x = this[--i], this[i] = this[i - inc], this[i - inc] = x);
return this;
};
})();
</pre>
Then of course, we need to know when the projectile hits anything. So while we check for collisions between the enemy and other obstacles, we also call the projectile.HIT() method below. Now these waves don't dissipate upon hitting something, in fact they pass right through. So what's the best method for determining collisions?<br />
<br />
Unlike collisions that ricochet, we don't need any fancy vector math to get the angles or energy transfer. All we really need to know is how close the hit was.<br />
<br />
I decided on a simple formula for this. Once the collision is detected, we keep track of the exact distance between the center of the enemy and the center of the projectile. Then, when the distance starts to increase, we record the closest point and call the appropriate function for the enemy's reaction. That distance is passed to help determine the power (effectiveness) of the wave. This code is part of the "projectile" object constructor.<br />
<pre class="prettyprint">this.HIT = function (Epos,Ehalo,Ename){
var dis = getDistanceSq(this.pos,Epos);
if (dis.ds > (this.halo.r+Ehalo.cr)*(this.halo.r+Ehalo.cr)) return false;//not close enough
//have it check to see if it has reached the closest location
if (!this.hitList[Ename]) {
this.hitList[Ename] = dis.ds;
return true;
}
if (this.hitList[Ename] === "HIT") return false;
// they have already hit...is it getting closer or farther?
if (this.hitList[Ename] >= dis.ds) { //closer
this.hitList[Ename] = dis.ds;
return true;
} else {
// use the closest distance and calculate damage
//takewave is the call that tells the enemy what to do with the "damage"
activeGuyDict.Enemies[Ename].takeWave(this.mass,this.hitList[Ename],this.color);
//remove the enemy from the list so he isn't hit again!
this.hitList[Ename] = "HIT";
delete this.hitList[Ename];
return true;
}
};
this.hitList = {};
// This is the helper function to get the squared distance
function getDistanceSq(a,b){ //for when the square will do, to save energy rooting it...
var x = b.cx - a.cx;
var y = b.cy - a.cy;
var ds = (x*x)+(y*y);
return {ds:ds,x:x,y:y};
}</pre>
getDistanceSq() is a handy little function that uses ol' Pythagoras, but returns the distance squared (and the x and y vector) as an object. By using the square for comparison, we save the trouble of using a costly Math.SQRT().<br />
<br />
One more blog post on the click cycle for battle mechanics, and then a DEMO! Yay! It will be playable, however without sound. And it won't be hard since you can't die yet... (one thing at a time).Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-88906451194870019592015-04-16T07:33:00.000-07:002015-04-16T07:34:03.201-07:00Projectiles as Javascript ObjectsIn my game, like many games, there is a chance to throw, shoot, fling or otherwise propel something hazardous at your enemy.<br />
<br />
I'll get more into the specific game mechanics in the next post. For now, we'll just focus on aiming and firing "something".<br />
<br />
I chose to treat each projectile as a separate entity, just like the characters and enemies. However since there are an unknown number of projectiles coming and going at any time, they don't need names. Thus we use an array to collect them.<br />
<br />
The nice thing about Javascript objects, is that we can make them do whatever we want. I decided to make a list object that not only keeps the projectiles, but adds and removes them thus:<br />
<br />
<pre class="prettyprint">var projectiles = {
list:[],
add : function (type,pos,x,y,mass,speed,range,color){
var newP = new projectile(type,pos,x,y,mass,speed,range,color);
newP.parent = this;
this.list.push(newP);
},
remove : function (p){
for (var i = this.list.length - 1; i >= 0; i--) {
if (this.list[i] == p) {
this.list.splice(i, 1);
return;
}
}
}
}</pre>
<br />
Then I created an object constructor for the projectiles thus:
<br />
<pre class="prettyprint">function projectile(type,pos,x,y,mass,speed,range,color){
this.parent = null;
this.type = type;//wide, dart, explosive...etc
this.pos = {"cx":pos.cx,"cy":pos.cy};
this.halo = {cr:4,r:8};
this.heading = normVec(pos,{"cx":x,"cy":y});//normalized vector vx and vy
this.mass = mass;
this.speed = 8;
this.range = 500;
this.irange = 500;
this.color = color;
this.delay = 25;//frame delay to allow for attack animation
this.HIT = function (Epos,Ehalo,Ename){}
this.hitList = {};
this.move = function (){};
this.kill = function (){
this.parent.remove(this);
};
}
</pre>
<br />
Most of these properties can be used in many ways to determine the exact behaviors. For now I'll be focusing on the HIT() and move() functions.<br />
<br />
The move() function will be called every frame and move the projectile by multiplying the heading by the speed. It also checks to verify that the range of the projectile has not been exceeded. If it has it calls kill().<br />
<br />
<br />
<pre class="prettyprint">this.move = function (){
this.pos.cx += this.heading.vx * this.speed;
this.pos.cy += this.heading.vy * this.speed;
this.range -= this.speed;//loses power as it moves
if (this.range <= 0) this.kill();
};</pre>
<br />
Easy. Now we have to DRAW them... For this game, the projectiles are actually a sound wave. So I used the arc() method of the canvas 2d context. By using some gradients and color stops, we can make a nice row of faded arcs. I created an "origin" for the center of the arcs 80 px behind the center. That way the arcs themselves are still drawn on the point that will be used for collisions. Then add a gradient that goes from the color to alpha 0. If you get the gradient just right, the arcs fade into nothing.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-2wvb6XH25EQ/VS_GgSlSO7I/AAAAAAAAARQ/kcD8DO5TuPY/s1600/waveprojectile.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/-2wvb6XH25EQ/VS_GgSlSO7I/AAAAAAAAARQ/kcD8DO5TuPY/s1600/waveprojectile.gif" /></a></div>
<br />
<br />
<pre class="prettyprint">for (var p = projectiles.list.length - 1; p >= 0; p--){
if (projectiles.list[p].delay > 0) {
projectiles.list[p].delay--;
} else {
//draw the projectile
var originx = projectiles.list[p].pos.cx - (projectiles.list[p].heading.vx * 80);
var originy = projectiles.list[p].pos.cy - (projectiles.list[p].heading.vy * 80);
var c = projectiles.list[p].color;
var rangeD = projectiles.list[p].range;
if (rangeD > 100) rangeD = 1;
else rangeD = rangeD / 100;
var dr = projectiles.list[p].irange - projectiles.list[p].range;
for (n = 1; n<=8; n++){
if (dr < (9-n) * 4 + 10) continue;//don't draw wave lines behind the character
var wave = Math.abs((n+frameCount)%fpb-(fpb/2))*10+50;
var w = 4*(n+4);
var nOffset = 4*n+48;
var offX = projectiles.list[p].heading.vx * nOffset + originx;
var offY = projectiles.list[p].heading.vy * nOffset + originy;
var b = Math.pow(-1,n)*20 + 80;//for now
if (b === 100) b = wave;
var Gradient3 = ctx.createRadialGradient(offX,offY,0,offX,offY,w);
Gradient3.addColorStop(0 , 'hsla(' + c + ',' + wave + '%,' + b + '%,' + rangeD + ')');
Gradient3.addColorStop(1 , 'hsla(' + c + ',' + wave + '%,' + b + '%,0)');
ctx.beginPath();
var nAngle = getAngle(projectiles.list[p].heading);
ctx.arc(originx,originy,nOffset, nAngle - (Math.PI/4), nAngle + (Math.PI/4), false);
ctx.strokeStyle = Gradient3;
ctx.lineWidth = 2;
ctx.stroke();
}
projectiles.list[p].move();//move each projectile
}
}</pre>
<br />
This code could be rolled into the projectile object... but I left it out while writing. Oh well. #thingsiwillfixlater #imtoolaz<br />
<br />
The delay at the beginning is there to hold the drawing until the animation of the character has time to complete his "attack". Although I don't yet have any animations sooo it just counts down for now.
<br />
<br />
The 'framecount' and 'fbp' (frames per beat) are global variables that keep track of where in the beat the animation will be drawn. This is used for synchronization with the music. Obviously not very necessary, but I think it will look cooler if the wave pulses with the sounds.<br />
<br />
And in case you haven't seen previous posts, the getAngle() function is below. It takes an x,y vector and turns it into radians. Thus telling us which way to "point" the arcs.<br />
<pre class="prettyprint">function getAngle(p2,p1){//takes two positions as parameters, or a single vector
if (!p1) var angle = Math.atan2(p2.vy,p2.vx);
else var angle = Math.atan2(p2.cy-p1.cy,p2.cx-p1.cx);
return angle;
}</pre>
<br />
Full code and a playable demo (without sound for now) coming soon. Next post will be about how the waves work in the game play mechanics and the use of color and "dissonance".
Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-62276299489102011452014-12-12T19:57:00.004-08:002015-04-16T07:33:45.019-07:00Accidents Do HappenSo after four great posts about how to avoid collisions, now it's time to talk about collision detection.<br />
<br />
Our characters are now successfully moving from point A to point B and usually avoiding each other. Just like in real life though, when it's crowded, people bump into each other. This looks like a job for Collision Detection!<br />
<br />
The concept is pretty simple: in my sample, each entity is described by a circle, or more practically, a center point and a radius. We merely determine if they overlap or touch. Solved.<br />
<br />
UNLESS you also want to know the <i>point of collision</i> and the force of the collision. Then you need some fancy vector math. Here's a <a href="http://gamedevelopment.tutsplus.com/tutorials/when-worlds-collide-simulating-circle-circle-collisions--gamedev-769" target="_blank">great link</a> to explain the details. I'll break it down into the important steps though.<br />
<h4>
Step 1: Did the objects collide?</h4>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-si6k0rNj0qA/VHy1uA9Lr7I/AAAAAAAAAPo/ebPz5-HAoLY/s1600/Screen%2Bshot%2B2014-12-01%2Bat%2B1.38.16%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/-si6k0rNj0qA/VHy1uA9Lr7I/AAAAAAAAAPo/ebPz5-HAoLY/s1600/Screen%2Bshot%2B2014-12-01%2Bat%2B1.38.16%2BPM.png" /></a></div>
This is easy math. We take the sum of the radii of each object (since they are all treated like circles). If the distance between their centers is <i>less</i> than sum of the radii, the objects have collided. Since the Math.sqrt() function is somewhat costly, it's faster to skip the square root when determining the distance and use the square fo the sum of the radii.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-PcoL0juhb-4/VIu4khTwaqI/AAAAAAAAAQU/606BQ7SKeVw/s1600/collision.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-PcoL0juhb-4/VIu4khTwaqI/AAAAAAAAAQU/606BQ7SKeVw/s1600/collision.jpg" /></a></div>
<h4>
Step 2: What was the collision point?</h4>
Some more simple math. The collision occurs at a point directly between the centers of the two circles. DUH. If the objects have an equal radius it will be exactly between them. If they are different sizes though, we just use the radii to create a ratio*. If (x1,y1) and (x2,y2) are the centers then the collision point (x,y) can be calculated thus.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-FH2dGQcFZlQ/VIu1gt0Rx7I/AAAAAAAAAQA/Iq0zwzRc8ig/s1600/Screen%2BShot%2B2014-12-12%2Bat%2B10.38.49%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/-FH2dGQcFZlQ/VIu1gt0Rx7I/AAAAAAAAAQA/Iq0zwzRc8ig/s1600/Screen%2BShot%2B2014-12-12%2Bat%2B10.38.49%2BPM.png" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-FhCDwyGSN8c/VIu1gqiKiiI/AAAAAAAAAP8/v0gGsR7FaxQ/s1600/Screen%2BShot%2B2014-12-12%2Bat%2B10.39.02%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-FhCDwyGSN8c/VIu1gqiKiiI/AAAAAAAAAP8/v0gGsR7FaxQ/s1600/Screen%2BShot%2B2014-12-12%2Bat%2B10.39.02%2BPM.png" /></a></div>
And because this is true:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-tMexpwCo0FM/VIu1go8tgLI/AAAAAAAAAP4/wBkzaQsjzB0/s1600/Screen%2BShot%2B2014-12-12%2Bat%2B10.39.12%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-tMexpwCo0FM/VIu1go8tgLI/AAAAAAAAAP4/wBkzaQsjzB0/s1600/Screen%2BShot%2B2014-12-12%2Bat%2B10.39.12%2BPM.png" /></a></div>
...point 1 and 2 can be reversed.<br />
<br />
<i>*Note: Since we only check for collisions each frame, this method does not account for the fact that the true time of collision actually happened between frames. Thus, the "true" collision point is usually different. However, for the sake of simplicity, this method is more practical.</i><br />
<h4>
Step 3: How hard did they hit?</h4>
Now we get some fancy math. When two objects collide, they exert force on each other and "bounce" like pool balls. Newton was a smart guy. This is the code to calculate everything. It returns the new velocity for the "guy" passed to the function. You can add that to his movement vector to simulate bounce, or apply it as the new velocity if it is free-floating in space.<br />
<br />
<pre class="prettyprint">function findCollisions(guy){
var velx = 0;
var vely = 0;
var collided = false;
//loop through all the other objects
for (var c in dict){
if (guy === c) continue; //skip yourself
if (!dict[c].collisions) continue;//skip if collisions are off
var haloSq = dict[guy].halo.cr + dict[c].halo.cr;
haloSq *= haloSq; //use the square to save rooting...
var dist = getDistanceSq(dict[guy].pos,dict[c].pos);
if (dist.ds > haloSq) continue; //they did not collide
else if (dist.ds <= haloSq) { //they collided
//find the collision point
var collisionPointX = ((dict[guy].pos.cx * dict[c].halo.cr) + (dict[c].pos.cx * dict[guy].halo.cr)) / (dict[guy].halo.cr + dict[c].halo.cr);
var collisionPointY = ((dict[guy].pos.cy * dict[c].halo.cr) + (dict[c].pos.cy * dict[guy].halo.cr)) / (dict[guy].halo.cr + dict[c].halo.cr);
//find the TOTAL velocity of the collision
var xVelocity = dict[c].vel.vx - dict[guy].vel.vx;
var yVelocity = dict[c].vel.vy - dict[guy].vel.vy;
//use the dot product to calculate the "exit" velocity
var dotProduct = -dist.x * xVelocity + -dist.y * yVelocity;
if (dotProduct > 0){
collided = true;
var collisionScale = dotProduct / dist.ds;
var xCollision = dist.x * collisionScale;
var yCollision = dist.y * collisionScale;
//The Collision vector is the speed difference projected on the Dist vector,
//thus it is the component of the speed difference needed for the collision.
var combinedMass = dict[guy].mass + dict[c].mass;
var collisionWeightA = 2 * dict[c].mass / combinedMass;
//var collisionWeightB = 2 * dict[guy].mass / combinedMass;
velx -= collisionWeightA * xCollision;//new vel for dict[guy]
vely -= collisionWeightA * yCollision;
//new vel for activeGuyDict.Allies[c]...not necessary since it will be calculated on another pass
//B.xVel -= collisionWeightB * xCollision;
//B.yVel -= collisionWeightB * yCollision;
//draw a green dot at the collision point
ctx.fillStyle = "green";
ctx.fillRect(collisionPointX-2,collisionPointY-2,4,4);
}
}
}
if (collided) return {nvx:velx,nvy:vely};//return the new vector to change after all collisions have been detected and calculated
else return null;
}
</pre>
<br />
This is a <a href="http://codeandcompose.blogspot.com/p/blog-page.html" target="_blank">fully functioning script</a> that demonstrates my own collision avoidance/detection algorithm at work. It isn't the fastest algorithm, but it functions well enough with 32 entities on the screen. For the purposes of my own demo, there will probably be no more than 12 characters on the screen at once, so this will do fine.<br />
<h4>
Instructions:</h4>
Press any key to load the entities.<br />
Press R to organize them into a circle.<br />
Press B to have them randomly switch places.<br />
Press T to split them into 3 groups that will move to a target on the opposite side of the circle.<br />
Press G to split them into two groups. They will organize into block formations.<br />
Press B to have them exchange sides.<br />
Press P to organize them into two vertical lines.<br />
Press B to have them invert the line and switch sides.<br />
<br />
The entities turn red after a collision, until they reach their target.Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-43486219509086885142014-11-19T13:33:00.004-08:002014-11-19T13:35:54.828-08:00Point A to Point B: part 4We can now move fluidly around <a href="http://codeandcompose.blogspot.com/2014/11/point-to-point-b-part-3.html">groups of objects</a>. Amazing! On to animation and sound!<br />
<br />
Hold your horses! One more problem to solve.<br />
<br />
What if the obstacle we want to avoid is moving? I didn't realize this was a problem until THIS happened.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-RkxkpaBtv4o/VGuSYtoy6yI/AAAAAAAAAOU/UjHXt1p2v48/s1600/VG-crossFail.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-RkxkpaBtv4o/VGuSYtoy6yI/AAAAAAAAAOU/UjHXt1p2v48/s1600/VG-crossFail.gif" /></a></div>
<br />
Each character is heading towards a different target, and since their paths cross, they get "locked" with each other. It's a classic "who goes first?" routine.<br />
<i><br /></i>
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> After you...</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> Right, after Yu...</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> No, I insist, after you!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> That's what I said. After Yu!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> After me?</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> No. Yu, then Mi!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> Right. Me, then you...</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> NO! Yu before Mi. Then Hoo.</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> I don't know. Who is there?</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> Hoo is already here.</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> Just me and you. But who is going first?</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> No. First Yu, then Mi, then Hoo.</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> I don't know, but I'm going anyway!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> You can't! Not until Yu goes.</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> I'll goes when I wants to goes!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Yu:<i> Not yet! Wait for Mi!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> Who's that?</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> That's Yu!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> How can he be me?</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Yu:<i> I'm not Mi. I'm YU!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> Then who am I?!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> Oh, hello, Mi!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Mi:<i> Hello, Mr. Costello!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> Yu, Mi, Mr. Abbott. (introducing them)</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Mi:<i> Hello. I'm Mi. Nice to meet you!</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> Well at least you know who you are! Now who are YOU?</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Hoo:<i> I am Hoo.</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> That's what I asked you.</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Costello:<i> Well, we better get going. After you. (indicating Yu)</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;"><br /></span>
<span style="font-family: Georgia,"Times New Roman",serif;"><i>Abbott takes a step towards the door at the same time as Yu. He has to stop to let Yu go. The same happens for Mi and Hoo. Abbott glares at Costello to let him go. Neither goes.</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;"><br /></span>
<span style="font-family: Georgia,"Times New Roman",serif;">Costello: <i>After you...</i></span><br />
<span style="font-family: Georgia,"Times New Roman",serif;">Abbott:<i> Just GO ALREADY!</i></span><br />
<br />
<h3>
Path Crossing and Look-ahead</h3>
This problem took a lot more forethought than the others. I decided to take each character and assess the information he has available from his own point of view. That way his decision making process will be able to mimic that of people walking around. Here's what you know:<br />
<br />
- Your own target<br />
- Your direction and speed<br />
- The direction and speed of the "obstacle"<br />
<br />
What you don't necessarily know is the target of the obstacle. But this should be enough information to make a relatively informed choice. So the question is, when do we adjust our path, and when don't we.<br />
<br />
As people, if we see someone about to cross our path, we can judge our relative speeds and make an assumption about the likelihood of a collision at the point of crossing. The farther ahead we look though, the less accurate our prediction.<br />
<br />
If we look ahead 40 animation frames, we can tell if our current trajectories cross, and if the obstacle's trajectory crosses our desired path to the target. By judging the speed of each object, we can tell who will hit the point of intersection first. We'll be polite and let whoever gets there first pass.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-7RTva7TcH3w/VGz7VrYatWI/AAAAAAAAAO8/LSkKPShGWvM/s1600/crosspath.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/-7RTva7TcH3w/VGz7VrYatWI/AAAAAAAAAO8/LSkKPShGWvM/s1600/crosspath.jpg" /></a></div>
The black dashed lines indicate each object's desired path. The dotted lines represent their current trajectory. Since that is the only information BOTH parties can anticipate, we'll use it to determine who gets the right-of-way. The green dot will reach the point of intersection one frame sooner than the orange dot, so he gets to keep his current trajectory. So what do we do to adjust the direction of the orange dot?<br />
<br />
My first thought was to have him "chase" the other, by flipping the red vector 180 degrees. That proved not to work well. Instead, I found that mirroring the red vector around the desired path as an axis got the desired result.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-IcNAisTq27Y/VG0AHwcm6JI/AAAAAAAAAPY/rwoyy4IivEU/s1600/crossmirror.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-IcNAisTq27Y/VG0AHwcm6JI/AAAAAAAAAPY/rwoyy4IivEU/s1600/crossmirror.jpg" /></a></div>
As you can see, by flipping the direction around the desired path as an axis (black dashes), it drastically changes his direction (red become dotted red). Keep in mind, the red vector is added back to the desired vector to produce the final direction. This works great to avoid a potential train wreck in scenarios like this. However there are some scenarios where we want to ignore a moving obstacle all together, like chasing.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-e8GgcrAeuG0/VGz7YlxvHoI/AAAAAAAAAPE/0VpFS5w0IZs/s1600/crosschase.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-e8GgcrAeuG0/VGz7YlxvHoI/AAAAAAAAAPE/0VpFS5w0IZs/s1600/crosschase.jpg" /></a></div>
Everyone loves a good chase. But if we have no chance of catching the obstacle we are following, then we might as well ignore its anti-gravity vector completely. What about a good ol' fashioned game of chicken?<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-MzTw9wXdZHc/VGz7YynuyQI/AAAAAAAAAPI/yxuf4p6lQBQ/s1600/crossheadon.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-MzTw9wXdZHc/VGz7YynuyQI/AAAAAAAAAPI/yxuf4p6lQBQ/s1600/crossheadon.jpg" /></a></div>
<br />
Naturally we don't want a crash. Fortunately, the normal collision avoidance algorithm takes care of this. And with both objects trying to avoid each other, they do it quite effectively.<br />
<br />
Here was my first attempt at making it all work.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-9CwLBdeY9zU/VGwomvbygaI/AAAAAAAAAOk/ejnUdfSGAjg/s1600/VG-cross1.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/-9CwLBdeY9zU/VGwomvbygaI/AAAAAAAAAOk/ejnUdfSGAjg/s1600/VG-cross1.gif" /></a></div>
<br />
Naturally, we need to scale down the adjustments as we near the time of intersection. This makes for a smooth transition out of the adjustment and back to normal collision avoidance. This is done by using ratios created with how close teach party is to the point of intersection.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-HVrGqC3un_U/VGwom9bymAI/AAAAAAAAAOo/jV27Gr0i6ck/s1600/VG-crossGoods.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/-HVrGqC3un_U/VGwom9bymAI/AAAAAAAAAOo/jV27Gr0i6ck/s1600/VG-crossGoods.gif" /></a></div>
<br />
There's a few bits of fancy math going on in this algorithm, starting with the calculation to find the time/point of intersect between the estimated trajectories. This handy function takes two line segments as arguments and returns the point of intersection (or false for no intersection).<br />
<br />
<pre class="prettyprint">function pathIntersect(p1a,p1b,p2a,p2b){
//input 4 points as arguments, 2 line segments
//returns the intersect point if they collide or false if they do not
var s1_x, s1_y, s2_x, s2_y,ix,iy;
var s1_x = p1b.x - p1a.x;
var s1_y = p1b.y - p1a.y;
var s2_x = p2b.x - p2a.x;
var s2_y = p2b.y - p2a.y;
var s = (-s1_y * (p1a.x-p2a.x) + s1_x * (p1a.y-p2a.y))/(-s2_x * s1_y + s1_x * s2_y);
var t = ( s2_x * (p1a.y-p2a.y) - s2_y * (p1a.x-p2a.x))/(-s2_x * s1_y + s1_x * s2_y);
if (s >= 0 && s <= 1 && t >= 0 && t <= 1){
// Collision detected, save the intersection point if necessary
var ix = p1a.x + (t * s1_x);
var iy = p1a.y + (t * s1_y);
return {x:ix,y:iy};
}
return false; // No collision
}</pre>
Did you notice in the last gif that the vectors turning "D" are practically pointing in the wrong direction? The adjustment I found most useful was to rotate the vectors around the path, like a mirror image. Here's the handy function for that. It mirrors mvec around vec as an axis.<br />
<br />
<pre class="prettyprint">function mirrorVector(vec,mvec){
// mirrors mvec around vec as an axis
//get a line for vec through origin:
//y = vec.vy/vec.vx * x
//get a perpendicular line through mvec:
//y = (vec.vx/vec.vy) * x + (mvec.vy - ((vec.vx/vec.vy)*mvec.vx))
//find the intersect point
var iy = (mvec.vy + (vec.vx / vec.vy * mvec.vx)) / (1 + ((vec.vx*vec.vx)/(vec.vy*vec.vy)));
var ix = vec.vx / vec.vy * iy;
var newx = 2 * (ix - mvec.vx) + mvec.vx;
var newy = 2 * (iy - mvec.vy) + mvec.vy;
return {"vx":newx,"vy":newy};
}</pre>
In addition to these helpers, we must insert this code in the original "move" function.<br />
<br />
<pre class="prettyprint">av.vx = Math.cos(aTheta)*mag;
av.vy = Math.sin(aTheta)*mag;
//place this code after the above lines from part 3.
////////experimental adjustment for crossing paths////////
var d40 = getVecLength(character.vel) * 40 + (character.w/2);//adjust this line!!!
// distance to obstacle must be less than velocity x 40 frames
// delta must be between -90 and 90
// other guy must be moving, not stationary
if (d < d40 && dx > -Math.PI/2 && dx < Math.PI/2 && Obstacles[o].isMoving()) {
var vv = crossPathFix(d40,d,vector,v,av,character,Obstacles[o]);
v.vx = vv.v.vx;
v.vy = vv.v.vy;
av.vx = vv.av.vx;
av.vy = vv.av.vy;
}
///////////End of experimental adjustment/////////////////
</pre>
Here is the code for the fix. There are a few more helper functions at the bottom. <br />
<br />
<pre class="prettyprint">function crossPathFix (d40,dis,vector,v,av,guy1,guy2){
//distance in 40 frames depending on current speed
//distance to path crosser
//desired vector
//angle of path crosser
//adjusted angle
//make line segments from current positions to anticipated position in 40 frames
var g1 = guy1.lookAhead(vector);
var g1v = guy1.lookAhead();
var g2 = guy2.lookAhead();
intersect = pathIntersect(g1.pos,g1.des,g2.pos,g2.des);//desired path and other's path
intersect2 = pathIntersect(g1v.pos,g1v.des,g2.pos,g2.des);//current path and other's path
ctx.strokeStyle = "hsl(30,50%,50%)";
ctx.beginPath();
ctx.moveTo(g1.pos.x,g1.pos.y);
ctx.lineTo(g1.des.x,g1.des.y);
ctx.stroke();
ctx.strokeStyle = "hsl(30,20%,20%)";
ctx.beginPath();
ctx.moveTo(g2.pos.x,g2.pos.y);
ctx.lineTo(g2.des.x,g2.des.y);
ctx.stroke();
if (!intersect) {//
var ratio1 = (1-(1.25*dis/d40))*(1-(1.25*dis/d40));
if (dis/d40 > 0.8) ratio1 = 0;
//account for the angle of direction too...are they heading toward or away from one another?
var ng1 = normalize(guy1.vel);
var ng2 = normalize(guy2.vel);
var ratiod = -(((ng1.vx + ng2.vx) * (ng1.vx + ng2.vx)) + ((ng1.vy + ng2.vy) * (ng1.vy + ng2.vy)))/2 + 1;//between -1 and 1
if (ratiod < 0) ratiod = 0;
ratiod += ratio1;
return {"v":{"vx":v.vx * ratiod,"vy":v.vy * ratiod},
"av":{"vx":av.vx * ratiod,"vy":av.vy * ratiod}};
}
//intersect2 is used to determine who should go first
if (intersect2) {//the current paths also intersect
//number of frames until intersect
var t1 = getDistance(intersect2,g1.pos) / guy1.speed;
var t2 = getDistance(intersect2,g2.pos) / guy2.speed;
} else {
//number of frames until intersect
var t1 = getDistance(intersect,g1.pos) / guy1.speed;
var t2 = getDistance(intersect,g2.pos) / guy2.speed;
//if (t2 > t1 || t1 < 1) return {"v":v,"av":av};
}
//no adjustment if t1 < 1... nearly crossing the path
// guy1 will cross the path first, no adjustment
if (t2 > t1 || t1 < 1) return {"v":v,"av":av};
var ratio = t2/t1;//between 0 and 1
var ratio1 = (1-(1.25*dis/d40))*(1-(1.25*dis/d40));
if (dis/d40 > 0.8) ratio1 = 0;
var ratio2 = 3;// * ((d40 - (t1 * guy1.speed)) / d40 + 1);//should yield ratio between 2 and 4
if (t2 - t1 > 50) ratio2 = ratio2 * (1 - ((t2 - t1 - 50) / 50));
if (ratio2 < 0) ratio2 = 0;
if (t1 > 80) ratio2 = ratio2 * (120-t1)/40;
//another adjustment to help ease the transition once their path is almost crossed
if (t2 < 20 && !intersect2) ratio2 = ratio2 * (t2 / 20) * ratio;
ratio2 += ratio1;
var newv = mirrorVector(vector,v)
var newav = mirrorVector(vector,av);
return {"v":{"vx":newv.vx * ratio2,"vy":newv.vy * ratio2},
"av":{"vx":newav.vx * ratio2,"vy":newav.vy * ratio2}};
}
</pre>
Helper functions and object constructor...<br />
<br />
<pre class="prettyprint">function getDistance(a,b){
//provided the x.y position of two objects, find the distance,
//cx and cy represent the center
if (a.cx && b.cx){
var x = b.cx - a.cx;
var y = b.cy - a.cy;
} else {
var x = b.x - a.x;
var y = b.y - a.y;
}
var d = Math.sqrt((x*x)+(y*y));
return d;
}
///this goes in the constructor for each Obstacles object
this.lookAhead = function(vector){
if (vector) {
var nx = vector.vx * 120 * this.speed + this.pos.cx;
var ny = vector.vy * 120 * this.speed + this.pos.cy;
} else {
var nx = this.vel.vx * 120 + this.pos.cx;
var ny = this.vel.vy * 120 + this.pos.cy;
}
return {"pos":{"x":this.pos.cx,"y":this.pos.cy},"des":{"x":nx,"y":ny}};
//position and destination coordinates
};</pre>
<br />
There it is. My complete algorithm to avoid obstacles! (Satisfaction not guaranteed).<br />
<br />
I intend to make a simple web app to demonstrate with full script available for download. Then I can test it out in extreme circumstances and see how it holds up. For the purposes of the game, there will probably be a maximum of 12 characters on the screen at once.<br />
<br />
Stay tuned for a related issue: collision detection! Even with a great avoidance algorithm, collisions are inevitable. And frankly, they make games fun!Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-47154214510993935642014-11-17T21:53:00.004-08:002014-11-19T13:35:27.975-08:00Point A to Point B: part 3The problem solving continues. In <a href="http://codeandcompose.blogspot.com/2014/11/point-to-point-b-part-1.html">part 1</a> we explored how to avoid objects in front of us, but not behind us. In <a href="http://codeandcompose.blogspot.com/2014/11/point-to-point-b-part-2.html">part 2</a> we solved the issue of avoiding objects directly in our path.<br />
<br />
Now what's wrong? THIS:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-9kuLmZamgeI/VGgdybXWuzI/AAAAAAAAANA/YGw_yFi7Sqg/s1600/VG-2obsFail.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-9kuLmZamgeI/VGgdybXWuzI/AAAAAAAAANA/YGw_yFi7Sqg/s1600/VG-2obsFail.gif" /></a></div>
<br />
If you look carefully, there are two characters in the way, "Mickey" and "Talan". Since the art is so bad (sorry!) it's hard to tell, but they are perfectly the same distance from Shytor's path to the enemy, Ultros.<br />
<br />
Didn't we already solve this? Well, no. We set the algorithm to adjust for something directly in our path, but not for two items creating equal but opposing anti-gravity forces.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-TbbN25f-3dA/VGggbU_tDqI/AAAAAAAAANM/0AHeB_Erll8/s1600/cancelout.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-TbbN25f-3dA/VGggbU_tDqI/AAAAAAAAANM/0AHeB_Erll8/s1600/cancelout.jpg" /></a></div>
Whether we use the direct anti-gravity force vectors (red lines) or the adjusted ones (blue lines) they cancel each other out because the obstacles are <i>exactly</i> the same distance from the path. While this may be a rare circumstance, we have to consider the implications. When faced with multiple objects to avoid, how do we choose which way to go?<br />
<br />
The answer was actually a bit more simple than I originally thought. All we have to do is add the original anti-gravity vectors <i>first, </i>and then adjust the remaining vector, as if it was one obstacle to avoid.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-v0u9wFMMk-8/VGgiIOuZa8I/AAAAAAAAANY/DgYdWKyKqw8/s1600/canceloutfix.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-v0u9wFMMk-8/VGgiIOuZa8I/AAAAAAAAANY/DgYdWKyKqw8/s1600/canceloutfix.jpg" /></a></div>
The yellow line represents the sum of the original anti-gravity vectors (red). The green line represents the vector after the adjustment. Obviously we choose to go to the right, because that's the side of the road you should drive on (sorry British colonies!).<br />
<br />
Now, no matter how many obstacles there are, we end up with two vectors to make our choice: first the sum of all the adjusted vectors, and the adjusted vector of the sum of the original anti-gravity vectors. Follow?<br />
<br />
The sum of the adjusted vectors line is not visualized in the picture above. If it was, it would be a short vector pointing in the same direction as the yellow line.<br />
<br />
But out of those two vectors, which do we use? In this scenario, we want to use "green", but in others we might want to use our previous algorithm. Here's a basic description of how it works.<br />
<br />
Let's call the sum of adjusted vectors "B" (sum of the blue lines above) and the adjusted sum of vectors "A" (green line above). If we add them together, we get a new vector, X. Then we find the difference between this new vector and the angle to the target: we'll call it "delta". If delta is near 180, then we want to ignore either A or B. We can defer to A or B depending on the situation, if either vector is near 180 degrees from the target angle, we want to use the other. I used the following equation to scale the deferral smoothly (it's a quarter ellipse since "delta" is less than 180 degrees). We end up with a value between 0 and 1 that is multiplied by the B vector.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-SiPByh7kjS4/VGgyn0DXmpI/AAAAAAAAAN0/FSTPqVvCNUc/s1600/Screen%2BShot%2B2014-11-16%2Bat%2B12.10.39%2BAM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-SiPByh7kjS4/VGgyn0DXmpI/AAAAAAAAAN0/FSTPqVvCNUc/s1600/Screen%2BShot%2B2014-11-16%2Bat%2B12.10.39%2BAM.png" /></a></div>
In code:<br />
<br />
<pre class="prettyprint">if (delta != 0) igFac = Math.sqrt(1 - Math.pow(((2 * delta) - Math.PI) / Math.PI,2));
else igFac = 0;
vector.vx += A.vx + (igFac*B.vx);
vector.vy += A.vy + (igFac*B.vy);
</pre>
<br />
It could also be a linear scale to avoid using the costly Math.sqrt()
function more than necessary. However, computers are getting faster, and
I like smooth transitions. The result is this:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-8ZpeRShFR5I/VGgkNHd5yPI/AAAAAAAAANk/2dq2pH2JjZo/s1600/VG-2obsGood.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-8ZpeRShFR5I/VGgkNHd5yPI/AAAAAAAAANk/2dq2pH2JjZo/s1600/VG-2obsGood.gif" /></a></div>
<br />
Notice that once he makes a choice to go right, the normal algorithm takes over since both obstacles are now to his left. Problem solved! What else could go wrong?<br />
<br />
THIS:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-JoUBU4FRSeU/VGjPNcpRnNI/AAAAAAAAAOE/lR61K86_-1Q/s1600/VG-superfail.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/-JoUBU4FRSeU/VGjPNcpRnNI/AAAAAAAAAOE/lR61K86_-1Q/s1600/VG-superfail.gif" /></a></div>
<br />
This particular issue was actually a simple math error, but you get the idea. Here's the code.<br />
<br />
<pre class="prettyprint">function moveGuy(character){
//sanity check, no movement if there is no target
if (!character.target) return;
//find the vector to the target and normalize
//then convert it to an angle in radians
//the "pos" property contains x,y coordinates
var vector = normVec(character.pos,character.target.pos);
var angle = Math.atan2(vector.vy,vector.vx);
//declarations
var d = 0;
var delta = 0;
var r = 0;
//this is the sum of the anti-gravity, yellow line
var AGSumx = 0;
var AGSumy = 0;
//this is the sum of the adjusted vectors, blue line
var AdjAGSumx = 0;
var AdjAGSumy = 0;
//"Obstacles" is a dictionary of all obstacles to avoid
for (var o in Obstacles){
if (o === character.name) continue; //skip yourself
if (o === character.target.name) continue;//skip your target
//get the distance to the obstacle
d = getDistance(character.pos,Obstacles[o].pos);
//calculate the anti-gravity magnitude
//the halo.cr property is the width with a buffer
var mass = (character.halo.cr + Obstacles[o].halo.cr);
//multiply by "personal space" constant for math fudging
//this effects the strength of the antigravity
var mass = mass * mass * 3;
var mag = mass / (d * d);
//v is anti-gravity vector (red)
var v = normVec(Obstacles[o].pos,character.pos,mag);
var av = {"vx":0,"vy":0}; //av is adjusted vector (blue)
//angle for the red lines
var vTheta = Math.atan2(v.vy,v.vx);
var obsAngle = 0;//angle to the obstacle
if (vTheta >= 0) obsAngle = vTheta - Math.PI;
else if (vTheta < 0) obsAngle = vTheta + Math.PI;
//get the difference between the angle to target and obstacle
//correct it be between -180 and 180
delta = obsAngle - angle;
if (delta > Math.PI) delta = delta - (2*Math.PI);
if (delta < -Math.PI) delta = delta + (2*Math.PI);
//magnitude of the force is scaled based on direction
r = (1 + Math.cos(delta))/2; //unit cardioid
r = r * r * r;
//get the difference between the target vector and antigravity vector
delta = vTheta - angle;
if (delta > Math.PI) delta = delta - (2*Math.PI);
if (delta < -Math.PI) delta = delta + (2*Math.PI);
//make the adjustment to get the blue lines
if (delta != 0) {
if (Math.abs(delta)>=Math.PI/2)var r2 = 1 - Math.sqrt(1 - Math.pow(((2 * Math.abs(delta)) - Math.PI) / Math.PI,2));//inverted quarter elipse
else {var r2 = 0;}// if delta > 90 else 0
var theta = Math.PI*r*r2/2;
//one method of correcting the sign if the angles are negative
var dir = Math.abs(delta)/delta;
var aTheta = vTheta - (theta * dir);
} else {
var aTheta = vTheta;
}
//convert the blue line angle to a vector
av.vx = Math.cos(aTheta)*mag;
av.vy = Math.sin(aTheta)*mag;
AGSumx += v.vx*r;//sum of red vectors (yellow)
AGSumy += v.vy*r;
AdjAGSumx += av.vx*r;//sum of blue vectors
AdjAGSumy += av.vy*r;
}//end for loop
//to fix the splitting issue, choose a direction.
//this algorithm has to choose which of the vectors to use
//so it's a bit more complex.
//basically it scales vectors to 0 based on their direction relative to the target
//magold is mag of yellow, magnew is mag of sum of blue
var magold = Math.sqrt((AGSumx*AGSumx)+(AGSumy*AGSumy));
var magnew = Math.sqrt((AdjAGSumx*AdjAGSumx)+(AdjAGSumy*AdjAGSumy));
var newx = 0;//placeholder for the adjusted anti-gravity sum (green)
var newy = 0;
//only adjust the yellow if the magnitude is greater than the sum of blue
if (magold >= magnew){
//convert the vector ratio to an angle, between 90 and 0
var newTheta = -(1-(magnew/magold))*(1/(magnew+1))*(Math.PI/2);
//find the difference between the old vector and the target vector
//is it between 90 and 180?
var oldVangle = Math.atan2(AGSumy,AGSumx);//yellow line
delta = oldVangle - angle;//diff from target vector to yellow
if (delta > Math.PI) delta = delta - (2*Math.PI);
if (delta < -Math.PI) delta = delta + (2*Math.PI);
//translate dTheta from between 90 and 180 to a ratio
if (Math.abs(delta) > Math.PI/2) {
//linear scaling
var axxx = (Math.abs(delta) - (Math.PI/2))/(Math.PI/2);
/square and give it a sign
axxx = axxx * axxx * (delta/Math.abs(delta));/
} else { axxx = 0;
}
var finalAngle = newTheta * axxx;
//calculate the adjustment, this is the green line
newx = AGSumx*Math.cos(finalAngle) - AGSumy*Math.sin(finalAngle);
newy = AGSumy*Math.cos(finalAngle) + AGSumx*Math.sin(finalAngle);
newx *= 1 - (magnew/magold);//adjust magnitude based on inverted mag ratio
newy *= 1 - (magnew/magold);
newx *= 1/(magnew + 1);
newy *= 1/(magnew + 1);
newx *= Math.abs(axxx);//if the old vector isn't near 180, don't add it
newy *= Math.abs(axxx);
}
//this scales out the sum of adjusted vectors
//first get the sum of both the adjusted vectors
var igAng = Math.atan2(newy + AdjAGSumy,newx + AdjAGSumx);
//find the difference between this combined vector and the target vector
delta = Math.abs(igAng - angle);
if (delta > Math.PI) delta = (2*Math.PI) - delta;
//if it's near 180 degrees from the target vector, we don't use it
//the sum of blue will be fine
if (delta != 0) var igFac = Math.sqrt(1 - Math.pow(((2 * delta) - Math.PI) / Math.PI,2));//quarter ellipse equation
else var igFac = 0;
//the movement vector is green vector + sum of blue vectors scaled
vector.vx += newx + (igFac*AdjAGSumx);
vector.vy += newy + (igFac*AdjAGSumy);
//normalize the vector, so it can be used for movement
vector = normalize(vector);
//set the movement vector for next frame
//then move on to the next character
character.nextmove(vector);
}</pre>
<a href="http://codeandcompose.blogspot.com/2014/11/point-to-point-b-part-4.html">Part 4</a> comes with the thrilling conclusion and something I initially failed to consider at all: moving obstacles!Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-32321396998281551342014-11-13T20:36:00.005-08:002014-11-19T13:44:15.310-08:00Point A to Point B: part 2Okay. So we <a href="http://codeandcompose.blogspot.com/2014/11/point-to-point-b-part-1.html">solved the issue of optimizing our anti-gravity obstacle avoidance algorithm by using a cardioid</a>. What problem could possibly be next?<br />
<br />
Well, how about, what happens if the obstacle is <i>straight ahead</i>?<br />
<br />
<h3>
Choosing Left or Right</h3>
<br />
It is possible, and even likely, that the obstacle will be <i>directly</i> in the path of the character. So if anti-gravity of the obstacle is pushing in the exact opposite direction of the movement vector, what happens when you get too close to the obstacle?<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://4.bp.blogspot.com/-wa9vs5NTdtc/VGV7WNePQAI/AAAAAAAAAKo/fhvl68sf15I/s1600/direct.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-wa9vs5NTdtc/VGV7WNePQAI/AAAAAAAAAKo/fhvl68sf15I/s1600/direct.jpg" /></a></div>
The forces eventually cancel out, or it's possible that the anti-gravity will exceed the movement vector. Normally the obstacle is outside the direct path, and the character gets nudged more and more as he moves closer, eventually rolling around. But with perfect math in a digital game, THIS is possible.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://2.bp.blogspot.com/-F2FF3mgi7yM/VGV77pNfBoI/AAAAAAAAAKw/5pPQ91KusnA/s1600/VG-1obsFail.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://2.bp.blogspot.com/-F2FF3mgi7yM/VGV77pNfBoI/AAAAAAAAAKw/5pPQ91KusnA/s1600/VG-1obsFail.gif" /></a></div>
<br />
The solution is simple to you or me... just pick a direction and go around, duh?! But computers aren't so clever... they need to be told what to choose.<br />
<br />
In this perfect scenario, the angle of anti-gravity is exactly 180 from the angle of the path. Ideally, this is when we want to adjust our path the most, since there is something REALLY in our way. So, let's just adjust the angle of anti-gravity by 90 degrees!<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://2.bp.blogspot.com/--gAr68Ij4DQ/VGV-cfQHU1I/AAAAAAAAAK8/nbClV8NUrsE/s1600/adjust1.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://2.bp.blogspot.com/--gAr68Ij4DQ/VGV-cfQHU1I/AAAAAAAAAK8/nbClV8NUrsE/s1600/adjust1.jpg" /></a></div>
The red line is the initial anti-gravity vector, the blue line is the new adjusted version.<br />
<br />
Done. Solved! Wait, but now we are making a huge adjustment. As soon as the obstacle is not directly in our path (probably after one frame) we'll only be making the same small adjustment... so let's scale the adjustment. If the angle of anti-gravity is off by 180 degrees, we adjust a lot, if it is off by 90 or less, we don't adjust.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://1.bp.blogspot.com/-s_BAYpSptQc/VGWGUhROYlI/AAAAAAAAAMg/Vu3c6KGFeY0/s1600/adjust2.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://1.bp.blogspot.com/-s_BAYpSptQc/VGWGUhROYlI/AAAAAAAAAMg/Vu3c6KGFeY0/s1600/adjust2.jpg" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
</div>
Here's the code:<br />
<br />
<pre class="prettyprint">//get the difference between the path vector and the obstacle
//invert it as necessary get a result between -180 and 180
delta = vTheta - angle;
if (delta > Math.PI) delta = delta - (2*Math.PI);
if (delta < -Math.PI) delta = delta + (2*Math.PI);
if (delta != 0) {
//this creates an inverted quarter ellipse
//that we use to scale the adjustment smoothly
if (Math.abs(delta)>=Math.PI/2)var r2 = 1 - Math.sqrt(1 - Math.pow(((2 * Math.abs(delta)) - Math.PI) / Math.PI,2));
else {var r2 = 0;}// if delta > 90 degrees
var theta = Math.PI*r*r2/2;
//one method of correcting the sign if the angles are negative
var dir = Math.abs(delta)/delta;
var aTheta = vTheta - (theta * dir);
} else {
var aTheta = vTheta;
}
//this calculates the vector of the new blue line
//as seen in the diagrams above
av.vx = Math.cos(aTheta)*mag;
av.vy = Math.sin(aTheta)*mag;
//then add these to the movement vector! </pre>
The result is a nice fluid movement around the object. It even makes the path react a bit sooner, so your character isn't waiting to the last minute to make a reasonable adjustment. In this code, it chooses left (d*mn hippy!).<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://1.bp.blogspot.com/-qdEBITvBhGE/VGWCo8qvYlI/AAAAAAAAALQ/jF0fGWSo4-M/s1600/VG-1obsGood.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://1.bp.blogspot.com/-qdEBITvBhGE/VGWCo8qvYlI/AAAAAAAAALQ/jF0fGWSo4-M/s1600/VG-1obsGood.gif" /></a></div>
<br />
Done. Solved! Or not. Stay tuned for <a href="http://codeandcompose.blogspot.com/2014/11/point-to-point-b-part-3.html">part 3</a>, where I discuss another scenario we must overcome. What if there are 2 obstacles perfectly evenly spaced directly in our path?Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0tag:blogger.com,1999:blog-6658844084624700142.post-17546475668489120202014-11-12T12:46:00.002-08:002014-11-19T13:41:53.663-08:00Point A to Point B: part 1Welcome to my very first post! What is this blog about, you ask? Let's get right down to it.<br />
<br />
I'm a composer, who wants to make a game, or at least a demo. I'll talk more about what my idea is with each post, but for now we'll focus on the basics, since that's about all my programming skills can handle.<br />
<br />
I'm using javascript as my language for now, but we'll see where this goes.<br />
<br />
The game will be a top-down strategy/RPG style game. In order to manage a team of characters, they will have to have some kind of individual movement AI. For nearly all the actions, the character will have to move from point A to point B on his/her own. Attacking is the most fun action, so here we go...<br />
<h2>
<span style="font-size: large;">Action one: Attack.</span></h2>
You choose a "Hero", then choose an enemy. The Hero walks over, body-slams the sh*t out of the enemy and walks back to his home position. Easy, right? But what if there is something in his way?<br />
<br />
When I first researched solutions for this simple problem (oh, how naive I was) I first encountered information about path-finding, and A* algorithms. Once my head stopped spinning, I stumbled across a much more elegant solution: <a href="http://buildnewgames.com/vector-field-collision-avoidance/" target="_blank">vector fields</a>.<br />
<br />
To simplify the explanation, imagine each obstacle has anti-gravity. The closer you get to it, the more it "pushes" you away. This is nice, because as your movement vector pushes you in one direction, the force of the "anti-gravity" pushes you away, and eventually around said obstacle.<br />
<br />
To illustrate, the green ball is moving to the right. As it approaches the obstacle, the obstacle exerts its "force" and pushes the ball away. As you can see in the exquisite to-scale drawing, the force is stronger based on proximity.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://3.bp.blogspot.com/--eCfk9HkzwE/VGWDoyKYkQI/AAAAAAAAALc/m2wIL2Orqyc/s1600/antigravity.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://3.bp.blogspot.com/--eCfk9HkzwE/VGWDoyKYkQI/AAAAAAAAALc/m2wIL2Orqyc/s1600/antigravity.jpg" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<br />
Great! Problem solved. NEXT!<br />
<br />
Or not. Here's the issue (the first of many). The anti-gravity is exerted in all directions equally, which so as the green ball moves past the orange one (to the point where the orange ball is no longer an obstacle) he is STILL affected by its anti-gravity. Now, why would you want to avoid an obstacle you've already passed?<br />
<br />
Well, some programmers have solved this by using a complex "look-ahead" to see just how in-the-way the obstacle is. But I found a much simpler solution. Math-geeks, prepare yourselves!<br />
<h3>
<span style="font-size: small;">Cardioids</span></h3>
<span style="font-size: small;">I heart cardioids. No, seriously. If you don't know what one is, plug this equation into a <a href="https://www.desmos.com/calculator" target="_blank">graphing calculator</a>.</span><br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://2.bp.blogspot.com/-FgqFggXh-Ds/VGWDy6gTxRI/AAAAAAAAALw/gNclWy2xKpM/s1600/Screen%2Bshot%2B2014-11-11%2Bat%2B9.57.04%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="//2.bp.blogspot.com/-FgqFggXh-Ds/VGWDy6gTxRI/AAAAAAAAALw/gNclWy2xKpM/s1600/Screen%2Bshot%2B2014-11-11%2Bat%2B9.57.04%2BPM.png" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<span style="font-size: small;">Imagine that normal anti-gravity is calculated in a circle (i.e. objects in all directions are treated equally) so why not use a cardioid? This allows us to calculate 100% anti-gravity on objects directly in our path, and scale to 0% for objects behind us. And multiplying the cardioid by a power increases the effect. These images show the circle vs. cardioid being applied to the moving player, aimed in the direction of the target.</span><br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://4.bp.blogspot.com/-u4XFwxNvgps/VGWDyvCfQcI/AAAAAAAAAMQ/BgBqMzJHiYo/s1600/Screen%2Bshot%2B2014-11-11%2Bat%2B9.22.46%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://4.bp.blogspot.com/-u4XFwxNvgps/VGWDyvCfQcI/AAAAAAAAAMQ/BgBqMzJHiYo/s1600/Screen%2Bshot%2B2014-11-11%2Bat%2B9.22.46%2BPM.png" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
</div>
"Shytor" is moving to the left. "M" is at 45 degree angle to his motion, so is sort of in the way. The colored lines represent the forces exerted on him due to anti-gravity. His resulting vector pushes him down a little, to avoid M.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://1.bp.blogspot.com/-LdwaLTiUeuM/VGWDyQKOxdI/AAAAAAAAALs/phdPP2XZ9Pc/s1600/Screen%2Bshot%2B2014-11-11%2Bat%2B9.21.20%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://1.bp.blogspot.com/-LdwaLTiUeuM/VGWDyQKOxdI/AAAAAAAAALs/phdPP2XZ9Pc/s1600/Screen%2Bshot%2B2014-11-11%2Bat%2B9.21.20%2BPM.png" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<span style="font-size: small;">Here, "Shytor" and "D" are moving toward their target, "Ultros". The colored lines protruding from Shytor represent the forces of anti-gravity exerted on him (I'll explain those in detail later). Both M and D are in his way (a little). D has nothing directly in his way, so there are no colored lines (very tiny ones). Without the cardioid, D would be more affected by M, but due to the scaling, the effect is negligible.</span><br />
<span style="font-size: small;"><br /></span>
<span style="font-size: small;">The result is a change in overall path. Notice the "circle" path continues to push away from orange ball, even after it is passed. Whereas, the cardioid, straightens out once it is clear. The perfectly to-scale anti-gravity vector lines (in red) show how the effect diminishes greatly as the green ball passes its target.</span><br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://3.bp.blogspot.com/-uklBk-EFHb4/VGWDy3gsZdI/AAAAAAAAAL0/rTyaUgWrKAM/s1600/cardioid.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/-uklBk-EFHb4/VGWDy3gsZdI/AAAAAAAAAL0/rTyaUgWrKAM/s1600/cardioid.jpg" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<span style="font-size: small;"><br /></span>
So that's the explanation. SHOW ME THE CODE!<br />
<br />
This code would run each frame, and can be looped if there are multiple obstacles present. The "orange" object is the obstacle. <br />
<br />
<pre class="prettyprint">function moveObject(green,orange){
//first get the normalized vector of green's target
//(i.e. which direction is he trying to move)
//this returns vector object with vx and vy properties
var vector = normVec(green.pos,green.target.pos);
//convert the vector to an angle in radians, 0 is East
var angle = Math.atan2(vector.vy,vector.vx);
//get the distance to the obstacle
d = getDistance(green.pos,orange.pos);
//calculate the anti-gravity magnitude based on distance
//it's not really the mass, but the "width" of the obstacle
var mass = (green.radius + orange.radius);
//multiply by "personal space" constant for math fudging
//this adjusts the strength of the anti-gravity
var mass = mass * mass * 2;
//the magnitude of the effect as distance approaches the "mass" is 1
var mag = mass / (d * d);
//find the angle between the two objects (as an "x, y" vector)
//multiplying by the magnitude
var v = normVec(orange.pos,green.pos,mag);
//convert the angle to radians
var vTheta = Math.atan2(v.vy,v.vx);
//invert it to get the "anti-gravity force"
var obsAngle = 0;
if (vTheta >= 0) obsAngle = vTheta - Math.PI;
else if (vTheta < 0) obsAngle = vTheta + Math.PI;
//get the difference between angles to the target and the obstacle
delta = obsAngle - angle;
//invert if more than 180 deg, this keeps the value in a usable range
if (delta > Math.PI) delta = delta - (2*Math.PI);
//invert if less than -180
if (delta < -Math.PI) delta = delta + (2*Math.PI);
//make a unit cardioid, if the difference in angles is 0 effect is 1
//if angle is 180 effect is 0
r = (1 + Math.cos(delta))/2;
//multiply the magnitude exponentially (optional)
r = r * r * r;
//add the calculated anti-gravity force to the original vector
vector.vx += v.vx*r;
vector.vy += v.vy*r;
//after all anti-gravity is calculated then move the character
vector = normalize(vector);//normalize the new movement vector
//then call the movement function of the character
//basically multiply the direction vector by speed
//and move to the next frame
green.move(vector);
}
//helper functions
function getDistance(a,b){
//provided the x.y position of two objects, find the distance
//cx and cy represent the center
var x = b.cx - a.cx;
var y = b.cy - a.cy;
var d = Math.sqrt((x*x)+(y*y));
return d;
}
function normVec(a,b,mag){
//find the direction vector between two points, multiplied by mag
if (!mag) mag = 1;
var x = b.cx - a.cx;
var y = b.cy - a.cy;
var d = Math.sqrt((x*x)+(y*y));
var v = {"vx": x / d * mag,"vy": y / d * mag};
return v;
}
function normalize(v){
//normalizes a vector object to 1
var d = Math.sqrt((v.vx*v.vx)+(v.vy*v.vy));
var v = {"vx": v.vx / d,"vy": v.vy / d};
return v;
} </pre>
<br />
Stay tuned for <a href="http://codeandcompose.blogspot.com/2014/11/point-to-point-b-part-2.html">part 2</a> where I solve the next problem: choosing left or right!<br />
<br />
The tile artwork is from "<span class="Apple-style-span" style="font-style: italic;">Wilderness Tile Set</span>" art by Daniel Cook (<a href="http://lunar.lostgarden.com/labels/free%20game%20graphics.html" target="_blank">Lostgarden.com</a>). Thanks to him for making some great free artwork available!
Anonymoushttp://www.blogger.com/profile/05659216724764279171noreply@blogger.com0