The Legend of Zelda: A Link to Your Page Add an interactive game character to your HTML
Sometimes I think we haven't played around enough with the easy stuff that the web can do.
We've gone from the age of the personal website where people from all backgrounds and skill levels made what they liked, to an age of letting either Facebook or premade templates do everything for us. It's an increasingly sophisticated and monotonous age, as sterile as an Apple products lineup, where professional designers and developers try to make sure they're doing the same thing as everybody else (IIRC something about job security in an industry where experience makes you obsolete). In short, it's an age of making your design adaptable to both desktop and phones, which means designing for the lowest common denominator.
For those of you wannabe programers who remember those days when, fascinated by what HTML and Javascript could do, you made all those cursor trailers and tiled animated gif backgrounds, here's something to help you rekindle that diversion-for-its-own-sake thrill (building off the example from my Shop page).
All you need is a simple text editor like Notepad++. Some of you may even have an old copy of Dreamweaver still laying around. Break it open and let's make a simple HTML file.
We want to be bare-bones simple - no libraries, no industrial-strength Node or React, no HTML5 canvas, no optimizing, just doing the thing we want to do. Let's start by making something you can move with the traditional WASD keys. Here's a chunk of code you can copy and paste into your HTML file that'll do just that.
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Step 1</title> <style type="text/css"> * {margin:0; padding:0; font-family:sans-serif} #stage {position:relative;} #link {position:absolute; z-index:2; width:50px; height:50px; text-align:center; line-height:3; color:#FFF; background:#333; outline:1px solid #FFF;} #link[data-key-s=true] {line-height:4;} /*makes Link face down if his "data-key-s" attribute is set to true*/ #link[data-key-w=true] {line-height:1;} /*face up*/ #link[data-key-a=true] {text-align:left;} /*face left*/ #link[data-key-d=true] {text-align:right;} /*face right*/ /*putting the left and right underneath means they'll override the top and bottom if you're moving in a diagonal direction*/ /* don't worry if this next part looks confusing, all it does is make the background look nice. Safe to ignore */ body {background:url( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAOUlEQVQ4jWPQn/r/v/7U//8ZTT+ShBkY1kIwxQaQqnE4GoBgkIspNYCQEwlFM+0NIBiYw8CAAU8HACLnAQ/QMJgdAAAAAElFTkSuQmCC") repeat center 0;} #stage {height:calc(100vh - 64px); width:calc(100vw - 64px); margin:32px auto 0; min-width:640px; min-height:512px; background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATElEQVRYhe3OoREAIAwEwS8lZVAMZVFmSmAmCAYE+MScOPPmV9Y8rHn0MVM7vyoHvENWknYAAAAAAAAAAAAAAAAAAKAc8A1J3d9qwAJDQcg59Rgs0wAAAABJRU5ErkJggg==') repeat 15px 15px; box-sizing:border-box; border:1px solid transparent; border-image-source:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAC/0lEQVR4nO3ayXHcMBBA0QlFYSgYh+UgFJxDcJV8ou2CDREbSTTwDv8wnI3ofke+Xq+Pz9fr4/Pt/YdU3OHmBZBa+g0ovfD0jWnuUicAqaosoNwHpLf3vAuAVFQxoLMvaK/OHACkL2sGVPoDWrPSvQOkrr2fAqr9QcWuds8AqWvPxYBa/0Axat0rQOraazWg3j/UXPXuEaDNewzQqBvQM43aG0CbNg2g0Tekaxu9J4A2a1pAV92g5t4LQJsUBlB6w5qjy/YM0B6FB/Tt+0/dGEAC6G84Vx9Ez8wdoMVaFlB6/elBr1Y6V4DUNO/j9bKAcgfWGDhHAKlqzun15QGdDUBtcI4AUtF8c+9vA6h0IKqbE0DqmtN2gGoHtFu1cwFogqXNFECN31cbAIAmWNpMAdQJ6OnHI6I9jgEQQACNAHTXIGZp1LkBAgigkYDS608v+qp5jQIAEEAAXQFoNUi5cwAEEEAzA1o9gAACCCCAAAoaQAABBBBAAAUNIIAAAggggIIGEEAAAQQQQEEDCCCAAAIIoKABBBBAAAEEUNAAAggggAACKGgAAQQQQAABFDSAAAIIIIAAChpAAAEEEEAABQ0ggAACCCCAggYQQAABBBBAQQMIIIAAAgigoAEEEEAAAQRQ0AACCCCAAAIoaAABBBBAAC0L6OrB3FXuHAABBNCMgFaBcwYJIIAAmgnQqnCuOjdAAAE0AlD6O7uUnh8ggAB6EtDuAQQQQHcAuvrAUaudC0ATLG2mABo8oF0rnRNA6prTNoDA6Ztv7n2AVDTf3PvLAwJn7JzT6wCpas7p9WUBgXPtvI/XAKlp3sfr5QCBc093zx2gxVoOUHogxXgcBKDNWw6Qng0g7QHo6hvWXHsBaJOmBwROjEbvCaDNmg4QODEbtTeANu1xQOCsUe8eAdq82wGBs2atewVIXXstBgTOHtXuGSB17fkUEDh7Vrp3gNS19ywgcFTiACB9WTUgcPS/ci4AUlGngMBRSakTgFTVP4DAUUt/HtgHSA0dbn4BLg5lqw4e9n4AAAAASUVORK5CYII=); border-image-slice:48; border-image-width:48; border-image-repeat:repeat;} </style> </head> <body> <div id="stage"> <div id="link" style="left:200px; top:200px;">Link</div><!--add the Left and Top positions here to specify where Link should be when the page loads; javascript will move it after--> <a href="#" style="position:fixed; bottom:100px; left:0; width:200px; text-align:right; background:#333; padding:10px; color:#FFF;">BACK</a> </div> <script> var Link = document.getElementById("link"); var currentKeys = [];//get an array ready to record currently pressed keys var UP = 'w'; var LEFT = 'a'; var DOWN = 's'; var RIGHT = 'd'; function gameLoop(){//for each frame of animation... var leftpos = parseInt(Link.style.left); //prepare to change Link's inline CSS positioning var toppos = parseInt(Link.style.top); if (currentKeys[LEFT]) Link.style.left = leftpos - 3 + 'px'; //if one of the key codes in the 'currentKeys' array is the key code for 'LEFT', set Link's left position to his current left position - 3 pixels (for every frame) if (currentKeys[RIGHT]) Link.style.left = leftpos + 3 + 'px'; //3 pixels per frame seemed like a good speed if(currentKeys[UP]) Link.style.top = toppos - 3 + 'px'; if(currentKeys[DOWN]) Link.style.top = toppos + 3 + 'px'; //the logic of your code is important: by not using "else if" with these ifs, you can have multiple keys working at the same time, which lets Link move both down and left, etc. window.requestAnimationFrame(gameLoop); //and now run this game loop again, and keep running it at as fast a rate as javascript can conveniently fit it in } document.body.addEventListener( "keydown", function(infoAboutTheKey){ currentKeys[infoAboutTheKey.key] = true; //add the key's name to the array of currently pressed keys Link.setAttribute('data-key-'+infoAboutTheKey.key, true); //point him in the direction he's walking with Data Attributes & the CSS }); document.body.addEventListener( "keyup", function(infoAboutTheKey){ currentKeys[infoAboutTheKey.key] = false; Link.setAttribute('data-key-'+infoAboutTheKey.key, ''); //blank out this key's data-key attribute, stop pointing him in the direction he's no longer walking in }); window.addEventListener( "load", function(){ gameLoop(); }); //start the game loop once the page is loaded </script></body></html>
Here we're using a game loop to check for certain things on every frame of animation. We detect whenever you hit a key, and then 1) we dig out the key's name and add it as a data attribute to Link's div for the CSS to make him face the right direction, and then 2) we add it to an array so that the game loop has a way to detect it and move him in the right direction.
This little chunk of code is all you need for a fully interactive element on your page, but the problem is he keeps moving even when he goes off the screen. We need to add a way to tell him when to stop.
Collision
Let's add a "Block" div and use the game loop to check, on each frame of animation, if we're crashing into it. Actually, wait, that's not good enough. If we're crashing, we also want to know which side we're hitting, so that we can stop moving in that one direction.
How do we do that? Let's think like our code for a bit: Javascript is blind. It can't know whether or not Link's colliding with anything until it's scanned everything. So once it scans Link and the block, can it tell where they both are? Do a little research and you find yes, by using getBoundingClientRect to read their screen coordinates. How convenient. So the way we'll know if we're colliding is if the X & Y coordinates of any of Link's 4 corners are within the X & Y coordinates of the block's 4 corners.
For example if the top of Link's 50px x 50px div is 100px down from the top of the page and the top of the block's 50px x 50px div is 90px down, is Link crashing into it? So far, he might be because vertically he's 10px in the block. But what if he's way off to the right side? It isn't until we know the left side of Link is 220px away from the left side of the page and the left side of the block is 200px away that there's definitely a collision.
Let that last paragraph soak in. It's the part I spent the most time thinking about. I had a rough time with math as a kid because guess what? Back then I couldn't think of anything fun to use it with.
How do we translate that into javascript? You could write out every condition (if block.top < link.bottom < block.bottom AND block.left < link.left < block.right OR block.left < link.right < block.right ...and that's just checking the top), but you'll find that's a ton of checks. Another quick search of StackOverflow shows that instead of checking for conditions that should be met, it'd be easier to check for the conditions that should not be met. Then once we detect there's a collision, we can tell which direction to stop moving by doing a little calculation to find which side Link's block is closest to.
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Step 2 - Collision</title> <style type="text/css"> * {margin:0; padding:0; font-family:sans-serif} #link {position:absolute; z-index:2; width:50px; height:50px; text-align:center; line-height:3; color:#FFF; background:#333; outline:1px solid #FFF;} #link[data-key-s=true] {line-height:4;} #link[data-key-w=true] {line-height:1;} #link[data-key-a=true] {text-align:left;} #link[data-key-d=true] {text-align:right;} body {background:url( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAOUlEQVQ4jWPQn/r/v/7U//8ZTT+ShBkY1kIwxQaQqnE4GoBgkIspNYCQEwlFM+0NIBiYw8CAAU8HACLnAQ/QMJgdAAAAAElFTkSuQmCC") repeat center 0;} #stage {background:url( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATElEQVRYhe3OoREAIAwEwS8lZVAMZVFmSmAmCAYE+MScOPPmV9Y8rHn0MVM7vyoHvENWknYAAAAAAAAAAAAAAAAAAKAc8A1J3d9qwAJDQcg59Rgs0wAAAABJRU5ErkJggg==') repeat 15px 15px; box-sizing:border-box; border:1px solid transparent; border-image-source:url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAC/0lEQVR4nO3ayXHcMBBA0QlFYSgYh+UgFJxDcJV8ou2CDREbSTTwDv8wnI3ofke+Xq+Pz9fr4/Pt/YdU3OHmBZBa+g0ovfD0jWnuUicAqaosoNwHpLf3vAuAVFQxoLMvaK/OHACkL2sGVPoDWrPSvQOkrr2fAqr9QcWuds8AqWvPxYBa/0Axat0rQOraazWg3j/UXPXuEaDNewzQqBvQM43aG0CbNg2g0Tekaxu9J4A2a1pAV92g5t4LQJsUBlB6w5qjy/YM0B6FB/Tt+0/dGEAC6G84Vx9Ez8wdoMVaFlB6/elBr1Y6V4DUNO/j9bKAcgfWGDhHAKlqzun15QGdDUBtcI4AUtF8c+9vA6h0IKqbE0DqmtN2gGoHtFu1cwFogqXNFECN31cbAIAmWNpMAdQJ6OnHI6I9jgEQQACNAHTXIGZp1LkBAgigkYDS608v+qp5jQIAEEAAXQFoNUi5cwAEEEAzA1o9gAACCCCAAAoaQAABBBBAAAUNIIAAAggggIIGEEAAAQQQQEEDCCCAAAIIoKABBBBAAAEEUNAAAggggAACKGgAAQQQQAABFDSAAAIIIIAAChpAAAEEEEAABQ0ggAACCCCAggYQQAABBBBAQQMIIIAAAgigoAEEEEAAAQRQ0AACCCCAAAIoaAABBBBAAC0L6OrB3FXuHAABBNCMgFaBcwYJIIAAmgnQqnCuOjdAAAE0AlD6O7uUnh8ggAB6EtDuAQQQQHcAuvrAUaudC0ATLG2mABo8oF0rnRNA6prTNoDA6Ztv7n2AVDTf3PvLAwJn7JzT6wCpas7p9WUBgXPtvI/XAKlp3sfr5QCBc093zx2gxVoOUHogxXgcBKDNWw6Qng0g7QHo6hvWXHsBaJOmBwROjEbvCaDNmg4QODEbtTeANu1xQOCsUe8eAdq82wGBs2atewVIXXstBgTOHtXuGSB17fkUEDh7Vrp3gNS19ywgcFTiACB9WTUgcPS/ci4AUlGngMBRSakTgFTVP4DAUUt/HtgHSA0dbn4BLg5lqw4e9n4AAAAASUVORK5CYII=); position:relative; border-image-slice:48; border-image-width:48; border-image-repeat:repeat; height:calc(100vh - 64px); width:calc(100vw - 64px); margin:32px auto 0; min-width:640px; min-height:512px;} </style> </head> <body> <div id="stage"> <div id="link" style="left:200px; top:200px;">Link</div> <div id="block" style="width:256px; height:64px;left:303px; top:207px; position:absolute; background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAeUlEQVRYhe3OuQ3AIBBE0S2FMijGZbkIiqMESzjwERghFnGN0QSTbLD/ibE+GOvDth9DJ+KuTQc8h1GQNwwH6A2JwrCA1pBkGB5QC8mGfwP4QrQjgAACCCBgPYD64T01BB5QGi6GwAJqw2oIHKB1OAuBAfQOJyGzASfqIyA4zUQpUAAAAABJRU5ErkJggg==') repeat 0 0;"></div> <a href="#" style="position:fixed; bottom:100px; left:0; width:200px; text-align:right; background:#333; padding:10px; color:#FFF;">BACK</a> </div> <script> var Link = document.getElementById("link"); var currentKeys = []; var UP = 'w'; var LEFT = 'a'; var DOWN = 's'; var RIGHT = 'd'; var canmoveleft=true; // you'll be free to move left if no collisions are happening on your left, etc var canmoveright=true; var canmoveup=true; var canmovedown=true; var Obstacle = document.getElementById("block"); function isCrashing(Block1, Block2) { // the genius formula we'll use to check if Link (Block1) is crashing into the obstacle (Block2) return !( Block1.bottom < Block2.top || Block1.top>Block2.bottom || Block1.right < Block2.left || Block1.left > Block2.right ); // "if none of these are true, we're crashing" } function checkforcrashes(){ var Me = Link.getBoundingClientRect(); // getBoundingClientRect gives the pixel position of the block's top and left (but doesn't include the bottom or right. Got to add that manually, below) Me.bottom = Me.top + Me.height; // bottom can be calculated like this and added to the 'Me' group Me.right = Me.left + Me.width; // get the same location info of the block: var Block = Obstacle.getBoundingClientRect(); Block.bottom = Block.top + Block.height; Block.right = Block.left + Block.width; //get the distances from each block side: var top = Math.abs(Me.bottom - Block.top); var bottom = Math.abs(Me.top - Block.bottom); var left = Math.abs(Me.right - Block.left); var right = Math.abs(Me.left - Block.right); var shortestDistance = Math.min(top, bottom, left, right); var sideOfBlockWereHitting = shortestDistance === top ? "top" : shortestDistance === bottom ? "bottom" : shortestDistance === left ? "left" : "right"; if (isCrashing(Me, Block) && sideOfBlockWereHitting === "bottom") { canmoveup = false; // "if we're crashing into the block and the shortest distance is from the bottom, stop Link from moving up" } else if (isCrashing(Me, Block) && sideOfBlockWereHitting === "top") { canmovedown = false; } else if (isCrashing(Me, Block) && sideOfBlockWereHitting === "left") { canmoveright = false; } else if (isCrashing(Me, Block) && sideOfBlockWereHitting === "right") { canmoveleft = false; } else { // and finally if we're not crashing anywhere, clear any previously stopped directions and allow full movement: canmoveleft = true; canmoveright = true; canmoveup = true; canmovedown = true; } } function gameLoop(){ var leftpos = parseInt(Link.style.left); var toppos = parseInt(Link.style.top); if (currentKeys[LEFT] && canmoveleft) { Link.style.left = leftpos - 3 + "px"; } if (currentKeys[RIGHT] && canmoveright) { Link.style.left = leftpos + 3 + "px"; } if (currentKeys[UP] && canmoveup) { Link.style.top = toppos - 3 + "px"; } if (currentKeys[DOWN] && canmovedown) { Link.style.top = toppos + 3 + "px"; } checkforcrashes(); window.requestAnimationFrame(gameLoop); } document.body.addEventListener("keydown", function(e){ currentKeys[e.key] = true; Link.setAttribute('data-key-'+e.key, true); }); document.body.addEventListener("keyup", function(e){ currentKeys[e.key] = false; Link.setAttribute('data-key-'+e.key, ''); }); window.addEventListener("load", function(){gameLoop();}); </script></body></html>
There are different ways to be clever in programing. One way is to use sophisticated tricks to write out an idea with fewer characters. That's the bad kind of clever that costs other people time who might want to pick up your work *cough arrowfunctions cough*. Then there's being clever with your logic and solving problems with simpler ideas, like this collision check. That's the good kind of clever, which makes people like you more and want to pay you more money. Try to be that kind of clever.
Important note: if you don't already know about and use your browser's 'Inspect Element' feature (right-click in the page, choose Inspect Element), you've got to start. Try that feature out on a few websites and you'll be addicted to web design in no time. Check YouTube for all the stuff it lets you do. You can use it here to check or modify Link's top pixel position and such. It'll even let you run javascript in its Console, which is a fantastic way to learn coding.
Okay so we have collision working on this one block, but that naturally leads to the next question/problem: what if we want more than one boundary on our page? Like, a lot more than one? Thank goodness for loops:
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Step 3 - Multiple Collisions</title> <style type="text/css"> * {margin:0; padding:0; font-family:sans-serif} #link {position:absolute; z-index:2; width:50px; height:50px; text-align:center; line-height:3; color:#FFF; background:#333; outline:1px solid #FFF;} #link[data-key-s=true] {line-height:4;} #link[data-key-w=true] {line-height:1;} #link[data-key-a=true] {text-align:left;} #link[data-key-d=true] {text-align:right;} .block{position:absolute; width:32px; height:32px; background:url( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAeUlEQVRYhe3OuQ3AIBBE0S2FMijGZbkIiqMESzjwERghFnGN0QSTbLD/ibE+GOvDth9DJ+KuTQc8h1GQNwwH6A2JwrCA1pBkGB5QC8mGfwP4QrQjgAACCCBgPYD64T01BB5QGi6GwAJqw2oIHKB1OAuBAfQOJyGzASfqIyA4zUQpUAAAAABJRU5ErkJggg==') repeat 0 0;} body {background:url( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAOUlEQVQ4jWPQn/r/v/7U//8ZTT+ShBkY1kIwxQaQqnE4GoBgkIspNYCQEwlFM+0NIBiYw8CAAU8HACLnAQ/QMJgdAAAAAElFTkSuQmCC") repeat center 0;} #stage {background:url( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATElEQVRYhe3OoREAIAwEwS8lZVAMZVFmSmAmCAYE+MScOPPmV9Y8rHn0MVM7vyoHvENWknYAAAAAAAAAAAAAAAAAAKAc8A1J3d9qwAJDQcg59Rgs0wAAAABJRU5ErkJggg==') repeat 15px 15px; box-sizing:border-box; border:1px solid transparent; border-image-source:url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAC/0lEQVR4nO3ayXHcMBBA0QlFYSgYh+UgFJxDcJV8ou2CDREbSTTwDv8wnI3ofke+Xq+Pz9fr4/Pt/YdU3OHmBZBa+g0ovfD0jWnuUicAqaosoNwHpLf3vAuAVFQxoLMvaK/OHACkL2sGVPoDWrPSvQOkrr2fAqr9QcWuds8AqWvPxYBa/0Axat0rQOraazWg3j/UXPXuEaDNewzQqBvQM43aG0CbNg2g0Tekaxu9J4A2a1pAV92g5t4LQJsUBlB6w5qjy/YM0B6FB/Tt+0/dGEAC6G84Vx9Ez8wdoMVaFlB6/elBr1Y6V4DUNO/j9bKAcgfWGDhHAKlqzun15QGdDUBtcI4AUtF8c+9vA6h0IKqbE0DqmtN2gGoHtFu1cwFogqXNFECN31cbAIAmWNpMAdQJ6OnHI6I9jgEQQACNAHTXIGZp1LkBAgigkYDS608v+qp5jQIAEEAAXQFoNUi5cwAEEEAzA1o9gAACCCCAAAoaQAABBBBAAAUNIIAAAggggIIGEEAAAQQQQEEDCCCAAAIIoKABBBBAAAEEUNAAAggggAACKGgAAQQQQAABFDSAAAIIIIAAChpAAAEEEEAABQ0ggAACCCCAggYQQAABBBBAQQMIIIAAAgigoAEEEEAAAQRQ0AACCCCAAAIoaAABBBBAAC0L6OrB3FXuHAABBNCMgFaBcwYJIIAAmgnQqnCuOjdAAAE0AlD6O7uUnh8ggAB6EtDuAQQQQHcAuvrAUaudC0ATLG2mABo8oF0rnRNA6prTNoDA6Ztv7n2AVDTf3PvLAwJn7JzT6wCpas7p9WUBgXPtvI/XAKlp3sfr5QCBc093zx2gxVoOUHogxXgcBKDNWw6Qng0g7QHo6hvWXHsBaJOmBwROjEbvCaDNmg4QODEbtTeANu1xQOCsUe8eAdq82wGBs2atewVIXXstBgTOHtXuGSB17fkUEDh7Vrp3gNS19ywgcFTiACB9WTUgcPS/ci4AUlGngMBRSakTgFTVP4DAUUt/HtgHSA0dbn4BLg5lqw4e9n4AAAAASUVORK5CYII=); position:relative; border-image-slice:48; border-image-width:48; border-image-repeat:repeat; height:calc(100vh - 64px); width:calc(100vw - 64px); margin:32px auto 0; min-width:640px; min-height:512px;} </style> </head> <body> <div id="stage"> <div id="link" style="left:200px; top:200px;">Link</div> <div class="block" style="width:126px; height:64px;left:303px; top:143px;"></div> <div class="block" style="width:96px; height:156px;left:79px; top:143px;"></div> <div class="block" style="width:124px; height:32px;left:175px; top:303px;"></div> <a href="#" style="position:fixed; bottom:100px; left:0; width:200px; text-align:right; background:#333; padding:10px; color:#FFF;">BACK</a> </div> <script> var Link = document.getElementById("link"); var currentKeys = []; var UP = 'w'; var LEFT = 'a'; var DOWN = 's'; var RIGHT = 'd'; var alltheObstacles = document.querySelectorAll('.block'); function isCrashing(Block1, Block2) { return !( Block1.bottom < Block2.top || Block1.top > Block2.bottom || Block1.right < Block2.left || Block1.left > Block2.right ); } function checkforcrashes() {// this gets run for every frame of animation var Me = Link.getBoundingClientRect(); Me.bottom = Me.top + Me.height; Me.right = Me.left + Me.width; var crashing = {};// if any of the blocks we're about to loop through are being crashed into, this is where we'll record it Array.prototype.forEach.call(alltheObstacles, function(currentObstacle) { // scan every block and.. var Block = currentObstacle.getBoundingClientRect(); Block.bottom = Block.top + Block.height; Block.right = Block.left + Block.width; //..get coordinates of each block if (isCrashing(Me, Block)) { var top = Math.abs(Me.bottom - Block.top); var bottom = Math.abs(Me.top - Block.bottom); var left = Math.abs(Me.right - Block.left); var right = Math.abs(Me.left - Block.right); var shortestDistance = Math.min(top, bottom, left, right); shortestDistance === top ? (crashing.top = true) : shortestDistance === bottom ? (crashing.bottom = true) : shortestDistance === left ? (crashing.left = true) : (crashing.right = true); } }); // When the code's done going through each block, the crashing var will have collected results from any crashes if (crashing.top) { canmovedown = false; } else { canmovedown = true; } // if any of the blocks on the page are being crashed into from their top, stop Link from moving down if (crashing.bottom) { canmoveup = false; } else { canmoveup = true; } if (crashing.left) { canmoveright = false; } else { canmoveright = true; } if (crashing.right) { canmoveleft = false; } else { canmoveleft = true; } // by using IFs and not ELSE IFs, you can stop movement in two directions at once (crash into a corner) } // We're giving every block we want to detect a class of 'block' in the HTML, and then telling the JS to grab all '.blocks', and then for each frame of animation, do a loop through them that records any collision, and if so on which side, then once the loop goes through them all, see if we recorded a collision on any side(s). If there WAS, set a variable to say you can't move in that direction; ELSE set that variable to say you can (in case we said false on the previous frame). This works well because you can crash into the side of one block and another and stop movement in both directions like in a corner. function gameLoop() { var leftpos = parseInt(Link.style.left); var toppos = parseInt(Link.style.top); if ( currentKeys[LEFT] && canmoveleft ){ Link.style.left = leftpos - 3 + 'px'; } if ( currentKeys[RIGHT] && canmoveright ){ Link.style.left = leftpos + 3 + 'px'; } if ( currentKeys[UP] && canmoveup ){ Link.style.top = toppos - 3 + 'px'; } if ( currentKeys[DOWN] && canmovedown ){ Link.style.top = toppos + 3 + 'px'; } checkforcrashes(); window.requestAnimationFrame(gameLoop); } document.body.addEventListener("keydown", function(e) { currentKeys[e.key] = true; Link.classList.add('direction-'+e.key); }); document.body.addEventListener("keyup", function(e) { currentKeys[e.key] = false; Link.classList.remove('direction-'+e.key); }); window.addEventListener("load", function(){ gameLoop(); }); </script></body></html>
At this point we could stop and finish up with some CSS graphics and you'd have everything needed for a stylish diversion on your page. But there's one more feature I want to add before I call this a completely satisfying project: enemy blocks that you can kill. (Feel free to skip to the CSS part now if you want to start simple, of course.)
Enemies
Let's add two features: a block with a class of 'enemy', and a way to attack it.
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Step 4 - Enemies</title> <style type="text/css"> * {margin:0; padding:0; font-family:sans-serif} .block{position:absolute; width:32px; height:32px; background:url( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAeUlEQVRYhe3OuQ3AIBBE0S2FMijGZbkIiqMESzjwERghFnGN0QSTbLD/ibE+GOvDth9DJ+KuTQc8h1GQNwwH6A2JwrCA1pBkGB5QC8mGfwP4QrQjgAACCCBgPYD64T01BB5QGi6GwAJqw2oIHKB1OAuBAfQOJyGzASfqIyA4zUQpUAAAAABJRU5ErkJggg==') repeat 0 0;} body {background:url( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAOUlEQVQ4jWPQn/r/v/7U//8ZTT+ShBkY1kIwxQaQqnE4GoBgkIspNYCQEwlFM+0NIBiYw8CAAU8HACLnAQ/QMJgdAAAAAElFTkSuQmCC") repeat center 0;} #stage {background:url( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATElEQVRYhe3OoREAIAwEwS8lZVAMZVFmSmAmCAYE+MScOPPmV9Y8rHn0MVM7vyoHvENWknYAAAAAAAAAAAAAAAAAAKAc8A1J3d9qwAJDQcg59Rgs0wAAAABJRU5ErkJggg==') repeat 15px 15px; box-sizing:border-box; border:1px solid transparent; border-image-source:url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAC/0lEQVR4nO3ayXHcMBBA0QlFYSgYh+UgFJxDcJV8ou2CDREbSTTwDv8wnI3ofke+Xq+Pz9fr4/Pt/YdU3OHmBZBa+g0ovfD0jWnuUicAqaosoNwHpLf3vAuAVFQxoLMvaK/OHACkL2sGVPoDWrPSvQOkrr2fAqr9QcWuds8AqWvPxYBa/0Axat0rQOraazWg3j/UXPXuEaDNewzQqBvQM43aG0CbNg2g0Tekaxu9J4A2a1pAV92g5t4LQJsUBlB6w5qjy/YM0B6FB/Tt+0/dGEAC6G84Vx9Ez8wdoMVaFlB6/elBr1Y6V4DUNO/j9bKAcgfWGDhHAKlqzun15QGdDUBtcI4AUtF8c+9vA6h0IKqbE0DqmtN2gGoHtFu1cwFogqXNFECN31cbAIAmWNpMAdQJ6OnHI6I9jgEQQACNAHTXIGZp1LkBAgigkYDS608v+qp5jQIAEEAAXQFoNUi5cwAEEEAzA1o9gAACCCCAAAoaQAABBBBAAAUNIIAAAggggIIGEEAAAQQQQEEDCCCAAAIIoKABBBBAAAEEUNAAAggggAACKGgAAQQQQAABFDSAAAIIIIAAChpAAAEEEEAABQ0ggAACCCCAggYQQAABBBBAQQMIIIAAAgigoAEEEEAAAQRQ0AACCCCAAAIoaAABBBBAAC0L6OrB3FXuHAABBNCMgFaBcwYJIIAAmgnQqnCuOjdAAAE0AlD6O7uUnh8ggAB6EtDuAQQQQHcAuvrAUaudC0ATLG2mABo8oF0rnRNA6prTNoDA6Ztv7n2AVDTf3PvLAwJn7JzT6wCpas7p9WUBgXPtvI/XAKlp3sfr5QCBc093zx2gxVoOUHogxXgcBKDNWw6Qng0g7QHo6hvWXHsBaJOmBwROjEbvCaDNmg4QODEbtTeANu1xQOCsUe8eAdq82wGBs2atewVIXXstBgTOHtXuGSB17fkUEDh7Vrp3gNS19ywgcFTiACB9WTUgcPS/ci4AUlGngMBRSakTgFTVP4DAUUt/HtgHSA0dbn4BLg5lqw4e9n4AAAAASUVORK5CYII=); position:relative; border-image-slice:48; border-image-width:48; border-image-repeat:repeat; height:calc(100vh - 64px); width:calc(100vw - 64px); margin:32px auto 0; min-width:640px; min-height:512px;} #link {position:absolute; z-index:2; width:50px; height:50px; text-align:center; line-height:3; color:#FFF; background:#333; border:1px solid #FFF;} /* show Link facing up if he's walking up OR if he was walking up but is now idle (we'll need to tell whether Link's moving or not - which we'll do in the JS */ #link { line-height: 4; } /* facing down (S key) is the default direction; the other 3 override this */ #link[data-key-w="true"], #link[data-moving="false"][data-lastdirection="w"] { line-height: 1; } #link[data-key-a="true"], #link[data-moving="false"][data-lastdirection="a"] { text-align: left; } #link[data-key-d="true"], #link[data-moving="false"][data-lastdirection="d"] { text-align: right; } #sword { width: 25px; height: 25px; top: 0; left: 0; position: absolute; outline: 1px solid #ddd; } /* make sword attack downward if Link's walking down OR if he was walking down but is now idle */ .attack #sword, .attack[data-moving="false"][data-lastdirection="s"] #sword { top: 50px; left: 10px; width: 25px; height: 50px; } /* again, default position = down */ .attack[data-key-w="true"] #sword, .attack[data-moving="false"][data-lastdirection="w"] #sword { top: -50px; left: 10px; width: 25px; height: 50px; } .attack[data-key-a="true"] #sword, .attack[data-moving="false"][data-lastdirection="a"] #sword { top: 10px; left: -50px; width: 50px; height: 25px; } /* order is important so sword follows where you're facing */ .attack[data-key-d="true"] #sword, .attack[data-moving="false"][data-lastdirection="d"] #sword { top: 10px; left: 50px; width: 50px; height: 25px; } .enemy{position:absolute; width:50px; height:50px; background:red;} .enemy.dead{background:#000;} </style> </head> <body> <div id="stage"> <div id="link" style="left:200px; top:200px;">Link<div id="sword"></div></div> <div class="block" style="width:126px; height:64px;left:303px; top:143px;"></div> <div class="block" style="width:96px; height:156px;left:79px; top:143px;"></div> <div class="enemy block" style="left:340px; top:250px;"></div> <a href="#" style="position:fixed; bottom:100px; left:0; width:200px; text-align:right; background:#333; padding:10px; color:#FFF;">BACK</a> </div> <script> var Link = document.getElementById("link"); var currentKeys = []; var UP = 'w'; var LEFT = 'a'; var DOWN = 's'; var RIGHT = 'd'; var ATTACK = 'Delete'; // ProTip: shift, spacebar and other function keys don't play nicely var canmoveleft=true; var canmoveright=true; var canmoveup=true; var canmovedown=true; var moving = false; var alltheObstacles = document.querySelectorAll('.block'); var Sword = document.getElementById("sword"); var attacking = false; var swordframecounter = 0; /////// End of Variables section, beginning of Functions section function isCrashing(Block1, Block2) { return !( Block1.bottom < Block2.top || Block1.top > Block2.bottom || Block1.right < Block2.left || Block1.left > Block2.right ); } function checkforcrashes() { var crashing = {}; var Me = Link.getBoundingClientRect(); Me.bottom = Me.top + Me.height; Me.right = Me.left + Me.width; var Blade = Sword.getBoundingClientRect(); Blade.bottom = Blade.top + Blade.height; Blade.right = Blade.left + Blade.width; Array.prototype.forEach.call(alltheObstacles, function(currentObstacle){ if( ! currentObstacle.classList.contains('block') ){ return; } // if the current block is an enemy I killed, it'll no longer have the 'block' class; I want to pretend it doesn't exist anymore. Disregard this block and skip to the next var Block = currentObstacle.getBoundingClientRect(); Block.bottom = Block.top + Block.height; Block.right = Block.left + Block.width; // 1) is Link crashing into anything? if (isCrashing(Me, Block)) { var top = Math.abs(Me.bottom - Block.top); var bottom = Math.abs(Me.top - Block.bottom); var left = Math.abs(Me.right - Block.left); var right = Math.abs(Me.left - Block.right); var shortestDistance = Math.min(top, bottom, left, right); shortestDistance === top ? (crashing.top = true) : shortestDistance === bottom ? (crashing.bottom = true) : shortestDistance === left ? (crashing.left = true) : (crashing.right = true); } // 2) is Link *attacking and *lodging his sword into *an enemy on *attack frame #2? if ( attacking && swordframecounter == 2 && currentObstacle.classList.contains("enemy") && isCrashing(Blade, Block) ) { currentObstacle.classList.add("dead"); //use CSS classes to show it dead currentObstacle.classList.remove("block"); //give the loop a way to ignore this dead block next time } }); // Okay we're done checking every block for collisions with Link and his sword, now handle the results: if (crashing.top) { canmovedown = false; } else { canmovedown = true; } if (crashing.bottom) { canmoveup = false; } else { canmoveup = true; } if (crashing.left) { canmoveright = false; } else { canmoveright = true; } if (crashing.right) { canmoveleft = false; } else { canmoveleft = true; } } // ends checkforcrashes() // we're now done checking for crashes and deciding if we need to stop moving in any direction(s) function gameLoop() { // 1) We'll need to know if Link's moving or not if ( currentKeys[LEFT] || currentKeys[RIGHT] || currentKeys[UP] || currentKeys[DOWN] ) { if (!moving) { Link.setAttribute("data-moving", true); moving = true; } } else { if (moving) { Link.setAttribute("data-moving", false); moving = false; } } var leftpos = parseInt(Link.style.left); var toppos = parseInt(Link.style.top); if (currentKeys[LEFT] && canmoveleft) { Link.style.left = leftpos - 3 + "px"; } if (currentKeys[RIGHT] && canmoveright) { Link.style.left = leftpos + 3 + "px"; } if (currentKeys[UP] && canmoveup) { Link.style.top = toppos - 3 + "px"; } if (currentKeys[DOWN] && canmovedown) { Link.style.top = toppos + 3 + "px"; } checkforcrashes(); // 2) control Link's attack if (currentKeys[ATTACK]) { attacking = true; } if (attacking) { swordframecounter++; if (swordframecounter == 1) { Link.classList.add("attack"); //CSS will move the sword forward } if (swordframecounter == 10) { Link.classList.remove("attack"); } if (swordframecounter == 20) { attacking = false; swordframecounter = 0; // reset & deactivate 'attacking' } } window.requestAnimationFrame(gameLoop); } // ends gameLoop() /////////// Event Listeners section: document.body.addEventListener("keydown", function(ev) { currentKeys[ev.key] = true; // record all the buttons, but.... if ([UP, LEFT, DOWN, RIGHT].indexOf(ev.key) < 0) { return; } // ..only do the rest of this function if you're pressing one of the WASD keys (things like spacebar will mess up attribute names) Link.setAttribute("data-key-" + ev.key, true); }); document.body.addEventListener("keyup", function(ev) { currentKeys[ev.key] = false; if ([UP, LEFT, DOWN, RIGHT].indexOf(ev.key) < 0) { return; } Link.setAttribute("data-key-" + ev.key, ""); // blank out the data-keys when you release the button Link.setAttribute("data-lastdirection", ev.key); // remember the last direction you walked so that Link's idle frame (and sword attack) faces the right way afterward }); window.addEventListener("load", function() { gameLoop(); }); </script></body></html>
It turns out the enemy is just a block with an additional class name which adds behavior that makes it disappear if you hit it with the sword.
For the sword, we're adding a div inside of Link's div so that it moves with him and makes the CSS positioning easier. When you hit the Delete key, the 'attack' state changes to true, which the game loop then notices and 1) changes Link's class (which could've easily been a data attribute instead) to make the CSS move the sword into place, then 2) checks if the sword is colliding with an enemy and if so, 3) changes the enemy's CSS to the killed state. We're also adding in some checks to record which way Link should be facing when either walking or idle so that the sword strikes in the right direction in every case.
With those feature done, now at last I can be satisfied the important pieces are there. Now at last I'll start filling in the graphics with CSS
CSS Graphics
Let's use CSS animation to handle Link's walk cycle. Step animation in CSS can be a little goofy so it helps to use the Inspect Element tools to get the right settings. You'll want a sprite sheet with each sprite evenly spaced apart. Link's divs will use that as a background image and the animation will change its background position.
I've doubled the size of the sprites - 200% Nearest Neighbor - to be readable on modern hi-def monitors. That makes the standard tiles 32x32, and standard tile sizes make CSS calculations simple. It's pretty cool how the restrictions the developers had to work within during NES days fit together so well with web design 30+ years later.
You may have noticed there's been no links to .gif or .png files in the code this whole time. I'm using inline data-URIs to keep everything you need in one HTML file. Although that means some really long strings of characters, the tradeoff is all you have to do is copy this one batch of code and paste it into a blank html file and you've got the whole example and it'll run anywhere.
Let's add the background images:
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Step 5 - Graphics</title> <style type="text/css"> * { margin: 0; padding: 0; } .block { position: absolute; width: 32px; height: 32px; background: url( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAeUlEQVRYhe3OuQ3AIBBE0S2FMijGZbkIiqMESzjwERghFnGN0QSTbLD/ibE+GOvDth9DJ+KuTQc8h1GQNwwH6A2JwrCA1pBkGB5QC8mGfwP4QrQjgAACCCBgPYD64T01BB5QGi6GwAJqw2oIHKB1OAuBAfQOJyGzASfqIyA4zUQpUAAAAABJRU5ErkJggg==") repeat 0 0; } body { background: url( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAOUlEQVQ4jWPQn/r/v/7U//8ZTT+ShBkY1kIwxQaQqnE4GoBgkIspNYCQEwlFM+0NIBiYw8CAAU8HACLnAQ/QMJgdAAAAAElFTkSuQmCC") repeat center 0; } #stage { background: url( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATElEQVRYhe3OoREAIAwEwS8lZVAMZVFmSmAmCAYE+MScOPPmV9Y8rHn0MVM7vyoHvENWknYAAAAAAAAAAAAAAAAAAKAc8A1J3d9qwAJDQcg59Rgs0wAAAABJRU5ErkJggg==") repeat 15px 15px; box-sizing: border-box; border: 1px solid transparent; border-image-source: url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAC/0lEQVR4nO3ayXHcMBBA0QlFYSgYh+UgFJxDcJV8ou2CDREbSTTwDv8wnI3ofke+Xq+Pz9fr4/Pt/YdU3OHmBZBa+g0ovfD0jWnuUicAqaosoNwHpLf3vAuAVFQxoLMvaK/OHACkL2sGVPoDWrPSvQOkrr2fAqr9QcWuds8AqWvPxYBa/0Axat0rQOraazWg3j/UXPXuEaDNewzQqBvQM43aG0CbNg2g0Tekaxu9J4A2a1pAV92g5t4LQJsUBlB6w5qjy/YM0B6FB/Tt+0/dGEAC6G84Vx9Ez8wdoMVaFlB6/elBr1Y6V4DUNO/j9bKAcgfWGDhHAKlqzun15QGdDUBtcI4AUtF8c+9vA6h0IKqbE0DqmtN2gGoHtFu1cwFogqXNFECN31cbAIAmWNpMAdQJ6OnHI6I9jgEQQACNAHTXIGZp1LkBAgigkYDS608v+qp5jQIAEEAAXQFoNUi5cwAEEEAzA1o9gAACCCCAAAoaQAABBBBAAAUNIIAAAggggIIGEEAAAQQQQEEDCCCAAAIIoKABBBBAAAEEUNAAAggggAACKGgAAQQQQAABFDSAAAIIIIAAChpAAAEEEEAABQ0ggAACCCCAggYQQAABBBBAQQMIIIAAAgigoAEEEEAAAQRQ0AACCCCAAAIoaAABBBBAAC0L6OrB3FXuHAABBNCMgFaBcwYJIIAAmgnQqnCuOjdAAAE0AlD6O7uUnh8ggAB6EtDuAQQQQHcAuvrAUaudC0ATLG2mABo8oF0rnRNA6prTNoDA6Ztv7n2AVDTf3PvLAwJn7JzT6wCpas7p9WUBgXPtvI/XAKlp3sfr5QCBc093zx2gxVoOUHogxXgcBKDNWw6Qng0g7QHo6hvWXHsBaJOmBwROjEbvCaDNmg4QODEbtTeANu1xQOCsUe8eAdq82wGBs2atewVIXXstBgTOHtXuGSB17fkUEDh7Vrp3gNS19ywgcFTiACB9WTUgcPS/ci4AUlGngMBRSakTgFTVP4DAUUt/HtgHSA0dbn4BLg5lqw4e9n4AAAAASUVORK5CYII=); position: relative; border-image-slice: 48; border-image-width: 48; border-image-repeat: repeat; height: calc(100vh - 64px); width: calc(100vw - 64px); margin: 32px auto 0; min-width: 640px; min-height: 512px; } #link { position: absolute; width: 32px; height: 32px; z-index: 3; } #sword { width: 0; height: 0; top: 0; left: 0; position: absolute; z-index: 4; } #link, #sword { background: url( "data:image/png;base64,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") 0 0 no-repeat transparent; } /* STAND, WALK (walking, standing or attacking, the order is SWAD: down(default starting pos) up then left right) */ @keyframes walkdown { from { background-position: 0 0; } to { background-position: 0 -72px; } } @keyframes walkup { from { background-position: -99px 0; } to { background-position: -99px -72px; } } @keyframes walkleft { from { background-position: -48px 0; } to { background-position: -48px -68px; } } @keyframes walkright { from { background-position: -144px 0; } to { background-position: -140px -68px; } } #link[data-key-s="true"] { animation: walkdown 0.2s steps(2) infinite; } #link[data-moving="false"][data-lastdirection="s"] { background-position: 0 0; } #link[data-key-w="true"] { animation: walkup 0.2s steps(2) infinite; } #link[data-moving="false"][data-lastdirection="w"] { background-position: -99px 0; } #link[data-key-a="true"] { animation: walkleft 0.2s steps(2) infinite; } #link[data-moving="false"][data-lastdirection="a"] { background-position: -46px 0; } #link[data-key-d="true"] { animation: walkright 0.2s steps(2) infinite; } #link[data-moving="false"][data-lastdirection="d"] { background-position: -144px -36px; } /* ATTACK */ #link.attack, #link.attack[data-moving="false"][data-lastdirection="s"] { background-position: 0px -73px; height: 32px; } #link.attack[data-key-w="true"], #link.attack[data-moving="false"][data-lastdirection="w"] { background-position: -94px -74px; height: 36px; } #link.attack[data-key-a="true"], #link.attack[data-moving="false"][data-lastdirection="a"] { background-position: -48px -74px; height: 32px; } #link.attack[data-key-d="true"], #link.attack[data-moving="false"][data-lastdirection="d"] { background-position: -140px -74px; height: 32px; } .attack #sword, .attack[data-moving="false"][data-lastdirection="s"] #sword { width: 16px; height: 32px; top: 30px; left: 11px; background-position: -24px -126px; } .attack[data-key-w="true"] #sword, .attack[data-moving="false"][data-lastdirection="w"] #sword { width: 16px; height: 32px; top: -32px; left: 3px; background-position: -42px -126px; } .attack[data-key-a="true"] #sword, .attack[data-moving="false"][data-lastdirection="a"] #sword { top: 10px; left: -30px; width: 32px; height: 16px; background-position: -64px -124px; } .attack[data-key-d="true"] #sword, .attack[data-moving="false"][data-lastdirection="d"] #sword { top: 10px; left: 29px; width: 32px; height: 16px; background-position: -64px -142px; } /* ENEMIES */ .enemy { position: absolute; width: 32px; height: 32px; z-index: 2; background: url( "data:image/png;base64,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") 0 0 no-repeat transparent; } .enemy.dead { background: #000; } </style> </head> <body> <div id="stage"> <div id="link" style="left:200px; top:200px;">Link<div id="sword"></div></div> <div class="block" style="width:126px; height:64px;left:303px; top:143px;"></div> <div class="block" style="width:96px; height:156px;left:79px; top:143px;"></div> <div class="enemy block" style="left:340px; top:250px;"></div> <a href="#" style="position:fixed; bottom:100px; left:0; width:200px; text-align:right; background:#333; padding:10px; color:#FFF;">BACK</a> </div> <script> var Link = document.getElementById("link"); var currentKeys = []; var UP = 'w'; var LEFT = 'a'; var DOWN = 's'; var RIGHT = 'd'; var ATTACK = 'Delete'; // ProTip: shift, spacebar and other function keys don't play nicely var canmoveleft=true; var canmoveright=true; var canmoveup=true; var canmovedown=true; var moving = false; var alltheObstacles = document.querySelectorAll('.block'); var Sword = document.getElementById("sword"); var attacking = false; var swordframecounter = 0; /////// End of Variables section, beginning of Functions section function isCrashing(Block1, Block2) { return !( Block1.bottom < Block2.top || Block1.top > Block2.bottom || Block1.right < Block2.left || Block1.left > Block2.right ); } function checkforcrashes() { var crashing = {}; var Me = Link.getBoundingClientRect(); Me.bottom = Me.top + Me.height; Me.right = Me.left + Me.width; var Blade = Sword.getBoundingClientRect(); Blade.bottom = Blade.top + Blade.height; Blade.right = Blade.left + Blade.width; Array.prototype.forEach.call(alltheObstacles, function(currentObstacle){ if( ! currentObstacle.classList.contains('block') ){ return; } // if the current block is an enemy I killed, it'll no longer have the 'block' class; I want to pretend it doesn't exist anymore. Disregard this block and skip to the next var Block = currentObstacle.getBoundingClientRect(); Block.bottom = Block.top + Block.height; Block.right = Block.left + Block.width; // 1) is Link crashing into anything? if (isCrashing(Me, Block)) { var top = Math.abs(Me.bottom - Block.top); var bottom = Math.abs(Me.top - Block.bottom); var left = Math.abs(Me.right - Block.left); var right = Math.abs(Me.left - Block.right); var shortestDistance = Math.min(top, bottom, left, right); shortestDistance === top ? (crashing.top = true) : shortestDistance === bottom ? (crashing.bottom = true) : shortestDistance === left ? (crashing.left = true) : (crashing.right = true); } // 2) is Link *attacking and *lodging his sword into *an enemy on *attack frame #2? if ( attacking && swordframecounter == 2 && currentObstacle.classList.contains("enemy") && isCrashing(Blade, Block) ) { currentObstacle.classList.add("dead"); //use CSS classes to show it dead currentObstacle.classList.remove("block"); //give the loop a way to ignore this dead block next time } }); // Okay we're done checking every block for collisions with Link and his sword, now handle the results: if (crashing.top) { canmovedown = false; } else { canmovedown = true; } if (crashing.bottom) { canmoveup = false; } else { canmoveup = true; } if (crashing.left) { canmoveright = false; } else { canmoveright = true; } if (crashing.right) { canmoveleft = false; } else { canmoveleft = true; } } // ends checkforcrashes() // we're now done checking for crashes and deciding if we need to stop moving in any direction(s) function gameLoop() { // 1) We'll need to know if Link's moving or not if ( currentKeys[LEFT] || currentKeys[RIGHT] || currentKeys[UP] || currentKeys[DOWN] ) { if (!moving) { Link.setAttribute("data-moving", true); moving = true; } } else { if (moving) { Link.setAttribute("data-moving", false); moving = false; } } var leftpos = parseInt(Link.style.left); var toppos = parseInt(Link.style.top); if (currentKeys[LEFT] && canmoveleft) { Link.style.left = leftpos - 3 + "px"; } if (currentKeys[RIGHT] && canmoveright) { Link.style.left = leftpos + 3 + "px"; } if (currentKeys[UP] && canmoveup) { Link.style.top = toppos - 3 + "px"; } if (currentKeys[DOWN] && canmovedown) { Link.style.top = toppos + 3 + "px"; } checkforcrashes(); // 2) control Link's attack if (currentKeys[ATTACK]) { attacking = true; } if (attacking) { swordframecounter++; if (swordframecounter == 1) { Link.classList.add("attack"); //CSS will move the sword forward } if (swordframecounter == 10) { Link.classList.remove("attack"); } if (swordframecounter == 20) { attacking = false; swordframecounter = 0; // reset & deactivate 'attacking' } } window.requestAnimationFrame(gameLoop); } // ends gameLoop() /////////// Event Listeners section: document.body.addEventListener("keydown", function(ev) { currentKeys[ev.key] = true; // record all the buttons, but.... if ([UP, LEFT, DOWN, RIGHT].indexOf(ev.key) < 0) { return; } // ..only do the rest of this function if you're pressing one of the WASD keys (things like spacebar will mess up attribute names) Link.setAttribute("data-key-" + ev.key, true); }); document.body.addEventListener("keyup", function(ev) { currentKeys[ev.key] = false; if ([UP, LEFT, DOWN, RIGHT].indexOf(ev.key) < 0) { return; } Link.setAttribute("data-key-" + ev.key, ""); // blank out the data-keys when you release the button Link.setAttribute("data-lastdirection", ev.key); // remember the last direction you walked so that Link's idle frame (and sword attack) faces the right way afterward }); window.addEventListener("load", function() { gameLoop(); }); </script></body></html>
By now this may be looking like a lot to sort through so I'll stop things here. But keep this in mind: I spent a bunch of time thinking about little parts and then adding them in to my main formula. Like writing a term paper, it doesn't all just come together. It came as bits and pieces to me and it should for you, so that you get a chance to slowly build a train of thought, get the dopamine hits from seeing each new part of your logic work correctly on the screen, and form an addiction to your project by adding "ooh, just one more thing.." (which you can see I did quite a bit of with this page) and going back to switch out old ideas with better ones as inspiration strikes.
This was all very flow-of-thought code, but hopefully that helps make it easy for the most possible people to follow. For those of you inspired by the possibilities, rip apart this page's final code and start adjusting things. Switch the graphics, see what changing this does, see what deleting that does, and go build something new with it. I'll leave you with some ideas of where you can take things:
- Accumulated damage - on this page one hit kills you or an enemy. But you can add something like data-life=3 to an enemy and reduce it by 1 every time they're hit. You can even add a fixed-position div at the top of the page that shows Link's heart icons, and match them to his data-life number. If the player dies, they have to reload the page (and thus your ads, if you have any)
- With a few more additions to the sprite sheet, you can have an end boss with a huge data-life and give him a repeating attack pattern.
- Projectiles. Beware, having too many projectiles running at a time can clog up a browser fast. Be sure to stop them from running and remove them once they leave the screen or go too far.
- You have no idea how hard it's been on me to follow internet courtesy and not play a repeating midi of the level music or attach sound effects to the attack key. It would be so easy.
- Isometric RPGs. If you're *really* into it, you can go all Final Fantasy and give a player stats and levels in their cookies or local storage, which can be saved for their next visit to your website. Loyal visitors get more points or unique stuff when they come for your next article.
- Hidden warp doors to pages with hidden content.
Man what on earth would that look like? - Simply switch out the graphics to other overhead games.
- If you want more general appeal, you could turn this into a PacMan game and maybe make the ghosts a bunch of links to your home, about, portfolio and contact. The more simple you get, the more possibilities you have.
Designers, developers: Don't make yourselves replaceable by following trends. Try new pet projects and break as many conventions as you can. Use fixed-width pages. Use absolute positioning. Heck, use the marquee tag. You know, the tag was made obsolete by CSS animations which can do the exact same thing, just with more lines of complex code (check out my home page on mobile if you think marquees have no ideal use cases anymore and notice the consistent speed of inconsistent title lengths).
Try working inside the more liberating confines of "is it good enough" instead of "is it perfect". Learn The Pareto principle that everyone in software seems to be forgetting about.
Above all, be nuts. Do new things. Play around with the easy stuff. Make something you like and bring back that pioneering age of the personal web pages.
This whole website is very impressive. I love it.
Man, this tutorial will certainly assist me in programing that one project I really was hoping to do but never had the time (or know-how) to do so. I certainly will look into spending the next few weeks creating something like this now…
i really like this site.
I used to frequent this site all the time, and randomly got the urge to come back today. This post in particular makes me very glad I did.
I took a class on web development and design a few years back, and a page like this is exactly what I wanted to do for my final project. But by the end I still had no clue how to go about it. Thanks for such a fun lesson!
This post is amazing, I share the same opinion!
I will try to implement this on my website !!
And the Konami code on the site is really cool!