26 April 2018

jQuery Game Map

For my PHP browser game, I needed a mini-game where the user can make their griffin run around a map and check out locations. First I looked at lots of JS libraries, thinking I wanted lots of features: tile map, smooth movement, sprite sheets, scrolling viewport, callbacks, pathfinding... but it was just too complicated to try and customise everything exactly, and I gave up. Did I really need a super fancy engine? Did the maps really need to use tiles? They seemed to slow everything down, and made 'organic' map design more difficult too.

I decided to try a simpler approach from scratch: plop a griffin sprite inside a div and move it around using jQuery. That was easy. Then build a HTML/CSS map underneath it, and start attaching each single movement to a change in X or Y variable. That resulted in the griffin smoothly moving one 'square' at a time, and each location being fixed at a certain 'co-ordinate' relative to the starting point, as if there was an invisible tile map. For 'collision detection', check if griff is already standing next to an impassable tile (or the map boundary) and if so, don't let them move in that direction towards it. And finally, it's simple to add 'quests' using hidden variables, make messages pop up when griff stands on certain co-ords, or change the animated sprites when griffin is running in different directions.

Here I'll show it step by step, in case it interests anyone.
Warning: I'm just a hobbyist, NOT a professional coder! There are some things I don't know how to code properly - the fake, way-too-long method of 'collision detection' will probably make you feel sick - but it works for my tiny simple game, that's all. If you're good at JS, you're welcome to try changing stuff and making it more efficient. :)

Below is the finished demo if you wanna try it out. Please note, these sprites are not for public use. You can borrow them for your learning/testing, but no more.

Use buttons to move griffin. Preferably hold them down. And preferably after all these directions have loaded ;)
Nest
The nest in the hillside is empty. Your hatchmates have all flown away.
X: 50
Y: 50

Rabbits caught:
0

Who's that big griffin over there?

1. Basic movement

Let's begin with the HTML and CSS. Make four buttons, that will correspond to directions, and make a div that will hold the griffin.
<style>
#griff {position:absolute;top:50px;left:50px;width:30px;height:30px;background-image:url(http://griffusion.elementfx.com/pics/20pxblack.gif);background-repeat: no-repeat;background-position: center;}
<style>
<div id='griff'></div>
<input id='btnN' type='button' value='North'><br>
<input id='btnW' type='button' value='West'>
<input id='btnE' type='button' value='East'>
<br><input id='btnS' type='button' value='South'>
Each button needs a unique ID so jQuery can identify which one is being clicked, if any. The griffin, or to be specific the invisible box it's in, also needs an ID, of course.
So, that jQuery...
<script>

var isDown = false; // by default, mouse is not clicking anything, make this variable false

$goEast = $("#btnE");
$goWest = $("#btnW");
$goNorth = $("#btnN"); // recognising each button...
$goSouth = $("#btnS");

$goEast.mousedown(function(){isDown = true;}); // a btn is clicked? set that variable to 'true'!
$goWest.mousedown(function(){isDown = true;});
$goNorth.mousedown(function(){isDown = true;});
$goSouth.mousedown(function(){isDown = true;});

$(document).mouseup(function(){ // mouse is no longer clicking anything
if(isDown){ isDown = false; }
});

// when mouse is clicking a button, run a function: moveSomewhere...
// and that '+=20' part is the parameter to send it. How many pixels to move each time
$goEast.mousedown(function() {moveHoriz('+=20');});
$goWest.mousedown(function() {moveHoriz('-=20');});
$goNorth.mousedown(function() {moveVert('-=20');});
$goSouth.mousedown(function() {moveVert('+=20');});

function moveHoriz(dist){
$( "#griff" ).animate({ 'left': dist +'px' }, 500, function() { if (isDown){ moveHoriz(dist); } });
}

function moveVert(dist){
$( "#griff" ).animate({ 'top': dist +'px' }, 500, function() { if (isDown){ moveVert(dist); } });
}

</script>

I hope the comments explain it a little! For the functions, you'll notice they move the griff div's position horizontally or vertically, by the dist number in the parameters. CSS counts things as going 'leftwards' or 'downwards', so top: +50 actually means 50 pixels away from the top. It's weird.

You might wonder: "What's the point of the isDown variable? Can't we just run the function when the mouse is down on each button?" Well yes, you can remove those bits, click a button and the griff will move... click by click. I think the user would like to hold down a button and see the griff keep running - so the script must keep checking. It's easy to set a variable, keep it fixed all the time the mouse is down, then turn it 'off' whenever the mouse goes up.

(Also there's another reason I need an "is the mouse up?" function - to change the griffin's animation back to Idle, when it has stopped running. We'll get to that graphical stuff later.)

2. Diagonal movement

It's very easy, in theory. Instead of moving griffin to the left OR downwards, we need to go both ways at once. Something like 'left': dist +'px', 'top': dist +'px'...
But there's a problem. This works for down AND left, or up AND right, as the same parameter (plus or minus 20) is being used for both directions. What about the other two directions? We need to go +20 pixels up, but -20 left, etc.

We will need two separate parameters here. One for the horizontal distance, one for vertical. Let's also give each direction its own function (you'll soon see the other reasons I need to do this). Set up the extra four buttons, in the same way as the first four, and then...

$goEast.mousedown(function() {moveEast('+=20');});
$goWest.mousedown(function() {moveWest('-=20');});
$goNorth.mousedown(function() {moveNorth('-=20');});
$goSouth.mousedown(function() {moveSouth('+=20');});
// note the extra parameters added inside function ( ) brackets
$goNorthWest.mousedown(function() {moveNW('-=20','-=20');});
$goNorthEast.mousedown(function() {moveNE('+=20','-=20');});
$goSouthWest.mousedown(function() {moveSW('-=20','+=20');});
$goSouthEast.mousedown(function() {moveSE('+=20','+=20');});

function moveWest(dist){
$( "#griff" ).animate({ 'left': dist +'px' }, 500, function() { if (isDown){ moveWest(dist); } });
}

function moveEast(dist){
$( "#griff" ).animate({ 'left': dist +'px' }, 500, function() { if (isDown){ moveEast(dist); } });
}

function moveNorth(dist){
$( "#griff" ).animate({ 'top': dist +'px' }, 500, function() { if (isDown){ moveNorth(dist); } });
}

function moveSouth(dist){
$( "#griff" ).animate({ 'top': dist +'px' }, 500, function() { if (isDown){ moveSouth(dist); } });
}

// same as above, but using extra parameter to go 2 ways
function moveNE(dist1,dist2){
$( "#griff" ).animate({ 'left': dist1 +'px', 'top': dist2 +'px' }, 700, function() { if (isDown){ moveNE(dist1,dist2); } });
}

function moveNW(dist1,dist2){
$( "#griff" ).animate({ 'left': dist1 +'px', 'top': dist2 +'px' }, 700, function() { if (isDown){ moveNW(dist1,dist2); } });
}

function moveSE(dist1,dist2){
$( "#griff" ).animate({ 'left': dist1 +'px', 'top': dist2 +'px' }, 700, function() { if (isDown){ moveSE(dist1,dist2); } });
}

function moveSW(dist1,dist2){
$( "#griff" ).animate({ 'left': dist1 +'px', 'top': dist2 +'px' }, 700, function() { if (isDown){ moveSW(dist1,dist2); } });
}

Now you should be able to make the griffin slide diagonally. Notice the 700 in these functions, rather than the 500? It's the number of milliseconds jQuery takes to move the griff. I made diagonal movement a bit slower than horiz/vert; otherwise the player might as well run diagonally everywhere :)

3. Co-ordinates

This is still pretty empty, isn't it? There's no sense of a map; the griff is floating in space. And we can't make anything interactive, or keep the griffin inside the map boundaries, if we don't know where the griffin and other 'objects' are, relative to each other.

So let's add some HTML and CSS, and place the griffin div inside a map div. I'll also add a convenient 'starting place' box positioned in the same spot as the griffin, so we can measure how far it has travelled.

<style>
.area {width:300px;height:300px;border:1px solid green;position:relative;margin-bottom:20px;}
.starting {position:absolute;top:49px;left:49px;width:30px; height:30px;border:1px dashed #b2c7ea;}
#griff{position:absolute;top:50px;left:50px;width:30px;height:30px; background-image:url(http://griffusion.elementfx.com/pics/20pxblack.gif);background-repeat: no-repeat;background-position: center;}
</style>

<div class='area'>
<div class='starting'></div>
<div id='griff'></div>
</div>

// buttons under here
You should notice how the griff's floaty movements are actually strictly consistent. Never a pixel out of place! It's a bit like a tiled map, but not.

We need some X and Y co-ordinates so the 'game' can keep track of where griff is. It's simple: set two global variables at the beginning of the script, which will be the starting numbers, and then every time griff moves one 'space', change one or both of those variable numbers depending on the direction.

Then we'll need to display those numbers on screen (for testing purposes) and see them update in real time. With jQuery we can replace the contents of divs with new stuff, so let's make two divs for X and Y - with unique IDs, of course! - and plop them under the map.
<style>
#xpos {display:inline-block;}
#ypos {display:inline-block;}
</style>

// map divs

<div id='xpos'><b>X:</b> 50 miles</div> <div id='ypos'><b>Y:</b> 50 miles</div>

// buttons
The two global variables will be griffx and griffy. Inside every direction's function, we add or subtract 1, to one or both of them. For example, make East and West visually update the X number:
var griffx = 50;
var griffy = 50;


// other stuff here

function moveWest(dist){
griffx = griffx - 1;
$( "#griff" ).animate({ 'left': dist +'px' }, 500, function() { if (isDown){ moveWest(dist); } });
$( '#xpos' ).html( '<b>X:</b> ' +griffx+ ' miles' );
}

function moveEast(dist){
griffx = griffx + 1;
$( "#griff" ).animate({ 'left': dist +'px' }, 500, function() { if (isDown){ moveEast(dist); } });
$( '#xpos' ).html( '<b>X:</b> ' +griffx+ ' miles' );
}
And it's exactly the same for the Y. In the diagonal functions, you'd change both vars and update both divs.
However the latter is redundant and inefficient. Let's make a separate function that updates both divs (and any extra counters/divs you may wanna add on screen later) and just call that function. Call it at the end of every movement function, so the vars have been updated already.
function showXY(){
$( '#xpos' ).html( '<b>X:</b> ' +griffx+ ' miles' );
$( '#ypos' ).html( '<b>Y:</b> ' +griffy+ ' miles' );
}

function moveWest(dist){
griffx = griffx - 1;
$( "#griff" ).animate({ 'left': dist +'px' }, 500, function() { if (isDown){ moveWest(dist); } });
showXY();
}

function moveEast(dist){
griffx = griffx + 1;
$( "#griff" ).animate({ 'left': dist +'px' }, 500, function() { if (isDown){ moveEast(dist); } });
showXY();
}
Now you should see the co-ordinates changing neatly. See, it really is just like a tiled map! (I am so inexplicably pleased by this)

4. Finding things

The map is a bit useless if there's nothing for griffin to look at or pick up. Let's think of some co-ordinates where interesting things can be noticed. There's not actually anything there... we just check griff's position: "if griff is 52 miles east and 59 miles south, make a message pop up saying there's a tower here".

Likewise, let's invent a lake at 57,54 and 57,55 (it can be multiple 'tiles' long) and whenever the griff is at the starting position (50,50) this place is said to be the nest.

Let's make a few simple message divs with HTML/CSS:
<style>
#nest {width:160px;padding:15px;background-color:#f4deb7;border:1px solid #998358;border-radius:5px;position:absolute;top:150;left:290px;}
#lake {width:160px;padding:15px;background-color:#d7e1f2;border:1px solid #3e5e91;border-radius:5px;position:absolute;top:150;left:290px;}
#tower {width:160px;padding:15px;background-color:#bbbec1;border:1px solid #525456;border-radius:5px;position:absolute;top:150;left:290px;}
</style>

<div class='area'>
<div class='starting'></div>

<div id='nest'><b>Nest</b><br>The nest is empty. Your hatchmates have all flown away.</div>

<div id='lake' style='display:none;'><b>Lake</b><br>You found a large body of water.</div>

<div id='tower' style='display:none;'><b>Tower</b><br>You found a crumbling stone tower.</div>

<div id="griff"></div>
</div>
We begin at the nest, so the message can be visible right away. The other two should be hidden until we get to them, hence the "display:none" extra CSS.

It doesn't matter where you put these divs, HTML-wise. I like them all to appear in the same fixed location by the side of the 'map', so made them position:absolute, relative to the map's position, and put their divs inside the map div, along with the griffin and starting box. We'll need to do this with other graphics like trees or mountains later - position them exactly according to the map.

So how do we check griffin's position and make the messages appear? It's a simple IF statement. If conditions are true, show div... ELSE hide the div. We need to include the hide bit, for when griff moves away from the thing. For convenience, all these 'place checks' can go in one function... and let's call it during the showXY function, so it will also happen after griffx/griffy have been updated.
function showXY(){
$( '#xpos' ).html( '<b>X:</b> ' +griffx+ ' miles' );
$( '#ypos' ).html( '<b>Y:</b> ' +griffy+ ' miles' );
checkPlace();
}

function checkPlace(){
if (griffx == 50 && griffy == 50){ $('#nest').show(500); } else{ $('#nest').hide(500); }
if ((griffx == 57 && griffy == 54) || (griffx == 57 && griffy == 55)){ $('#lake').show(500); } else{ $('#lake').hide(500); }
if (griffx == 52 && griffy == 59){ $('#tower').show(500); } else{ $('#tower').hide(500); }
}
When you slide the griffin over those 'tiles', the message boxes should pop up.

5. Not really collision detection

I'll be honest here: I haven't the slightest idea how to do proper detection. Instead my idea is very crude, maybe backwards: identify walkable tiles next to ones I want to block, and in the relevant move functions, block griff from moving in that direction, if griff is on that walkable tile.

This is OK for simple map borders; "if griff is already at top of map, don't move them north".
It's horrible for more specific tiles though, like a tree in the middle of nowhere. All 8 move functions will have to check if griff is just north/east/west... of it, and refuse to move them there. >__<

Oh well, this map is small and simple so I don't mind hard-coding a few things.
Let's make another message div, with the ID of 'nogo'. The CSS is the same as the other three. We'll show it to the player when they try to cross a boundary, like so:
function moveNorth(dist){
if(griffy == 48){ $('#nogo').show(300); } // show message
else{ $('#nogo').hide(300);
griffy = griffy - 1;
$( "#griff" ).animate({ 'top': dist +'px' }, 500, function() { if (isDown){ moveNorth(dist); } });
showXY(); }
}
And I'll choose two more spots inside the map that will be inaccessible. One will hold an NPC, the other will be a rock. While it's a bit of a chore looking up all the co-ordinates to write in for each function, it is easy to add them, at least. Just add the 'OR' operator, ||, and group each pair of co-ords together in () so the script considers the pairs separate to each other.
function moveNorth(dist){
if(griffy == 48 || (griffx == 56 && griffy == 50) || (griffx == 52 && griffy == 58)){ $('#nogo').show(300); }
else{ $('#nogo').hide(300);
griffy = griffy - 1;
$( "#griff" ).animate({ 'top': dist +'px' }, 500, function() { if (isDown){ moveNorth(dist); } });
showXY(); }
}
The north border is 47 (Y), and the two obstacles are at 56,51 and 52,59. This is the moveNorth function, so we have to see if griff is standing directly south of these 'tiles', and stop them moving up. For the moveSouth, you'd instead check the griff is just north of these places, and stop them moving down. Yeah, I really need to think of a better system...

The invisible map is getting quite annoying, so I quickly painted a background at this point. It, uhh, isn't exactly top-down (sorry, I'm not used to drawing that stuff) and it definitely doesn't look 10+ miles wide, LOL. The 'tower' turned into a tree trunk, the 'lake' became a piddly little stream, the NPC will be 2 tiles wide (this griffin must be a chick or something) and I ended up drawing a jungle wall and waterfalls at the top. No problem for code though! Just have to edit a few things. And I'll add a bunch more description-divs as the chick runs around, just for flavour.

OK, I did just realise one nice thing about this 'collision' system: it can allow griff to move on certain tiles only from certain directions. Such as letting them jump down a riverbank (moveSouth) but not being able to climb back up (moveNorth). I think I'll make the tree trunk enterable from underneath, but not the top or sides. If griff could move up and down, out of the trunk onto the grass behind it, that would look unnatural and lose any sense of perspective.

6. A quest

As you probably guessed from Ravenbeak's dialogue box, I want to add a little hunting challenge. Four rabbits will be placed around the map. When you move the griff over them, they'll be caught... meaning +1 is added to a counter, and the rabbit sprite should disappear.
After you catch them all, maybe Ravenbeak will notice you. There are also three convenient places to hide treasures (tree trunk, nest and pool) so those can be picked up too. Hmm, let's... hide them until after you've caught the rabbits. Yes. The elder will tell you to go and find some shinies - changing a variable, like a quest flag - and then you can notice them.

(I take a moment here to clean up all the CSS: put the X/Y divs and 8 buttons neatly on one side, and the status display on the other: div ID 'task' shows "Rabbits caught: X" or "Treasures: X" (it will change to the current task) and div ID 'ravbeak' shows a hint, what to do next. Yes, all of these are in divs with IDs, so we can update the contents later.)
Choose some co-ords for the rabbits, make four unique-ID divs with their sprites as the background images, and position:absolute those divs on the map at the right places. When griff catches them, show message about hunting, remove the sprite, and global variable rabbit will increase by one.

There are two ways to keep track of hunted rabbits. One would be to have four separate global variables, turning from 'no' to 'yes' or from 0 to 1, and when you move to a rabbit tile, hide the rabbit div and update variable. On later visits, check "if rabbit1 == 'yes', show nothing" as it has already been hunted.
The other method is to use jQuery's Remove to actually delete the rabbit div from this page. Instead of checking a variable, check if this div exists. Obviously this method is kinda permanent, but it's OK for this little game as I'm not using the rabbits for anything else later.

// another message div, inside map div HTML like the rest
<div id='catch' style='display:none;'><b>Hunted!</b><br>You caught a rabbit.</div>

... below, hunting added to checkPlace function:

if (griffx == 48 && griffy == 54){
if ( $( "#rab1" ).length ){ rabbits = rabbits+ 1; // if div 'rab1' exists, +1 to caught
$( '#task' ).html( 'Rabbits caught: <b>' +rabbits+ '</b>' ); // update it on page too...
$('#catch').show(500); $('#rab1').remove(); // show hunt msg, and delete 'rab1'
$('#catch').delay(3000).hide(500); } // after 3 second delay, hide message again
}
First time, hunting message appears, number increases and the rabbit vanishes. Walk away and back again, nothing happens now. Repeat this bit of code for the four rabbits.

Now, I'll give the player a hint to talk to Ravenbeak again, by updating the 'quest status' div to say something new, if 4 rabbits have been caught but variable raven is still 0. When you reach them, it becomes 1, and the second set of dialogue is shown.
I might as well set up the three items too. The "rabbits caught: 4" counter will change to "treasures found: 0" to reflect the new task.find1, find2, find3 will all turn to 1 when you pick them up later, and the quest status will urge you to take them to Ravenbeak. Finally, raven will update to 2, and the elder will be satisfied.

// added to end of showXY function

if (rabbits == 4 && raven == 0){
$( '#ravbeak' ).html( 'Will this impress them?' ); }
if(find1 == 1 && find2 == 1 && find3 == 1){
$( '#ravbeak' ).html( 'They <i>must</i> like these shinies!' ); }

// in checkPlace function, editing the two tiles in front of RB
if ((griffx == 56 && griffy == 50) || (griffx == 57 && griffy == 50)){
if (rabbits < 4){ $('#raven').show(500); } // starting text
// update raven, show second text
if (rabbits == 4){ raven = 1; $('#raven2').show(500);

// change "Rabbits caught" to new counter
$( '#task' ).html( 'Treasure found:<br><b>' +items+ '</b>' );

// ... and update quest status with a hint
$( '#ravbeak' ).html( 'Ravenbeak waits for you to find some treasures.' ); }

// if all items found, update again, show final text
if(find1 == 1 && find2 == 1 && find3 == 1){ raven = 2; $('#raven3').show(500);

// ... and quest status says you're done.
$( '#ravbeak' ).html( 'Ravenbeak is satisfied. Well done!' ); }
}
// hide any potential text, when moving away
else{ $('#raven').hide(500); $('#raven2').hide(500); $('#raven3').hide(500);
}

Yep, that seems to be working smoothly. Now, the three items are meant to be hidden, so there's no point having sprites or divs for them, unlike the rabbits. So I'll have to track and check for them with the variables only. No problem:
// editing checkPlace, the nest position
if (griffx == 50 && griffy == 50){
if(raven == 1 && find1 == 0){ // must be on quest 2, and not found anything here yet
items = items + 1; find1 = 1; // find something!
$( '#task' ).html( 'Treasure found:<br><b>' +items+ '</b>' ); // show it...
$('#shiny').show(500); $('#shiny').delay(3000).hide(500); // and a message...
}
$('#nest').show(500); // normal nest text always appears, regardless of conditions
}
else{ $('#nest').hide(500); } // moving away from nest
Repeat in the 3 places, go and test... the whole quest should be working from start to finish! Here it is.

7. Extra graphics

The little game is finished! Yay. But finally, the griff sprite could be a bit more realistic. It needs some nice isometric sprites running in each direction.
Fortunately, I have an old set laying around! These were drawn for a game called BeastEon, but ended up not being used (I made some 3D models instead). Hopefully they don't mind me re-using this gryphon for this tiny tutorial/test game. :) Here are the full-size animations on DeviantArt.

It's easy to change a div's background image with jQuery. Add this in every move function, and the mouseUp one, to turn griff back to standing idly.
$('#griff').css('background-image', 'url(IMAGEURL)');
Wow, it looks... uhh... questionable.
There was already a problem with movement being a bit floaty (especially when you press lots of buttons together - the griff can start swirling around in unusual directions, as jQuery gets through the movements one by one) but it makes the animations look very messy, too. The sprite turns back to 'idle' the moment you let go, meaning you must hold down the button to see the griff run - and all that sliding around into the final position! Gosh.

Now I'm honestly unsure if I'll bother with full sets of sprites, when making bigger maps for my actual game. It might be neater just to have two versions of the idle sprite, one facing each direction. When moving NE/E/SE change it to face the right, and it will carry on facing the right until you move it NW/W/SW. Currently it's not possible to have the creature stay standing 'in the right direction' when it turns idle. So... hmm, I'll see later.

Anyway, the tutorial is done! Here's the final jsFiddle.

Other ideas

This minigame only needed to be very simple for my plans, so I'm not going to show you how to do all these things too. Besides, I'm sure you can now figure out how to add some of them :) and if not, just google JS/jQuery and the issue, and you'll probably find an answer somewhere. StackOverflow is a great source.

- Add a timer to force the player to find things quickly.
- Add a fatigue counter that increases by 1 with every move, and can be reset by visiting certain tiles. If the creature gets too tired, game over.
- Make a maze. Possibly with tricky tiles that can only be entered from certain directions, automatically move player in another one, or teleport them.
- Set up some simple money-paying or item-trading with an NPC.
- Add stats etc to be raised by finding certain tiles, meeting NPCs...
- More NPCs, items, gates/barriers etc in divs, who appear and disappear at different stages of quests, so player can access new areas.

And so on. Have fun!
(If anyone does find a way of making the sprites/slidey movement neater, please let me know... I'd love to let users see their griffs running, swimming or flying around, but if they're gonna keep moonwalking and spinning around in strange directions, that would look ridiculous! Although maybe players would see the humorous side? :D )