INPUT (Click me! Click me!)

One of the most important elements of game-play is user interaction.

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.

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.

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... ?

Anyway. Step one is capturing the click. Since everything is done inside the canvas, we use the following code:

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){}
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!

Each event listener is directed to a handler function. Now we have to do, you know, logic.

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.

Or as I quickly learned, the FIRST step is to offset your click location to account for the placement of the canvas and scrolling...

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;
}
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?

We could setup the logic to see if x > 200 AND x < 250 AND y > 130 AND y < 230...

OR

... 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.

For my game, we also check to see if the character is "ready" before selecting. Now onto the real tricky logic.

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.

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.

In plain text it looks like this:
  • Check to see if a selected character is waiting to move.
    • If a character was clicked, change focus
    • If an empty space was clicked, move
  • Check to see if an attacking character is "PRIMING".
    • Was the appropriate enemy clicked?
  • Check to see if an enemy must be selected to attack.
    • Did you click an enemy?
    • If a character was clicked change focus
  • If none of those criteria are met, select the clicked character.
   
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
}
This is the mouseup handler. It only pays attention if the player is holding the mouse down to "AIM". Thus avoiding unnecessary event functions.
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;
    }
}
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!

And as promised, here's a link to the demo!

Instructions:
  • CLICK and DRAG from a character to shoot a projectile. They will not do damage, but different colors will have different effects,
  • PRESS Z or X to change the color of a character after he is selected.
  • 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.
  • CLICK the selected character again, and then click an empty space to move him.
  • No damage indication is given, just keep attacking, and the enemies will eventually disappear.

0 comments:

Color and Dissonance

So what exactly is this game concept I keep mumbling about?

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.

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".

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.

Imagine each spirit is assigned a "note" or frequency. In addition to fighting them, you can also blast them with another "note" using the wave projectiles we created here. Depending on the "interval" between the notes, a different effect is achieved.

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?

This presented me with more of a challenge than I had expected. HOW do you color all 12 notes?

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.

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.
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.

For game play purposes I grouped them like this:
Unison (same note) will heal the enemy.
5th is consonant, so no effect.
2nd (whole-tone) will have a disruptive effect, like stopping the enemy for a moment.
Minor 3rd will have a positive effect, like speeding up the enemy.
Major 3rd will have a negative effect, like slowing.
Semi-tone and tri-tones are so disruptive they will actually change the note of the enemy.

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.

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.

I had to add some properties to my good guy object:
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;
};
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 other methods of doing this here... but I liked this the best.
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;
    };
})();
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?

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.

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.
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};
}
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().

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).

0 comments:

Projectiles as Javascript Objects

7:33 AM , 0 Comments

In my game, like many games, there is a chance to throw, shoot, fling or otherwise propel something hazardous at your enemy.

I'll get more into the specific game mechanics in the next post. For now, we'll just focus on aiming and firing "something".

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.

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:

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;
            }
        }
    }
}

Then I created an object constructor for the projectiles thus:
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);
    };
}


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.

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().


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();
};

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.


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
    }
}

This code could be rolled into the projectile object... but I left it out while writing. Oh well. #thingsiwillfixlater #imtoolaz

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.

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.

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.
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;
}

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".

0 comments: