Online Music Hackathon

First Prize: $5000

Ends on Jul 22:
00
Days
00
Hours
00
Mins
00
Secs

Learn to Code via Tutorials on Repl.it

Posts
HTML, CSS, JS
HTML, CSS, JS
6
🎮 Making 3D Babylon Scenes more Dynamic with Actions
# 🎮 Making 3D Babylon Scenes more Dynamic with Actions In the [last tutorial](https://repl.it/talk/learn/3D-Games-with-BabylonJS-A-Starter-Guide/15957), I showed you guys how to make a basic Babylon scene, with lights, cameras, player controls, gravity, collision and other stuff. This time, I'm going to show you how to make scenes more dynamic! If you want to continue where I left off, make sure you fork my repl from the last tutorial! ![image](https://storage.googleapis.com/replit/images/1561234058959_068dab6fa178259c011429903c03c093.png) *Image depics the logo of BabylonJS* ### Prerequisites - Intermediate JavaScript knowledge ## Actions To create things that are more dynamic in BabylonJS, you can use things called "Actions". To put it simply, Actions let you execute some function when an event occurs. For example, you can log some output to the console if you look at a Sphere. Before creating an Action, you must create an `ActionManager`. This lets you attach multiple actions to the same object. In this case, we are adding an `ActionManager` to the `scene` ```js scene.actionManager = new BABYLON.ActionManager(scene); ``` Now since we have our `ActionManager` all set up, we can create some Actions. To start off simply, we're going to create an "action" that tells the user when they have pressed space. ```js let myAction = new BABYLON.ExecuteCodeAction( BABYLON.ActionManager.OnKeyUpTrigger, function(event) { if(event.sourceEvent.key === " ") { console.log("You have pressed space"); } } ) gameScene.actionManager.registerAction(myAction); ``` - We're using the [`BABYLON.ExecuteCodeAction()`](https://doc.babylonjs.com/api/classes/babylon.executecodeaction) constructor to create the "Action". - The first argument of the constructor is a "trigger". A trigger will *cause* the action to execute. In this case, the trigger is "pressing a button on the keyboard" - The second argument to the constructor is the function we want to execute. In this case, we create a function that logs some output to the console, but only if the user pressed space. Cool! So when you press space, it logs to the console. If you want to see it in action, check out [this repl]( [repl](https://repl.it/@eankeen/babylon-animation-tutorial-1)). ![image](https://storage.googleapis.com/replit/images/1561681774645_22c419deb727de54a4c779804a2cc53c.png) But logging some text to the console is pretty boring. Wouldn't it be cooler if it spawned in some spheres? Here I simply call the `createSphere` function instead of logging output to the console. ```js let action = new BABYLON.ExecuteCodeAction( BABYLON.ActionManager.OnKeyUpTrigger, function(event) { if(event.sourceEvent.key === " ") { createSphere(scene); } } ) scene.actionManager.registerAction(action); }; ``` Inside of the `createSphere` function, I create a Sphere Mesh. Like in the prevoius tutorial, I create a physics imposter and set collisions to `true`, so it collides with the ground and other spheres. ```js function createSphere(scene) { // Create Sphere Mesh let mySphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 1 }, scene); // Give the sphere mesh a height so it spawns above the ground mySphere.position.y = 2; // Create a Pysics Imposter around the Sphere, and enable collisions so it interacts with other parts of the scene let mySpherePhysicsImposter = new BABYLON.PhysicsImpostor( mySphere, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 0.01, friction: 0.1, restitution: .85 }, scene ); mySphere.collisionsEnabled = true; mySphere.checkCollisions = true; } ``` As you can see in the gif below, it works! 😄 ![gif](https://i.imgur.com/Avt2Yhj.gif) If the spheres are not acting the way you want them to, you might want to change the `mass`, `friction`, or `restitution` of the spheres. You may also want to make gravity stronger. See it working in [this repl](https://repl.it/@eankeen/babylon-animation-tutorial-2)! ## Changing a single Mesh Besides creating new meshes, you can also modify the properties of existing meshes! For example, I could use Actions to change the color of a Mesh whenever I look at it. Let's do exactly that. First, let's create a Cylinder Mesh. ```js let cone = createCone(scene); ``` The `createCone` function looks something like this ```js function createCone(scene) { // Create the cylinder let cone = new BABYLON.MeshBuilder.CreateCylinder("cone", { height: 5, diameterBottom: 3, diameterTop: 0 }); cone.position.x = -3; cone.position.z = -3; cone.position.y = 1; // Create a physics imposter and make sure collisiosn are enabled (so the spheres bounce off of it) new BABYLON.PhysicsImpostor( cone, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 1000, friction: 3, restitution: .7 }, scene ); cone.collisionsEnabled = true; cone.checkCollisions = true; let coneMaterial = new BABYLON.StandardMaterial(scene); coneMaterial.diffuseColor = new BABYLON.Color3(105 / 256, 256 / 256, 124 / 256); // green cone.material = coneMaterial; return cone; } ``` The function looks pretty similar to what we went over in the [first tutorial](https://repl.it/talk/learn/3D-Games-with-BabylonJS-A-Starter-Guide/15957). The difference is at the bottom - we're changing the color of the cone. We do this by creating a *new* [(Standard) material](https://doc.babylonjs.com/api/classes/babylon.standardmaterial). To actually change the color, we set the [diffuseColor](https://doc.babylonjs.com/api/classes/babylon.standardmaterial#diffusecolor) property of the material to a color. Then, we assign that material to our `cone`. OK, so we created our cone. Let's make it change color when we hover over it. First, create another action manager. This time, we create an `Actionmanager` for the `cone` rather than the `scene`. ```js cone.actionManager = new BABYLON.ActionManager(scene); let coneAction = new BABYLON.ExecuteCodeAction( BABYLON.ActionManager.OnPointerOverTrigger, function(event) { changeConeColor(cone); } ); cone.actionManager.registerAction(coneAction); ``` The `changeConeColor` function will trigger when the mouse has hovered over the cone. If you want to see all of Babylon's triggers, you can check their [documentation](https://doc.babylonjs.com/how_to/how_to_use_actions#triggers)! Inside `changeConeColor`, I just created random *red*, *green*, and *blue* numbers and created a color using Babylon's `BABYLON.Color3` constructor. I then set the `cone`'s material to that color (`diffuseColor`). ```js function changeConeColor(cone) { let r = Math.floor(Math.random() * 256) + 1; let g = Math.floor(Math.random() * 256) + 1; let b = Math.floor(Math.random() * 256) + 1; cone.material.diffuseColor = new BABYLON.Color3(r / 256, g / 256, b / 256); } ``` And there you have it! There is a lot more movement in our scene now! If you wish, you can check this repl](https://repl.it/@eankeen/babylon-animation-tutorial-3) to take a look! ![gif](https://i.imgur.com/hJI4UNR.gif) I hope you found this tutorial useful, please let me know!
4
posted by eankeen (528) 19 days ago
115
Making a Phaser Game with HTML5 and JavaScript
# Making a Phaser Game with HTML5 and JavaScript Hi guys! Everybody likes to make games, right? And everybody has their own way of doing it. In this tutorial I wanted to share a very simple, yet effective way to make games in your browser! It should be easy enough for most people with javascript knowledge to follow along and, if you want to investigate further, there are endless possibilities out there! ![image](https://storage.googleapis.com/replit/images/1539468462288_75406edb94f04ca097cc5b4705ccbc85.pn) ### Phaser As Phaser describes itself it is a fast, free and fun open source framework for Canvas and WebGL powered browser games. And it really is! It is super simple to use and is quite easy to set up. No super extensive javascript knowledge is necessary and the process of making games is fun and rewarding. It also comes with tons of extra features that you may need in some more complicated games so while it caters to starters as well, it also does not lack depth if you want to look further. Anything from image editing to complex game mechanic mathematics is possible. ##### Sites to use The official Phaser website is [here](https://phaser.io). Additionally, because we are going to use Phaser 3, the latest release, the examples on the site will most probably not work for v3. If you want some examples of v3 features the link is [here](https://labs.phaser.io/). You should not need the examples during this tutorial but if you want to learn further that is where you start. Google works as well but be careful about which version is being discussed. Version 3 is relatively new and v2 has loads more documentation and examples and tutorials on it. However, I would recommend learning v3 because it is generally better in many ways and the knowledge will last you longer and it will be more current. #### Prerequisites (what you need before doing this tutorial) The pre-requisites are: * A basic understanding of HTML, CSS and Javascript. * Knowledge in Javascript about the `this` keyword. * Some time and patience. * 3 rolls of duct tape. * Lots of cardboard * Creativity ### Let's Get Started! The repl.it project that I will be using for this tutorial is [here](https://repl.it/@kaldisberzins/Phaser-Tutorial) and the website for it if you just wanna play the game is [here](https://phaser-tutorial.kaldisberzins.repl.co/). If you ever get stumped on a step that I take in this tutorial just check the repl and see how the code looks in it. If all else fails a bit of copy-paste will solve your issues. Make a new HTML/CSS/JS repl and follow along... So, first of all we need to include the Phaser script into our website. The only piece of HTML in this tutorial will be the following:`<script src="//cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script>`Just paste this into your project's HTML file right above your script tag that links to `script.js`. The order is important and if you get it wrong nothing will work. If your project is not working you should definitely have a look at the order of your scripts. The Phaser script should be first. With that out of the way, let's get into making our game! The first bit of code is a standard template that is in most simple Phaser games (more advanced ones may use a slightly different structure but the idea is the same). The code looks like this: ```javascript let config = { type: Phaser.AUTO, width: 800, height: 500, physics: { default: 'arcade', arcade: { debug: false } }, scene: { preload: preload, create: create, update: update } }; const game = new Phaser.Game(config); function preload(){ } function create(){ } function update(){ } ``` While this may look alien to you, don't stress. To follow along this tutorial you don't need to understand what everything does exactly. The main things you should pay attention to are: * The three functions at the bottom `preload`, `create` and `update`. These we will fill in with the game's code. * The `width` and `height` properties. You can set these to anything you like, I did not make it `window.innerWidth` and `window.innerHeight` because scaling can quickly become messy. It is easier to make it a fixed width for everybody. So now if you run your repl you should see a black square in your browser window. Success! If you do not, make sure you have the Phaser script in the right place and that you have the code in your `script.js` exactly like above. You should also get a message in the console, something like: ```%c %c %c %c %c Phaser v3.14.0 (WebGL | Web Audio) %c https://phaser.io background: #ff0000 background: #ffff00 background: #00ff00 background: #00ffff color: #ffffff; background: #000000 background: #fff``` This may look awful in the repl.it console but if you open it in a new tab and check the console it should be a colorful banner. ### Loading Assets The `preload` function that we are going to use for this section is where you load your assets. If you want some images or audio (Phaser does that as well) in your game you first have to load it here. This way you are loading all the required assets immediately and you can use them throughout the game. I have made some assets for this tutorial so that you do not need to find or make some yourself. Go [here](https://drive.google.com/drive/folders/1TzRicUBL8V0T_9fPMaNCL6M0UEV51irQ?usp=sharing) and click download like so to get the files: ![image](https://storage.googleapis.com/replit/images/1539468612344_eeb16f0a94e74fa401749d11f7b89333.pn) If you get the files in a `.zip` folder just unzip them and drop them into your repl. Once you have them in your repl we have to load them into our game. The following code in the `preload` function will do the trick: ```javascript this.load.atlas("player", "spritesheet.png", "sprites.json"); this.load.image("platform", "platform.png"); this.load.image("spike", "spike.png"); this.load.image("coin", "coin.png"); ``` The first parameter in all of the functions is the "key" for the image. This key you would use when you need to add the image into the game. You can put it as whatever you want but make sure it is descriptive of the image in some way. I suggest you keep them the same as mine so that later code in my tutorial works for you. The second parameter is the path to the image. Because I put the assets in the same folder as the html and js files the path is just the name of the image. If you put your assets in another folder the file path string would look like `"folder_name/file_name.png"`. You may also have noticed that the first command is a bit different. It loads an __atlas__ and not an image. An atlas is a collection of images put together to make a larger image accompanied by a file that states where all the smaller images are. If you open the file `sprites.json` in the assets I gave you you should see that it contains a bunch of named objects that have x, y, width and height properties among others. Each object is an image inside the larger image. In this tutorial we will use the atlas for the player animations. All of the frames for the player (in our case only three) are in the `spritesheet.png` file. The third parameter for the atlas is the path to the `.json` file which we looked at already. If you now run the current code the screen should remain black and no errors should be in the console. If you see a web audio warning that is fine, it does not mean anything important. It's just chrome messing with you. ### Adding Objects to Our Game The `create` function is where the building of our game happens. It is run right after `preload` and is run only once. If you want to add an object to the game, this is where you do it. If you want to repeatedly create some object. Make a function (read below) that creates the object and run that as may times as you like. So we now have loaded some images but we need to have something happen on the screen. Let's add a function in the `create` function that will spawn our player in. Add this code to the `create`function: ```javascript this.spawnPlayer = ()=>{ this.player = this.physics.add.sprite(400, 250, "player", "sprite_0"); } this.spawnPlayer(); ``` I put this in a seperate function so that we can spawn the player multiple times. We are saving the player to __`this`__ which is the Phaser game object so that we can access it from anywhere. The function itself creates a sprite (image/object) that is in the Phaser physics system. The parameters are: 1. X position 2. Y position 3. Image key 4. (optional) If the image is an atlas, which frame in the atlas. There may be a few more parameters but those are not important for this tutorial. The way we find out which frame is which in the atlas is by looking at the `sprites.json` file. Find an object and look at its x and y properties. For example `sprite_2` has the following object: ```javascript "sprite_2":{"frame":{"x":0,"y":0,"w":48,"h":64}... ``` We can see that the x and y coordinates of the frame are `0, 0`. This means that it will be in the top left corner. If you look at the top left corner of the `spritesheet.png` image you will see which frame is `sprite_2`. Try changing the last parameter in the add function to be `sprite_2`. You will see that it has changed. ##### Adding a Background If the only background color we could have would be black Phaser would look really bad. Luckily enough, Phaser has an easy way to add a background to our game. Just add this code to the top of your `create` function above the `spawnPlayer` function: ```javascript this.cameras.main.setBackgroundColor('#ffffff'); ``` This sets the background color for our main camera to white. If you have not used hex color codes before don't worry about it, just know that `#ffffff` is white. The only problem with that is that now we can't see where our canvas window starts and ends. We can fix this with a little CSS: ```css canvas{ border: 1px solid black; } ``` Now if you run your code it should look something like this: ![image](https://storage.googleapis.com/replit/images/1539468654218_bf2de4d9dc55069fde105a14f6c9818a.pn) You can see we have our little character in the middle of the screen. The background is now white. You may have noticed that the character is not offset to a side even though we put in the coordinates for the center of the screen. This is because Phaser draws images from their center. This makes it easier to center images. Another simple thing we can add to the game is a camera that follows the player. This is quite easy to do in Phaser: ```javascript this.spawnPlayer = ()=>{ this.player = this.physics.add.sprite(400, 250, "player", "sprite_0"); this.cameras.main.startFollow(this.player); }; this.spawnPlayer(); ``` The function should be quite self-explanatory and if you run it you should see no change for now. As long as you do not get any errors you are fine. ### Adding Platforms Before we start I wanted to show you the most basic way to add an image to the game. The method used above has a very specific use case (only for sprites). Here is a more general use way of doing it: ```javascript // This goes beneath the spawnPlayer function call this.platform = this.add.image(404, 302, "platform"); ``` This is good for some simple use cases like for example a logo image in your title screen. However it has its shortcomings. Imagine you want to create a map of these platforms. You would have to add `platform1` `platform2` and so on... It would be a nightmare. Let's not get started on collisions. So by now you can see why we are not going to use this to add our platforms. Instead we will have a group. Defining a new group is easy. Remove the above code and add this instead. ```javascript this.platforms = this.physics.add.staticGroup(); ``` Currently we are just defining a new static (non-moving) group and assigning it to the variable `this.platforms`. If you run this now the platform image will disappear. That is because we need to add some platforms to the group. This can be done simply like this: ```javascript //Below the spawnPlayer function this.platforms = this.physics.add.staticGroup(); this.platforms.create(404, 302, "platform"); ``` There we go! Now we have our platform back! But what is the benefit? In a moment when we deal with collisions you will see why. For now we will leave the platforms and get back to them later. ### Keyboard Inputs As you have probably gathered by now, Phaser has made its own version of everything you may need when developing games. Keyboard inputs are no exception. Phaser even supports many ways to do keyboard inputs. We are going to do the shortest and simplest. We are going to have a bunch of variables, one for each key. And we will check each frame if any of the keys are pressed and set velocities accordingly. The code for the keyboard variables in the `create` function looks like this: ```javascript this.key_W = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W); this.key_A = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A); this.key_D = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D); ``` You do not need to understand this, just get the idea of what is happening. When a player presses a key the variable associated with that key will have `isDown` set to `true`. This makes adding keybinds really easy. Now for the rest of this section we are using the `update` function. `update` is your game loop. This function is run very fast repeatedly all throughout your game. This is where you would handle things like movement and other stuff you would want to check every frame. If you would be coding your own physics this would be where you do it. In the `update` function now let's check if the W key is pressed: ```javascript if(this.key_W.isDown){ this.player.setVelocityY(-50); }else{ this.player.setVelocityY(0); } ``` Instead of incrementing or decrementing the Y property of the player we set its velocity. We do this because it sets a velocity within Phaser which has some benefits. First of all Phaser object velocities take into account the frame rate. If every frame you increase the position X of a player, the higher the frame rate the faster the player moves. However, Phaser counteracts this. We do not need to know how, just that no matter the frame rate the player will always move at the same speed. The value we put into `setVelocityY` is the amount of pixels we want it to move in one second. If you run this now you will see that is you press the W key your character will move up. Success! Now let's add keybinds for A and D. This is only a few more lines of code: ```javascript if(this.key_A.isDown){ this.player.setVelocityX(-50); }else if(this.key_D.isDown){ this.player.setVelocityX(50); }else{ this.player.setVelocityX(0); } ``` We have this in an if/else if statement because we don't want to head left and right at the same time. We can only go in one direction or the other. And that's it! We now have linked up our keyboard keys to our Phaser game! Now it's time to deal with physics. ### Game Physics Phaser also has its own physics engine. In fact it has three but we will only use the most basic one for this tutorial. Just simple square and square collisions. Before we can do collisions, how about we add some gravity. We only need it on the player so it would look like this: ```javascript this.spawnPlayer = ()=>{ this.player = this.physics.add.sprite(400, 250, "player", "sprite_0"); this.player.body.setGravityY(800); this.cameras.main.startFollow(this.player); }; ``` Now if you run your game you will see that the player drops. But he is dropping very slowly. Why so? This is because each frame we are setting his velocity to 0 if the W key is not pressed. Previously that was needed so that he would not just fly away but now we need to remove that bit: ```javascript //In the update function if(this.key_W.isDown){ this.player.setVelocityY(-50); }/*else{ this.player.setVelocityY(0); } NO LONGER NEEDED*/ ``` Now if you run it the player falls a bit faster. You can still fly with W but we will change that in a second. #### Collisions Now that we have gotten gravity mostly out of the way let's make the player collide with the platform that we have. We can do this with one simple line of code. Add this to your `spawnPlayer` function: ```javascript this.physics.add.collider(this.player, this.platforms); ``` That's it. Just one line of code does everything. But if you run this now it will not work. The player will fall right through. And this is actually for a really stupid reason. We are running this code before we add the platforms. All you have to do is move the `spawnPlayer` function __call__ (not the function itself) below where we add the platforms. And Viola! We have the player not falling through the platform. There are some small problems that we should address before moving on. First of all, When we press W we can fly endlessly. That defeats the point of the game. To prevent this all we need to do is to only let us jump when we are on the ground. This is easy to do: ```javascript if(this.key_W.isDown && this.player.body.touching.down)... ``` When the key W is down and the player's body is touching a platform with its bottom it will jump. If you run this now you will see that the player now makes many little jumps if you press W. To make the jumps larger we have to increase the `setVelocitY`: ```javascript if(this.key_W.isDown && this.player.body.touching.down){ this.player.setVelocityY(-550); } ``` And also while we are at it we can make the left/right movement a bit faster: ```javascript if(this.key_A.isDown){ this.player.setVelocityX(-150); }else if(this.key_D.isDown){ this.player.setVelocityX(150); }else{ this.player.setVelocityX(0); } ``` So there we have it! A running and jumping player! Now let's give him a map to run around in. #### Map Building Phaser supports multiple ways to build a map (of course). However, I have decided that it would be better to cook up our own map builder that would work off of a string. Spaces would indicate that at that position there is no platform, 1 would mean that there is, 2 that this is a spawn point for the player and a dot(.) would mean that this is the end of a row. The map I designed looks something like this: ```javascript //At the top of your js file const map = '11111111111111111111111111.'+ '1 1.'+ '1 1.'+ '1 2 1 1 1 1 1.'+ '1 1 1 1 1 1.'+ '1 1.'+ '1 1.'+ '1 1 1 1 1 1.'+ '1 1 1 1 1 1.'+ '1 1.'+ '1 1.'+ '1 1 1 1 1 1.'+ '1 1 1 1 1 1.'+ '1 1.'+ '1 1.'+ '11111111111111111111111111'; ``` You can see that it is a box that is riddled with platforms. How do we turn this into a map? The parser for this that I made is only a few lines of code: ```javascript //Goes instead of the previous platform adding code this.platforms = this.physics.add.staticGroup(); let mapArr = map.split('.'); let drawX = 0; let drawY = 0; mapArr.forEach(row=>{ drawX = 0; for(let i = 0; i<row.length; i++){ if(row.charAt(i)==='1'){ this.platforms.create(drawX, drawY, "platform"); }else if(row.charAt(i)==='2'){ if(row.charAt(i+1)==='1'){ this.spawnPlayer(drawX-4, drawY-12); }else if(row.charAt(i-1)==='1'){ this.spawnPlayer(drawX+4, drawY-12); }else{ this.spawnPlayer(drawX, drawY-12); } } drawX+=40; } drawY+=40; }); ``` First we split the string that we have into an array of rows using the . that says that a row ends at that point. Then we loop through each row and at each row we loop through each character in the row. If the character is a 1, we add a platform at that place. If the character is 2 we spawn the player. I have a bit more code there that checks if there is a platform to the left or right and that nudges the character to a side just so that the player does not spawn in a platform. Also, you may have noticed that we are calling `spawnPlayer` here with some parameters. These are just x and y coordinates of where to spawn. To make that work we just have to edit the `spawnPlayer` function like so: ```javascript this.spawnPlayer = (x, y)=>{ this.player = this.physics.add.sprite(x, y, "player", "sprite_0"); this.player.body.setGravityY(800); this.cameras.main.startFollow(this.player); }; ``` Now if you run this you should get a map inside of which the player can run around. You can mess around with the map string if you want and design your own map. I would love to see what you come up with in the comments! ### Player Animations A while ago, I mentioned that we would use the atlas for player animations. Now is the time! We have three frames in our atlas and we have only used one. It's time to use the other two. Phaser has its own animation manager (by now you get the idea - Phaser === everything) that makes it super simple to do animations. First we have to set up our animations: ```javascript // At the bottom of the create function this.anims.create({ key:"walk", frames:[{key:"player", frame:"sprite_2"}, {key:"player", frame:"sprite_1"}], frameRate:10, repeat:-1 }); this.anims.create({ key:"stand", frames:[{key:"player", frame:"sprite_0"}], frameRate:1 }); ``` This creates an animation for our player that we can play when we want. The array `frames` is what Phaser will loop though and play. `frameRate` is quite self explanatory - the amount of frames that are played each second. `repeat` with the value -1 will make the animation loop again and again. Not specifying `repeat` will just make it run once. The key is the string that we can use to reference to the animation later. Just the same way as with images. Now let's run the animations when we walk right or left: ```javascript //In the update function if(this.key_A.isDown){ this.player.setVelocityX(-200); this.player.anims.play("walk", true); }else if(this.key_D.isDown){ this.player.setVelocityX(200); this.player.anims.play("walk", true); }else{ this.player.anims.play("stand", true); this.player.setVelocityX(0); } ``` The `true` parameter is just whether if there is already an animation running, should Phaser continue it? If you set this to false you will see that it will just freeze on a frame. That is because every frame it is checking if a key is pressed and then playing the animation. It will start again every frame making it look like it is frozen. Now if you run this you will see that we have a running animation with the legs moving and the hat bobbing up and down. There is only one more problem with the sprite. The player does not flip when he runs to the left. This is an easy fix: ```javascript //In the update function if(this.key_A.isDown){ this.player.setVelocityX(-200); this.player.anims.play("walk", true); this.player.flipX = true; }else if(this.key_D.isDown){ this.player.setVelocityX(200); this.player.anims.play("walk", true); this.player.flipX = false; }else{ this.player.anims.play("stand", true); this.player.setVelocityX(0); } ``` There we go! Now we have a map, player animations, keybinds, physics and most of all - a weird blob of a character who has a hat that flaps in the breeze! ### The Final Step - Spikes and Coins Now let's add some spikes that the player has to dodge and some coins that the player can collect. First, let's add a score counter in the top of the screen that displays our score: ```javascript this.spawnPlayer = (x, y)=>{ this.player = this.physics.add.sprite(x, y, "player", "sprite_0"); this.player.body.setGravityY(800); this.physics.add.collider(this.player, this.platforms); this.cameras.main.startFollow(this.player); //====================================== this.player.score = 0; this.scoreText = this.add.text(0, 0, "Score: "+this.player.score, { fill:"#000000", fontSize:"20px", fontFamily:"Arial Black" }).setScrollFactor(0).setDepth(200); }; ``` `setScrollFactor(0)` will make sure that when our camera moves, the text does not. This way it will always be in the same position in the top-left of the screen. Text is drawn from its top-left (don't ask me why it is one way for one thing and another for another) so drawing it at `0, 0` will put in the top-left corner. `setDepth(200)` will make sure the text always appears on top. We also make a variable for the score of the player that can be increased when we collect a coin. #### Coins Time to make an incentive to run and jump around. Coins will be a `c` in our map string. So, the map would now look like this: ```javascript const map = '11111111111111111111111111.'+ '1 c 1.'+ '1 c c c 1.'+ '1 2 1 1 c 1 c 1 1.'+ '1 1 1 1 1 1.'+ '1 1.'+ '1 c c 1.'+ '1 c 1 1 c 1 c 1 1.'+ '1 1 1 1 1 1.'+ '1 1.'+ '1 c c c 1.'+ '1 1 c 1 c 1 1 1.'+ '1 1 1 1 1 1.'+ '1 1.'+ '1 c c c c 1.'+ '11111111111111111111111111'; ``` Now to make this work we have to add an option of what to do if the current character is a `c` in our map parser. I added something like this: ```javascript this.platforms = this.physics.add.staticGroup(); //================================== this.coins = this.physics.add.group(); //================================= let mapArr = map.split('.'); let drawX = 0; let drawY = 0; mapArr.forEach(row=>{ drawX = 0; for(let i = 0; i<row.length; i++){ if(row.charAt(i)==='1'){ this.platforms.create(drawX, drawY, "platform"); }else if(row.charAt(i)==='2'){ if(row.charAt(i+1)==='1'){ this.spawnPlayer(drawX-4, drawY-12); }else if(row.charAt(i-1)==='1'){ this.spawnPlayer(drawX+4, drawY-12); }else{ this.spawnPlayer(drawX, drawY-12); } //================================= }else if(row.charAt(i)==='c'){ this.coins.create(drawX, drawY+10, "coin"); } //================================= drawX+=40; } drawY+=40; }); ``` If you run this you will see that a bunch of little coins appear. But we can't collect them! This is fairly easy to add: ```javascript // Add this after the map parsing code this.physics.add.overlap(this.player, this.coins, this.collectCoin, null, this); ``` This function will check if there is an overlap between two objects. The two objects are the first two parameters. If there is an overlap, it will run the function that is passed in with the third parameter. `null` is just there for reasons and `this` is just passing on the `this` value to the function. We now need to make a function `collectCoin` that will run if there is an overlap: ```javascript this.collectCoin = (player, coin)=>{ player.score+=10; this.scoreText.setText("Score: "+ this.player.score); coin.destroy(); }; ``` If you run this you will see that you can now collect coins and increase your score. Success! There is only one more step before we are done. #### Spikes Time to add some difficulty to the game. We are going to have spikes that if you step on they will clear your score and respawn you. Let's first add them to our map as an `s`: ```javascript const map = '11111111111111111111111111.'+ '1 c 1.'+ '1 c c s c 1.'+ '1 2 1 s 1 c 1 c 1 1.'+ '1 1 1 1 1 1.'+ '1 1.'+ '1 c c s s 1.'+ '1 c 1 s 1 c 1 c 1 1.'+ '1 1 1 1 1 1.'+ '1 1.'+ '1 c s c c 1.'+ '1 s 1 c 1 c 1 s 1 1.'+ '1 1 1 1 1 1.'+ '1 1.'+ '1 c c c c 1.'+ '11111111111111111111111111'; ``` And now we can render them into our game: ```javascript this.platforms = this.physics.add.staticGroup(); this.coins = this.physics.add.group(); //================================== this.spikes = this.physics.add.group(); //================================== let mapArr = map.split('.'); let drawX = 0; let drawY = 0; mapArr.forEach(row=>{ drawX = 0; for(let i = 0; i<row.length; i++){ if(row.charAt(i)==='1'){ this.platforms.create(drawX, drawY, "platform"); }else if(row.charAt(i)==='2'){ if(row.charAt(i+1)==='1'){ this.spawnPlayer(drawX-4, drawY-12); }else if(row.charAt(i-1)==='1'){ this.spawnPlayer(drawX+4, drawY-12); }else{ this.spawnPlayer(drawX, drawY-12); } }else if(row.charAt(i)==='c'){ this.coins.create(drawX, drawY+10, "coin"); //================================== }else if(row.charAt(i)==='s'){ this.spikes.create(drawX, drawY+10, "spike"); } //================================== drawX+=40; } drawY+=40; }); ``` Let's do what we did last time - add an overlap detector between the player and the spikes. The code is pretty much the same: ```javascript //Next to the other overlap checker for the coins this.physics.add.overlap(this.player, this.spikes, this.die, null, this); ``` And now we have to make a function `die` that will be run when the player hits the spike. All we will do is stop the game and display text saying **YOU DIED**: ```javascript this.die = ()=>{ this.physics.pause(); let deathText = this.add.text(0, 0, "YOU DIED", { color:"#d53636", fontFamily:"Arial Black", fontSize:"50px" }).setScrollFactor(0); Phaser.Display.Align.In.Center(deathText, this.add.zone(400, 250, 800, 500)); } ``` `this.physics.pause` is what stops the game. The text adding should be pretty self explanatory. The bit that may be confusing is the line after that. This is the code I used to center the text. It accepts two arguments - the object to center and the zone in which to center it in. `this.add.zone` in turn accepts four arguments - the x, y, width and height of the zone. The x and y are in the center of the screen and the width is the width of the screen and the same for the height. When you run this code and jump on a spike you will see that it shows some big red text saying __YOU DIED__. And there we have it! Our completed game! Make sure to celebrate by wrapping __lots__ of duct tape around some cardboard. That was what the duct tape and cardboard were for. Nothing, really :). ## Final Word Thank you for sticking to the end of this monster of a tutorial. I hope you are proud of what you have made. If you liked this tutorial, please show support by voting for it. If you have any questions, suggestions or if you found a typo don't hesitate to post it in the comments! Also, if you put a spin on the game or make a cool map that be sure to share it! I would love to see what you guys can make out of this :). If you are too lazy to scroll up, the link to the repl that I made is [here](https://phaser-tutorial.kaldisberzins.repl.co/). Also, if you would like me to make some follow up tutorials outside of the competition about some more advanced features like scenes and (multiplayer?) then be sure to leave a comment. If enough people want it I will be sure to make some more tutorials. ## __EDIT__ I made an additional demo that delves into some more complicated concepts but it looks a lot better (I stole some sprites off the internet). It is just some uncommented code that you can play around with and try and see if you can make anything out of it. If you want to check it out you can find it [here](https://repl.it/@kaldisberzins/Phaser-Demo). Just wanna play it? Go [here](https://phaser-demo--kaldisberzins.repl.co/). Also, thank you guys for all the support in the comments! So heartwarming to see that many people like it. [email protected]_
22
posted by kaldisberzins (255) 9 months ago
5
JavaScript's Intersection Observers
# JavaScript's Intersection Observers JavaScript's [intersection observers](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) are pretty cool. They let you test if some DOM element is intersecting another DOM element. ## Concept There are two words you might want to know when dealing with Intersection Observers. The *root* element and the *target* element. I think it's easiest to show you the difference: First example: ![image](https://storage.googleapis.com/replit/images/1562027552786_7ae3510f9c3a6d6da181f9f014f85e05.png) Second example: ![image](https://storage.googleapis.com/replit/images/1562026909950_7441027c29655c6f4f64804ab836d39b.png) As you may have concluded, you usually want the target element to be a descendent of the root element. And the root element can be another DOM element, or the browser's viewport. You can think of the root element as "watching" to see if any child elements are intersecting it. For the second example, your code might look like the following ```html <body> <div id="root-element"> <div id="target-element"></div> </div> </body> ``` The root element *must* be an ancestor of the target element. So, you **cannot** have something that looks like the following ```html <!-- you cannot use intersection observers with this html --> <body> <div id="root-element"></div> <div id="target-element"></div> </body> ``` ## Creating the Intersection Observer Here is an example (I'll break it down step by step right underneath). ```js let player = document.getElementById("player"); let options = { root: document.getElementById("main"), threshold: 0 // when 50% of the target element is inside of the root element, that is when the callback fires }; let callback = function(entries, observer) { console.log(`The target element is intersecting with the root element. The threshhold ratio is ${options.threshold}.`) }; let observer = new IntersectionObserver(callback, options); // The root element is observing one element (player) observer.observe(player); ``` To create new Intersection Observer (`observer`), you must specify two things - `callback`, the function to invoke when your elements intersect - `options`, some options to change the behavior of the Intersection Observer After you create the Intersection Observer, you can then observe any other elements you want. In the example above, I am only observing the `player` element. (`observer.observe(player)`) ### Options When creating your Intersection Observer `options`, you can specify what the root object is and the threshold. (see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver) for more properties you can add) ```js let options = { root: null threshold: .5 // when 50% of the target element is inside of the root element, that is when the callback fires }; ``` - If the `root` property is `null`, then your root element is the viewport (like in the first visual example I showed). - When creating the `threshold`, as yourself this question: "How much do I want the *target* element to be inside the *root* element for the callback function to execute?" ## Thresholds The following shows a threshold of `0`. 0% of the target element is inside the root element. So in this case, 0% of the target element must be in the root element for the callback function to execute. ![InkedScreenshot_46_LI](https://storage.googleapis.com/replit/images/1562031846809_ba1a0a87d32f28cd43c00956fe76b221.jpeg) The following shows a threshold of `.5`. 50% of the target element is inside the root element. In this case, 50% of the target element must be in the root element for the callback function to execute. ![InkedScreenshot_44_LI](https://storage.googleapis.com/replit/images/1562031784168_b8803bdeb1422f5a92477931bcf2ebc7.jpeg) The following shows a threshold of `1`. 100% of the target element is inside the root element. And in this case, 100% of the target element must be in the root element for the callback function to execute. ![InkedScreenshot_45_LI](https://storage.googleapis.com/replit/images/1562031766698_6df199c2f09cf7d203cb579bcad78165.jpeg) ### Callback The callback function executes every time an intersection occurs. From here you have access to two variables: `entries` and `observer`. ```js let callback = function(entries, observer) { console.log(`The target element is intersecting with the root element. The threshhold ratio is ${options.threshold}.`) }; ``` - `entries` gives you an array of the DOM elements you are observing. *However*, note that each array element is an `IntersectionObserverEntry`. From this object, you can extract where the DOM element is, or how much the DOM elements are intersecting. Learn more about `IntersectionObserverEntry`s on the [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry). If you want to check out some example of using IntersectionObservers, I have a repl [here](https://repl.it/@eankeen/intersection-observer-tutorial-viewport) that uses the viewport as the root element and [another one](https://repl.it/@eankeen/intersection-observer-tutorial) that uses a DOM element as the root element. The latter is interactive, so you can move the squares around with the 'wasd' keys. I hope this tutorial was useful! Let me know what you think and post a comment!
0
posted by eankeen (528) 15 days ago
13
Javascript Games Tutorial #2: Awari
# Javascript Games Tutorial #2: Awari ## Introduction In this tutorial, we present a variation on the Ashanti game _Awari_. We have translated the BASIC game from David H. Ahl's 1978 book, "BASIC Computer Games -- Microcomputer Edition" into Javascript. This tutorial builds on the concepts discussed in Javascript Games Tutorial #1: GuessIt. It extends those ideas by introducing a game board and a computer opponent -- these will be the main focus of the tutorial. ## The Rules Before we begin presenting code, let's define the rules of the game. The game is played on a board with 14 spaces: two rows of six small "bins", and two larger "bins", one on either side of the two rows (see the image below). ![basic_board](https://storage.googleapis.com/replit/images/1545874722135_776cc70a4ee0d8a1ed0690b0082add63.pn) In this image, the green bins (the top row and the large bin on the left), belong to the computer, and the brown ones (bottom row and large bin on the right) belong to the human. Each of the twelve small bins start with three tokens each. The large "scoring" areas are empty. ![start_board](https://storage.googleapis.com/replit/images/1545875467417_1d566d9bb6955156e27ab1170bd38043.pn) The human player moves first. You can select the tokens from any of the small bins in the bottom row. You then "sow" the tokens by dropping one into each bin, starting immediately to the right of the bin you chose and moving counterclockwise around the board. For example, if you drew the tokens in bin #4 and sowed them, this would be the result: ![sown_board](https://storage.googleapis.com/replit/images/1545875467394_24603cde782c6c200ab7b353f63268dc.pn) If the last token you sow falls into your scoring bin, you may immediately take another turn. You may only earn one extra move each turn, even if your last token lands in your scoring area during your extra move. If the last token you sow falls in an empty small bin and there are tokens in the small bin opposite it, you may collect the tokens from _both_ bins and move them into your scoring bin. Once you have completed your move (or _moves_, if you earned a bonus turn), the computer will take its turn, following the same rules. If, after any turn, all the small bins in either row are empty, the game ends and the winner is the player whose scoring area contains the most tokens. Ties are allowed. ## Concepts This tutorial will cover two topics: * Representing the game board * Creating a computer opponent As with tutorial #1, we will present high-level topics here, and provide comments within the code itself with extra context and detail. # Representing the Board The Awari board is really just a line with 14 elements that wraps around on itself: ![numbered_board](https://storage.googleapis.com/replit/images/1545876858375_b4b46f91a3d8b8caea57f547a488f7ce.pn) Notice that we have started our numbering at 0, rather than 1. We'll explain why in a bit. Each "bin" on the board holds exactly one piece of information: the number of tokens it contains. So, to represent this board in Javascript, we need a data structure consisting of adjacent cells, each of which can hold a single number. The "array" is the perfect fit. In tutorial #1, we learned to define an array as follows: ```js var board[14]; ``` It's a good convention to define "constants" (i.e., variables whose values never change) and then define your variable sizes in terms of these constants, rather than using the constants directly. So: ```js var BINS_PER_BOARD = 14; var board[BINS_PER_BOARD]; ``` This defines a linear list of cells. Recall from tutorial #1 that the first cell in the list has index '0': _board[0]_ = first cell in the list We can read values from the array as follows: ```js var numTokensInCellFive = board[4]; ``` and write them like this: ```js board[11] = 12; // puts 12 tokens in the twelfth cell ``` In general, we access the nth cell like this: _board[n - 1]_ Keep these general array behaviors in mind as we discuss the functions that will manipulate the values on the board. ## Manipulating the Board To initialize the board for a new game, we could do this: ```js var STARTING_TOKEN_COUNT = 3; // 3 tokens per bin at game start var setUpBoard = function() { for (var i=0; i<BINS_PER_BOARD; ++i) { if (i == PLAYER_SCORE_BIN || i == COMPUTER_SCORE_BIN) { board[i] = 0; } else { board[i] = STARTING_TOKEN_COUNT; } } }; ``` That's nice and simple, but we need a way to convey this configuration to the player. We can modify the 'print' function from tutorial #1 to facilitate display of an ASCII representation of the board. ```js var print = function(text, stayOnLine) { if (!text) text = ""; if (!stayOnLine) text = text + "<br>"; text = text.replace(/\s/g, "&nbsp"); document.writeln(text); document.body.style.fontFamily = "monospace"; }; ``` This new 'print' function has several key features. This line: ```js if (!text) text = ""; ``` checks to see if the 'text' argument is invalid (the '!', or 'not' operator, reverses the truth value of whatever follows. If the _text_ variable is invalid, it evaluates to _false_, and the 'not' operator will reverse this to _true_). If the text _is_ invalid, we replace it with a valid, but empty, string. This next line: ```js if (!stayOnLine) text = text + "<br>"; ``` checks the value of the 'stayOnLine' argument. Here again, we use '!' to reverse the truth value of the argument. So, is 'stayOnLine' is _false_, we add the final "<br>" line break to the text, which will force the output down to the next line. Conversely, if 'stayOnLine' is _true_, then !stayOnLine is _false_, and we don't add any <br>. *This means that the next call to _print_ will start where the last one left off.* We will use that to display our board in ASCII text. This line: ```js text = text.replace(/\s/g, "&nbsp"); ``` uses regular expressions (the text demarked with forward slashes /.../) to replace all whitespace characters (\s) globally (the trailing 'g') within the the string with html space characters ("&nbsp"). We do this because, normally, web browsers purge whitespace as they see fit. This makes it difficult to display ASCII-art style drawings. This allows us to use "regular" spaces in code and have them render as HTML spaces on the web page. Finally, we get to this line: ```js document.body.style.fontFamily = "monospace"; ``` which tells the web page to render everything in a monospace font. Modern web fonts usually render different letters in different widths. This looks beautiful, but it makes it hard to align text when displaying console output. This line causes our text to render in a monospace style, in which all characters have the same width. This makes it easy to line up all our game board elements. Armed with this modified _print_ function, we can now display the game board with a couple of relatively simple functions: ```js var PLAYER_SCORE_BIN = 6; var COMPUTER_SCORE_BIN = BINS_PER_BOARD - 1; var BINS_PER_PLAYER = 6; var drawBoard = function() { print(" COMPUTER'S BINS"); print(" ", true); drawBins(COMPUTER); print("COMP YOUR"); print("SCORE SCORE"); if (board[COMPUTER_SCORE_BIN] < 10) { print(" " + board[COMPUTER_SCORE_BIN], true); } else { print(" " + board[COMPUTER_SCORE_BIN], true); } print(" ", true); if (board[PLAYER_SCORE_BIN] < 10) { print (" " + board[PLAYER_SCORE_BIN]); } else { print (" " + board[PLAYER_SCORE_BIN]); } print(" ", true); drawBins(PLAYER); print(" (1) (2) (3) (4) (5) (6)"); print(" YOUR BINS"); print(""); ; var drawBins = function(startIndex) { var i = 0; for (i=0; i<BINS_PER_PLAYER; ++i) { var iBin = i + startIndex; if (startIndex === COMPUTER) { iBin = board.length - 2 - i; } var numTokens = board[iBin]; if (numTokens < 10) { print (" " + numTokens + " ", true); } else { print (" " + numTokens + " ", true); } } print(); }; ``` Now that we can display the board, how do we make it change in reponse to player input? Assume that we somehow obtain a number from 0-5 to identify the player bin from which to pull tokens to sow. Here is the algorithm that sows the tokens where _startIndex_ is the bin from which we are drawing tokens and _scoreBin_ is the index of the scoring bin for the current player (6 if it's the player's move and 13 if it's the computer's move): ```js var sowTokens = function(startIndex, scoreBin) { var i=0; var numTokens = board[startIndex]; // How many tokens in the selected bin? var doMoveAgain = false; var iBin = 0; // Remove tokens from the selected bins. board[startIndex] = 0; // Starting with the bin after the selected one, // loop numToken times... for (i=startIndex+1; i<startIndex+1 + numTokens; ++i) { // ...wrapping around if we went past the end of the board... iBin = i % BINS_PER_BOARD; // ...and adding 1 token to each space. board[iBin] += 1; } // Find the bin across from our end point. var acrossBin = (BINS_PER_BOARD - 2) - iBin; // Did we place the last token on our score bin? if (iBin === scoreBin) { doMoveAgain = true; } // Did our last token land in an empty bin across from a non-empty bin? else if (board[iBin] === 1 && board[acrossBin] > 0) { // Yes -- so gather up the tokens from both bins... var capturedTokens = board[acrossBin] + board[iBin]; board[acrossBin] = 0; board[iBin] = 0; // ...and pu them in our score bin. board[scoreBin] += capturedTokens; } return doMoveAgain; }; ``` There are a couple of lines that need further explanation. First, this line: ```js iBin = i % BINS_PER_BOARD; ``` As the comment says, it's supposed to wrap around if we go past the end of the board. This works because the modulus operator (%) works as follows: A % B = the remainder of A divided by B. In our case, B is always BINS_PER_BOARD, or 14. Try plugging some numbers in to see how this works. 3 % 14 = 3 7 % 14 = 7 11 % 14 = 11 etc. In fact, for any 'A' less than 14, we just get 'A' back. At exactly 14, we get 0, because 14 / 14 has a remainder of 0. Note that board[14 % 14] is the same as board[0] -- which returns us to the starting bin. 15 % 14 has a remainder of 1. So board[15 % 14] is the same as board[1]. In other words, we've wrapped back around to the second space. Similary, 18 % 14 has a remainder of 4, which wraps us back to the 5th space. And so on. Then there's this line: ```js var acrossBin = (BINS_PER_BOARD - 2) - iBin; ``` This formula returns the index of the small bin across from any other small bin. For instance, the bin across from bin the player's second bin (which has index 1) is: acrossBin = (14 - 2) - 1 = 11 With a quick glance at our diagrams above, we can see this is correct. Feel free to plug in more values to make sure you believe it. Finally, notice that sowTokens() returns the value of doMoveAgain. If this is 'true', this indicates the last token fell in the appropriate scoring bin. On the player's turn, let's assume that she presses a key from 1-6. We have to convert this text value to a number value and subtract one so it corresponds to the correct spaces in the array (0-5). We also have to ensure that the chosen bin contains some tokens. If all that is true, we then sow the tokens and watch out for the case in which the player earned a free move. That routine will look something like this: ```js var isExtraMove = false; var doPlayerMove = function(key) { // Use parseInt to convert the text value of the key // the user pressed into an integer value. Subtract 1 // because the user pressed 1-6 to select from the first // six spaces, but these correspond to locations 0-5 in // the 'board' array. var binIndex = parseInt(key) - 1; // Check: if the chosen space is empty, report an invalid move. if (board[binIndex] === 0) { // TODO: report illegal move and try again. } // Call 'sowTokens'. This distributes the tokens around the board // and returns 'true' if the last token fell in the PLAYER_SCORE_BIN. // If that hapened (sowTokens returnd 'true') and we're not already // in the extra move phrase... else if (sowTokens(binIndex, PLAYER_SCORE_BIN) && !isExtraMove) { // ...check for game over... if (isGameOver()) { // TODO: tell the player the game is over. } else { // ...grant the player a second move. isExtraMove = true; // TODO: allow player to move again. } } // If the player is already taking an extra move, or if his move // didn't end in his score bin, go here: else { if (isGameOver()) { // TODO: tell the player the game is over. } else { // TODO: let the computer move. } } }; ``` Finally, we need to check to see if the game has ended. Recall that the end game condition is either row of small bins being empty. The check for that looks like this: ```js var isGameOver = function() { var i=0; var topTotal = 0; var bottomTotal = 0; for (i=0; i<BINS_PER_PLAYER; ++i) { topTotal += board[i + PLAYER_SCORE_BIN + 1]; bottomTotal += board[i]; } return topTotal === 0 || bottomTotal === 0; }; ``` # Creating a Computer Opponent In this section, we will outline how to make a computer opponent for the game. Our opponent will not do much thinking -- it will just make the play that earns the most points, given the current state of the board. The simplest way to do that is to loop through the top row of small bins (from index 7 through 12), simulate a turn taken at each bin, and keep the best resulting board. The trick to this is using the same starting board configuration each time through the loop. This requires that we have a way to save the board, simulate a move, then restore the board: var tryAllMoves = function() { saveCurrentBoard(); for (int i=FIRST_COMPUTER_BIN; i<COMPUTER_SCORE_BIN; ++i) { if (board[i] > 0) { if (sowTokens(i, COMPUTER_SCORE_BIN)) { // TODO: go again, unless this is already a bonus move. } } loadSavedBoard(); } } Notice that we have to handle the case where the computer earns a bonus turn. This is tricky, because when this happens, we need to try all possible moves based on the new board configuration, then return to the *original* configuration and try the remaining possibilities. For example, suppose that we're trying all possible moves, and on move #3, the computer gets an extra turn. We then need to check all possible moves based on the board state after making move 3. Once we have tested those, we need to return back to the original configuration and try moves 4, 5, and 6: ![hierarchy](https://storage.googleapis.com/replit/images/1545960484387_3800468a9b78268985218545ef75be4f.pn) In this diagram, all the moves in green represent moves simulated starting from the board's current configuration. The moves in black would be simulated using the state of the board after making move 3. Ugh. This looks complicated. How are we going to use the new board configuration when simulating a bonus turn? How will we return to the old board configuration afterwards? Fortunately, there's a clever way to do all this, but first we have to learn about _variable scope_. ## Variable Scope and the Execution Stack In everyday English, **scope** means, "extent or range," and broadly refers to the breadth of a region. It's much the same in computer jargon: "scope" refers to the region of a program over which a variable is accessible. "Wait, what? Variables aren't accessible everywhere?" Short answer: usually, no. We have been writing our programs in a very sloppy way, so far, putting all our variables in the "global scope", which **is** accessible everywhere, but this isn't great practice. It's good for us because it makes life easier, allowing us to focus on the fundamentals of programming, but soon, we'll develop better habits. For now, though, we'll use a simple rule for scoping variables: **_In a Javascript program, a variable is visible to anything within the same set of curly braces._** Here's an example: ```js { var valueOut = 5; var myFunc = function() { var valueIn = 3; console.log("Sum is " + (valueOut + valueIn)); }; var myOtherFunc = function() { console.log("ValueOut: " + valueOut); console.log("valueIn: " + valueIn); // Error! }; }; ``` In this case, valueOut is visible everywhere, because everything is contained within that outer set of curly braces. The variable valueIn is visible only within the function _myFunc_, whose scope is defined by the curly braces wrapping its code. This means that _myOtherFunc_ will throw an error because it's trying to access valueIn, which is defined outside its braces. "But wait...isn't valueOut *also* defined outside the _myOtherFunc_'s braces?" Yes -- but _myOtherFunc_ is contained within the same braces that contain valueOut, so valueOut is still visible to _myOtherFunc_ -- they are both defined within the scopt of the outer braces. In contrast, _valueIn_ is defined only within the scope of _myFunc_, and _myOtherFunc_ is defined outside of _myFunc_'s scope. Now consider another example: ```js var myFunc2 = function() { var outVar = 3; for (var i=0; i<3; ++i) { var inVar = i + outVar; console.log("Value is " + inVar); } console.log("inVar = " + inVar); // Error! }; ``` The curly braces of the 'for' loop create a new _local scope_ in which we define _inVar_. Therefore, _inVar_ is available to any code inside those braces. Notice that the last _console.log_ function is outside this scope, so trying to access _inVar_ would be an error. There is one final note to add to this entire discussion: you can think of all the code you write as being surrounded by a single set of invisible braces. This is known as the _global scope_, and variables defined in that scopre are visible everywhere in your program. So far, we have been defining everything in the global scope. As we mentioned above, that makes it easy to focus on learning programming fundamentals, but it's generally not a good idea, especially for larger programs. OK, so now we know what scope is. Is that helpful? By itself, not so much, but in conjunction with the concept of the _execution stack_, it's going to solve our problem. So, what's the execution stack? Without getting too technical, it's the program that runs our program. It performs two important functions: 1) it makes sure our code runs in the correct order, and 2) it reserves memory our program needs to run, as it is running. #2 is the key for us: the execution stack is responsible for reserving the memory our program needs. More specifically, every time the stack executes one of our game functions, it reserves memory required by the variables in that function's _scope_. _This is true even if it's executing a function inside of another function._ Consider this function: var myFunc = function() { var value = 1; value = value + 1; if (value < 3) { // Call this function again. myFunc(); } }; Think through how this might work when we call it. First, the exectuion stack creates a new scope, and within that scope, it creates the variable _value_, which contains 1. Next, in adds 1 to _value_'s contents, which become 2. Since the value is less than 3, the execution stack calls _myFunc_ again. At this point, you might be tempted to think that _value_ already exists, so the next time we add 1 to it, it will increase to 3 and we will skip over the 'if', but that's not what happens. Each time the execution stack enters the _if_ statement and calls _myFunc_, it creates a _new_ scope, because this is a new call to the function. Within that scope is a new _value_ variable that gets set to 1. That means it always executes the contents of the _if_ clause. This leads to an endless cycle called _infinite recursion_. The word _recurse_ refers to the process in which a function calls itself, as in _myFunc_ above. The important takeaway here is that each version of the function maintains its own local data, which is visible only to that version of the function. With that knowledge, and these functions: ```js var copyBoard = function(srcBoard, destBoard) { destBoard.length = 0; for (var i=0; i<srcBoard.length; ++i) { destBoard.push(srcBoard[i]); } }; var saveBoard = function(boardCopy) { var i=0; boardCopy.length = 0; for (i=0; i<board.length; ++i) { boardCopy.push(board[i]); } }; var restoreBoard = function(boardCopy) { var i=0; for (i=0; i<board.length; ++i) { board[i] = boardCopy[i]; } } ``` we can now write the function that will act as the brain for our computer opponent: ```js var tryAllMoves = function(bestBoard) { // Find the best move from among the computer's six choices. var i = 0; var bestScore = -1; var bestBin = -1; var boardCopy = []; var newBestBoard = []; saveBoard(boardCopy); for (i=0; i<BINS_PER_PLAYER; ++i) { var iBin = PLAYER_SCORE_BIN + 1 + i; if (board[iBin] > 0) { if (sowTokens(iBin, COMPUTER_SCORE_BIN)) { if (!isExtraMove) { isExtraMove = true; tryAllMoves(newBestBoard); copyBoard(newBestBoard, board); } } if (board[COMPUTER_SCORE_BIN] > bestScore) { saveBoard(bestBoard); bestScore = board[COMPUTER_SCORE_BIN]; } restoreBoard(boardCopy); } } }; ``` Here are the keys to understanding how it works: 1) It accepts a single argument called bestBoard. This is an array into which the best move will be copied. 2) It defines a local variable called boardCopy into which we copy the current board configuration. 3) It uses restoreBoard(boardCopy) to return the board to it's original configuration after every simulated run. 4) If sowTokens() returns 'true', meaning the computer earned an extra turn, _it calls itself before restoring the board._ Think about #4 for a moment, then think about the last illustration with the green and black "Play #" diagram. When the computer moves, it calls _tryAllMoves_. The program will start making simulated plays. These correspond to the green "Play #" entries above. If the computer earns an extra turn, it will call _tryAllMoves_ again. Since the board hasn't been restored for this pass through the loop, it will reflect the state of the board after the current simulated play. As the program starts to execute this second version of _tryAllMoves_ (i.e., the black "Play #" entries above), it will save this modified board and start trying all the moves using **it** as the original state. Once this second version of _tryAllMoves_ has finished, it will copy its best result into the _bestBoard_ array, then exit back into the first version of _tryAllMoves_. This takes us back to the lower, green "Play #" entries in the diagram. In terms of code, we come back at the 'copyBoard(newBestBoard, board)' command inside the _if_ statement. This copies the best board from our free move back into the current board. The first version of _tryAllMoves_ then tests this configuration to see if it's the best result, and if so, copies it into the _bestBoard_ variable. Then it restores the board back to its original configuration and tries the next move. Don't worry if this makes your brain hurt. Recursion is a difficult concept to grasp. Carefully stepping through a specific example can help. But, believe it or not, that's all there is to our simple brain for Awari. There isn't much else to the program, as you will see if you examine the source code. One thing you'll notice if you play a few games is that your opponent will always choose exactly that same options, given a particular board configuration. Even if there are two plays that yield the identical amount of points, it will always pick the move closest to the player's starting goal. Here's a challenge: see if you can figure out why _tryAllMoves_ produces this behavior. Once you figure it out, see if you can modify it so that there's a chance it might pick a different move of equal worth. You will also notice that your opponent isn't smart enough to protect itself. Specifically, if it has tokens across from an empty space, these tokens are subject to theft from the human player. Can you modify _tryAllMoves_ to detect these vulnerabilities and avoid them? # Conclusion That wraps up this tutorial. We hope this gives you some idea how to represent your game board as data inside your program, as well as how to make your program "play" a game. That subject -- which blurs into "game AI" (artificial intelligence) is broad and deep -- we've barely scratched the surface of the tip of the iceberg. But, hopefully, you have some idea this solution works, and how you might adapt it for other, similar games.
11
posted by MarkKreitler (33) 7 months ago
14
Solve any Quadratic Equation (HTML, CSS, JS)
If you are lazy like me, it won't hurt you to just create this little app and solve all the quadratics with it instead of factorizing every damn time... so let's begin. First things first, let's prepare our basic HTML backbone: ```html <!-- This file is: index.html --> <!DOCTYPE html> <html> <head> <title>Quad</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <link href="style.css" rel="stylesheet" type="text/css"> </head> <body> <!-- HTML elements go here --> <script src="script.js"></script> </body> </html> ``` Now, we'll need a header `h1`, few `input` fields, submit `button`, and `div`s to display answers. ```html <!-- This file is: index.html --> <!DOCTYPE html> <html> <head> <title>Quad</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <link href="style.css" rel="stylesheet" type="text/css"> </head> <body> <h1>Solve Any Quadratic Equation</h1> <div id="input"> <input id="a"> x<sup>2</sup> <input id="b"> x <input id="c"> = 0 </div> <button id="submit">Solve</button> <div id="answers"> <p id="DIS"></p> <p id="NOS">No solutions</p> <p id="S1"></p> <p id="S2"></p> </div> <script src="script.js"></script> </body> </html> ``` I put them in `div`s so it's easier to manipulate their CSS styles. Our `input`s are going to be filled with the **coefficients** for the quadratic, `#DIS` is going to display the **discriminant**, `#NOS` is a single line that says "No solutions" (we are going to only make it visible when there are no real roots), `#S1` & `#S2` are going to hold the answers. > The HTML portion of our code is now complete. Let's add some styles now: ```css /* This file is: style.css */ h1 { font-family: Arial, Calibry, sans-serif; margin-bottom: 30px; } body { font-family: Georgia, serif; text-align: center; } input { text-align: center; width: 80px; font-size: 28px; } #input { font-size: 28px; } button { margin: 30px auto; padding: 10px 15px; background-color: blue; border: none; color: white; cursor: pointer; } #NOS { display: none; } ``` I recon I don't need to explain anything here since these are just styles. You can change these as you like. I didn't make it too fancy because why would I, but if you want to, you can "sweat" a bit. > The only important part is to make `#NOS` `display: none` since we only want to display this one when there are no real roots. The last and the main thing we need is some JavaScript magic to make it work. I am not going to use JQuery just because this app is too small and we can actually deal with it without any external help, so first of all I'll write 2 functions to replace some of the JQuery's functionality. ```javascript /* This file is: script.js */ function getID(i) { return document.getElementById(i); } function getVal(i) { return getID(i).value; } ``` Now that we have `getID()` and `getVal()` in place, we can code for real. What we want is this: whenever user would press the `#submit` button, we want to do execute some function `solve()`. To do so, we are going to add an event listener to the button and also let's declare an empty (for now) function `solve()`. ```javascript /* This file is: script.js */ function getID(i) { return document.getElementById(i); } function getVal(i) { return getID(i).value; } function solve() { // IMPLEMENT ME, PLEASE! } var submitButton = document.getElementById("submit"); submitButton.onclick = function() { solve(); }; ``` > This is the backbone of our JS. Now let's `solve()` it. Before anything else, we have to fetch those coefficients, so let's get to it: ```javascript /* Focusing on the solve() function */ function solve() { var a = parseInt( getVal("a") ), b = parseInt( getVal("b") ), c = parseInt( getVal("c") ); if ( isNaN(a) ) { a = 1; } if ( isNaN(b) ) { b = 0; } if ( isNaN(c) ) { c = 0; } } ``` Last 3 lines with `if ( isNaN(x) ) { x = ... }` ensure that we don't have a `NaN` type inside `a`, `b`, or `c`. The next thing to do is calculating the discriminant. Discriminant is always equal to `D = b*b - 4ac` ```javascript /* Focusing on the solve() fucntion */ function solve() { var a = parseInt( getVal("a") ), b = parseInt( getVal("b") ), c = parseInt( getVal("c") ); if ( isNaN(a) ) { a = 1; } if ( isNaN(b) ) { b = 0; } if ( isNaN(c) ) { c = 0; } var D = b*b - 4 * a * c; } ``` Let's declare some more variables and set everything to its initial state (e.g. `#S1` & `#S2` must be empty before we display any solutions and `#NOS` must be reset to `display: none` every time the `#submit` button is pressed): ```javascript /* Focusing on the solve() fucntion */ function solve() { var a = parseInt( getVal("a") ), b = parseInt( getVal("b") ), c = parseInt( getVal("c") ); if ( isNaN(a) ) { a = 1; } if ( isNaN(b) ) { b = 0; } if ( isNaN(c) ) { c = 0; } var D = b*b - 4 * a * c; var dis = getID("DIS"), nos = getID("NOS"), s1 = getID("S1"), s2 = getID("S2"); nos.style.display = "none"; s1.innerHTML = ""; s2.innerHTML = ""; dis.innerHTML = 'Discriminant = <span id="D"></span>'; var d = getID("D"); } ``` Now that all the helper variables are put in place, we can start displaying the answer. First of all, let's display the discriminant. After we do that, we have three options depending on its value: + D is smaller than 0 --> no real roots + D is equal to 0 --> one root + D is greater than 0 --> two roots ```javascript /* Focusing on the solve() function */ function solve() { var a = parseInt( getVal("a") ), b = parseInt( getVal("b") ), c = parseInt( getVal("c") ); if ( isNaN(a) ) { a = 1; } if ( isNaN(b) ) { b = 0; } if ( isNaN(c) ) { c = 0; } var D = b*b - 4 * a * c; var dis = getID("DIS"), nos = getID("NOS"), s1 = getID("S1"), s2 = getID("S2"); nos.style.display = "none"; s1.innerHTML = ""; s2.innerHTML = ""; dis.innerHTML = 'Discriminant = <span id="D"></span>'; var d = getID("D"); d.innerHTML = D.toString(); if (D < 0) { // no real roots // display #NOS } else if (D == 0) { // one root equal to // S = -b / (2a) } else { // two roots equal to // S1 = (-b + √D) / (2a) // S2 = (-b - √D) / (2a) } } ``` > Last step and we are all set! Let's just replace comments with real code! ```javascript /* Focusing on the solve() function */ function solve() { var a = parseInt( getVal("a") ), b = parseInt( getVal("b") ), c = parseInt( getVal("c") ); if ( isNaN(a) ) { a = 1; } if ( isNaN(b) ) { b = 0; } if ( isNaN(c) ) { c = 0; } var D = b*b - 4 * a * c; var dis = getID("DIS"), nos = getID("NOS"), s1 = getID("S1"), s2 = getID("S2"); nos.style.display = "none"; s1.innerHTML = ""; s2.innerHTML = ""; dis.innerHTML = 'Discriminant = <span id="D"></span>'; var d = getID("D"); d.innerHTML = D.toString(); if (D < 0) { nos.style.display = "block"; } else if (D == 0) { var S = -b / (2 * a); s1.innerHTML = S.toString(); } else { var S1 = ( -b + Math.sqrt(D) ) / (2 * a), S2 = ( -b - Math.sqrt(D) ) / (2 * a); s1.innerHTML = S1.toString(); s2.innerHTML = S2.toString(); } } ``` > Our script is now finally ready. We can now solve any quadratic equation using this simple web app! Enjoy.
2
posted by sharpvik (36) 9 months ago
8
Javascript Games: Lesson 1 -- GuessIt
# Introduction This is the first tutorial in a series designed to teach basic JavaScript game programming. It's the spiritual successor to BASIC Computer Games, in which David H. Ahl provided source code for 100 BASIC games. Today, we would think of those games as "terminal programs", displayed using ASCII text. We will be running our games in a web browser, and while they will initially be text-based, we will quickly move into the realm of canvas graphics. But first things first. In this tutorial, we will cover the boilerplate concepts behind all games: variables, output, input, functions, loops, and flow-of-control. Don't worry if you don't know what any of those mean -- that's what the tutorial is for. Once we have discussed these fundamentals, we'll present the code for our first game: GuessIt. # Fundamentals All games rely on the following foundational concepts. More advanced games involve additional ideas, but *every* game needs these: * variables -- named buckets that hold data * output -- how we show the game to the user * input -- how we accept data from the user * functions -- how we re-use code from at different points in a program * loops -- how we perform repeated tasks efficiently * flow-of-control -- how we make the game choose one path among many **Variables** Variables are just chunks of computer memory that hold data for us. You can think of a variable as a bucket with a name. You can put things in the bucket and take them out again. In some languages, there are different kinds of buckets for different kinds of data. Text goes in one kind of bucket, decimal numbers in another, and integers in yet another. In JavaScript, all buckets can hold anything you want. This simplifies life in some ways, and complicates it in others. To define a new bucket, we use the 'var' keyword, followed by the name of the bucket: ```js var myName; ``` (the semicolon tells JavaScript that it has reached the end of this line of code). Once you have defined a new bucket, you can refer to it by name elsewhere in your code. You can use *operators* like the equals sign to manipulate the data inside the bucket. For example, in JavaScript, the single equals sign (**=**) tells the computer to take whatever is on the right hand side and put it into the thing on the left hand side: ```js myName = "Mark"; ``` takes the text data inside the double quotes and sticks it in the bucket called myName. You can combine the definition of the bucket and the assignment of its value into a single line, if you like: ```js var myAge = 49; ``` Now the variable 'myAge' has a value of 49. Variables can hold text data, numerical data, code, graphics, sound, and any combination of these things wrapped into an 'object' (which we will cover in later tutorials). Variables can also contain lists of data. These lists are often called "Arrays", and you create a list by enclosing the entries in square brackets ('[' and ']') and separating them by commas: ```js var myDogs = ["Hershey", "Koda"]; ``` You can also break this up over multiple lines: ```js var myDogs = [ "Hershey", "Koda" ]; ``` That's a lot to remember, but don't sweat it. The most important thing is: *variables are buckets that hold data*. **Output** All games need to output data to the player(s). Our first games will run on a web page, so we will use HTML for output. We will rely on the "write line" command supplied by the document in which we are running: ```js document.writeln("Your message goes here..."); ``` Note that in HTML, the "string" of letters '<br>' (without quotes) is a "line break" that marks the end of a line of text. Use this to move down to the next line. For example: ```js document.writeln("First line.<br>"); document.writeln("Second line.<br>"); ``` These will appear as two separate lines in the web browser. Without the line breaks, they would appear together on a single line, even though we used two different writeln statements. **Input** Players need a way to affect the games they play, whether it's telling the dealer to "hit me" in Blackjack, or firing a weapon in Doom. In our case, we will be responding to keyboard events. To do this, we will rely on the "event listener" system supplied by the document in which we are running. An "event" is a change in the computer environment that affects the document. This could be many things: a key press, a mouse click, a message telling the browser to resize itself. Web documents supply an "event listener" system to capture and respond to important events. To listen for a particular event, we will call the document's "addEventListener" code: ```js document.addEventListener("keydown", function(event) { // ...your code here... }); ``` The above line tells the document to execute "...your code here..." whenever the user presses key down. Don't worry about the 'function(event)' bit -- we'll discuss that in the next section. For now, let's consider this concrete example: ```js document.addEventListener("keydown", function(event) { document.writeln("Someone pressed a key!"); }); ``` This displays the message, "Someone pressed a key!" each time the user presses a key. There are many other events to which the document can respond, like "keyup", "mousedown", "mouse", and "mousemove", just to name a few. We'll get to those when we need them. For now, "keydown" is good enough. Sometimes, you want your game to ignore events to which it previously responded. You can accomplish this using the "removeEventListener" command: document.removeEventListener("keydown"); This would undo the "addEventListener" we used above. **Functions** Often, you write a chunk of a code that you want to use over and over. For example, suppose you wrote a chess program. Games of chess can last many turns. You would hope that you could write the code to play a single turn, then simply re-use that code for each subsequent turn. *Functions* allow you to do this. A function is a chunk of self-contained code designed to perform a specific task. Functions often require data to complete their task, so when you define the function, you must indicate what data it's expecting as input. In JavaScript, function definitions look like this: function(list of input data) { // Chunk of code to execute using the input data }; If this seems confusing, think of a function as a meat grinder: you put stuff in, you turn the crank, and something useful comes out. The "stuff you put in" is the "list of input data". The "turn the crank" bit is the "chunk of code" part. The "something useful comes out," well...we haven't covered that. It's called the 'return value'. A specific example will clear all this up. Consider a function that computes the area of a rectangle: ```js function(length, width) { var area = length * width; return area; }; ``` You send in a length and a width, it multiplies them together to find the area, then it 'returns' that area back to you. Remember that addEventListener stuff? Recall that the function looked like this: ```js function(event) { document.writeln("Someone pressed a key!"); } ``` Now you know what the 'function' is for -- that tells JavaScript what function to execute when someone presses a key. The 'event' data is passed into the function from the web browser, and it tells the function things like what key was pressed. In fact, we could change the function to display that information as follows: ```js function(event) { document.writeln("Someone pressed the " + event.key + " key!"); } ``` Notice that this function doesn't return anything (there is no 'return' statement at the end). That's fine. Not all functions return a value. Sometimes you use them just to produce output when the crank is turned. One last note: you can store a function inside a variable. Doing so lets you execute (or call) the function using the bucket's name. This is handy shorthand for executing code! Example: ```js var print = function(theMessage) { document.writeln(theMessage + "<br>"); }; ``` Now, any time you want to display a message, you can use this: print("Fred ate bread!"); This will send the data "Fred ate bread" to the above function and print it out, automatically appending a <br> so that subsequent messages appear on subsequent lines in the document. Applying this to our area function, we get: ```js var areaOfRect = function(length, width) { var area = length * width; return area; } ``` And we could write a short program as follows: ```js var area = areaOfRect(3, 4); print("The area is " + area); ``` What do you think would be the output of this program? **Loops** Often, games need to rapidly repeat a task. For example, if you are drawing a square grid, you must print multiple horizontal and vertical lines. For something like tic-tac-toe, this isn't so bad, but for a game like Go, you wouldn't want to draw every line with a new command. Loops allow programmers to efficiently perform duplicate commands. There are several kinds of loops, all of them follow the same philosophy: the programmer defines a *loop condition* during which the program repeats a chunk of code over and over. The programmer follows the loop condition by the code to be repeated: [Loop Condition Goes Here] { // ...code to repeat goes here } Notice that in JavaScript, we use curly bracers ('{' and '}') to "section off" the code that will be repeated. JavaScript supports several different kinds of loops. We will start with just two: the 'while' loop and the 'for' loop (which is really just a 'while' loop in fancy clothes). The *while* loop looks like this: while (...something is true...) { // Do this code } Consider this example: ```js var greets = 0; while (greets < 5) { print("Hey there!"); greets = greets + 1; } ```` This probably looks confusing, especially this bit: greets = greets + 1; Clearly, this is nonsense! There is no number that equals itself plus one. True...but remember -- in JavaScript, the '=' symbol differs from what you learned in Math. In JavaScript, it says, "takes what's on the right-hand side and put it into the thing on the left-hand side." Also remember that 'greets' is a bucket that we initially fill with the value 0. So this: greets = greets + 1; reads as: "Take the value in greets (currently 0), add one to it, and place this back in the greets bucket." So, after executing this line the first time, greets' value changes from 0 to 1. We then hit the bottom curly brace and bounce back up to the 'while' statement, where we check the loop condition. Is the value in greets less than 5? Yep! So we once again execute the code inside the curly braces. While doing so, we again hit the line greets = greets + 1; but *this* time, greets contains '1'. So what happens? We add one to it, and store the resulting value (2) back in greets. Then we hit the bottom curly brace, bounce back up to the top of the loop, and check the conditions. 2 is still less than 5, so we go around again... ...and again... ...and again... ...and again... ...and finally we bounce back to the top, check the condition and see that greets is **equal** to 5, rather than less than 5, so the loop condition is false and we break out of the loop by instantly jumping past the last curly brace. That might seem like a silly example: why would we want to print out the same message 5 times? So let's consider a more useful example. Remember my dogs? ```js var myDogs = ["Hershey", "Koda"]; var count = 0; while (count < myDogs.length) { print(myDogs[count] + " is barking."); count = count + 1; } ``` Now we're using the *while* statement to *iterate* (or step through) a list of data. Notice that we use square brackets ('[' and ']') to access a single element of the list, and that the first element in the list is number 0. In other words: myDogs[0] contains the word "Hershey", and myDogs[1] contains the word "Koda". Also note that JavaScript will tell you how long any list is when you use the ".length" accessor: myDogs.length is 2. *While* loops are handy, but after you've written a few, you'll see that most of them fall into the following pattern: ```js var some_loop_counter = some initial value; while (condition_involving_the_loop_counter) { // ...loop code... some_loop_counter = some_loop_counter + some increment; } ``` Recognizing this pattern, the authors of JavaScript created the *for* loop to make your code more concise. The *for* loop works exactly like the *while* loop, but it lets you compress the syntax: for (initialize the counter; check the condition; update the counter) { // ...loop code... } For example, our doggy-loop above would look like this when written as a *for* loop: ```js for (var count = 0; count < myDogs.length; count = count + 1) { print("My dog " + myDogs[count] + " is barking."); } ``` If you want to compress this even further, you could take advantage of the math operators '+=' or '++': count += 1; is shorthand for count = count + 1; And both count++ and ++count tell JavaScript to increase the value of *count* by 1. There is a difference between these ++count and count++, but we'll save that for another tutorial. So, the super-compressed version of our doggy loop would be this: ```js for (var count = 0; count < myDogs.length; ++count) { print("My dog " + myDogs[count] + " is barking."); } ``` **Flow-of-Control** Finally, let's talk about "flow of control." That's just a fancy way of saying, "my game can execute one of several options at the appropriate time." For example, suppose you are making a dungeon crawler and you have AI monsters that can take one of several actions when encountering the player. You would use flow of control statements to pick one of those actions when an encounter occurs. As with loops, there are several flavors of decision-making in JavaScript. For now, we will restrict ourselves to the "if...then" statement. Generally speaking, that structure looks like this: If (condition 1 is true) { // Do this code } else if (condition 2 is true) { // Do *this* code instead } else if (condition 3 is true) { // etc. } // and so on for as many conditions as you like, until finally else { // Do this if nothing else up to this point was true. } Let's look at an example by modifying our doggy loop: ```js for (var i=0; i<myDogs.length; ++i) { if (myDogs[i] === "Hershey") { print(myDogs[i] + " is barking."); } else if (myDogs[i] === "Koda") { print(myDogs[i] + " is farting."); } else { print("That's not my dog!"); } } ``` Note that the triple-equals ('===') in JavaScript asks the question, "is equal to". So a line like this: if (myDogs[i] === "Hershey") Reads as, "if the ith element in myDogs is equal to the word Hershey". As you can see, this will print out: Hershey is barking. Koda is farting. Which might seem like a fart joke, but is sadly the status quo with my black lab, who is currently practicing his "art" 6 feet from my desk. Ugh. # Conclusion You now have all the building blocks you need to make a simple game. For our first outing, we will write "GuessIt" -- a variation on the card game Acey Ducey. In that game, you flip two cards, then place a bet as to whether a third card, when revealed, will fall between the first two. In GuessIt, we use randomly-generated numbers from 0 to 9, but otherwise, the game is identical. Take a look at the source code. We have broken it up into two files: _definitions.js -- in which we define all the functions and variables used in the game_ _commands.js -- which actually executes the commands defined in definitions.js to start the game_ If you just loaded definitions.js, the game would never start, because the definitions don't do anything but fill buckets with values and functions. If you just loaded commands.js, the program would throw an error because the commands rely on the buckets defined in definitions.js. You don't have to lay out your JavaScript programs this way -- we just did it to drive home the point that defining variables and functions is a *passive* activity, like setting up the game board. It doesn't actually result in the activity of playing the game itself. To do that, you have to execute the commands you created earlier. The code for GuessIt contains a lot of comments to help you understand what we're doing at each point. Some of it repeats what we have covered here, but in a more specific context. Once you are comfortable with what it's doing, try modifying the game. Here are some suggestions: Change the background color by using the command: document.body.style.backgroundColor = "gray"; // Or whatever common color name you want Change the amount of starting cash given to the user Change the payout formula Or, the biggie: Add a new eventListener when the player loses that resets her starting cash and allows her to play again Good luck, and have fun!
7
posted by MarkKreitler (33) 7 months ago
3
🎮 3D Games with BabylonJS: A Starter Guide
🎮 3D Games with BabylonJS: A Starter Guide Babylon is a JavaScript framework enabling developers to create 3D graphics with ease. Popular games such as [Shell Shockers](https://shellshockers.io) use this framework to create 3D games. It's shinier than ever, now that Version 4.0 just released! I'll show you how to use Babylon to create a simple 3D environment. We'll go over creating an environment, adding objects to the environment, adding gravity, and adding collisions. ![image](https://storage.googleapis.com/replit/images/1561234058959_068dab6fa178259c011429903c03c093.png) ## Setup First, create an `HTML,CSS,JS` repl Then, modify your `index.html` file. We're going to be adding the BablyonJS script tag, and our `main.js` file. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <script src="https://cdn.babylonjs.com/babylon.js"></script> <title>Babylon FPS</title> </head> <body> <canvas id="canvas"></canvas> <script src="main.js" type="module"></script> </body> </html> ``` You may have noticed the`type="module"` attribute on the `main.js` script tag. That means we're importing `main.js` as a JavaScript module. This means that we can use the [`import`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) syntax to import other `.js` files inside of `main.js`. I like doing this because it makes the `index.html` file more readable. (you can learn more about JS Modules (ESM) [here](https://flaviocopes.com/es-modules). When you start the app, it should serve an empty page. Let's add the basic BabylonJS stuff now! 😄 ## Creating a Babylon Scene Make sure you add the following to your `main.js`. I'll explain nit below ```js import { createGameScene } from "./gameScene.js"; window.addEventListener("DOMContentLoaded", start); function start() { let canvas = document.getElementById("canvas"); let engine = new BABYLON.Engine(canvas, true); let scene = createGameScene(canvas, engine); let sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {}, scene); engine.runRenderLoop(function() { scene.render(); }); window.addEventListener("resize", function() { engine.resize() }); }; ``` - Inside the `start`, function, we're creating a BABYLON [engine](https://doc.babylonjs.com/api/classes/babylon.engine). The `engine` 'translates' the BabylonJS API to canvas painting. For example, when you create a sphere with `BABYLON.MeshBuilder.CreateSphere("sphere", {}, scene);`, it is responsible for actually drawing the `sphere` on the screen. - A scene contains all of our scene objects. Think of it as a room - it can contain lights, cameras, 3D objects and more - [`engine.runRenderLoop`](https://doc.babylonjs.com/api/classes/babylon.engine#runrenderloop) is a function that runs continuously. In this case, we want Babylon to continuously render the scene (`scene.render()`) You may notice that we're using the `createGameScene` function, and we're importing it from `./gameScene.js`. So, create a `gameScene.js` file. Like the name says, we're going to be creating the game scene in this file. This is where we will be creating our lights, cameras, and our meshes (3D objects). ```js let createGameScene = function(canvas, engine) { // Create scene let scene = new BABYLON.Scene(engine); // Create camera let camera = new BABYLON.UniversalCamera("Camera", new BABYLON.Vector3(2, 2, 2), scene); camera.attachControl(canvas, true); camera.setTarget(BABYLON.Vector3.Zero()); // Create lights let light = new BABYLON.HemisphericLight("myLight", new BABYLON.Vector3(1, 1, 0), scene); return scene; }; export { createGameScene }; ``` - First, we're creating a regular BABYLON [`scene`](https://doc.babylonjs.com/api/classes/babylon.scene). Whatever you put inside this scene will get rendered by the `engine`. - Then, we create a new [`UniversalCamera`](https://doc.babylonjs.com/api/classes/babylon.universalcamera) camera. BabylonJS automatically sets up your camera so you can look around with your mouse or move around with the arrow keys. The`camera.attachControl()`lets us interact with the canvas using our mouse. If we did not add that, then Babylon would not know when we would be moving our mouse. If you're wondering what the `export { createGameScene }` is all about, it's a part of the ES6 Module syntax, also known as ECMAScript Modules (ESM). We don't have time to go over it in this post, but you can read about it [here](https://flaviocopes.com/es-modules). In a nutshell, we're creating a reusable function that can be imported into any other file. Anyways, we're getting results! :D But unfortunately, the canvas is not styled properly ![initial-view](https://storage.googleapis.com/replit/images/1560012861816_73694c5a7f3b945b6d62086a668314c1.pn) ## Styling the Canvas Let's create a `style.css` to fix the canvas styling ```css * { padding: 0; margin: 0; box-sizing: border-box; } #canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; touch-action: none; } ``` Be sure to add `<link rel="stylesheet" href="style.css">` to your `index.html` And presto! Now the canvas fills up the entire screen! ![displayed-properly](https://storage.googleapis.com/replit/images/1560013151353_538243f989f87cb8811eed3c0c6b5463.pn) View the code in this [this repl](https://repl.it/@eankeen/babylon-tutorial-1) ## Loading Screen Games usually have a loading screen. This improves the user experience since the user sees a loading bar rather than a blank page. Let's create one! Inside of `loadingScreen.js`, add ```js let showLoadingScreen = function(canvas, engine) { let defaultLoadingScreen = new BABYLON.DefaultLoadingScreen(canvas, "Please Wait", "black"); engine.loadingScreen = defaultLoadingScreen; engine.displayLoadingUI(); }; let hideLoadingScreen = function(engine) { engine.hideLoadingUI(); }; export { showLoadingScreen, hideLoadingScreen } ``` - The `showLoadingScreen` function creates a default loading screen. The words "Please Wait" is shown on a `black` background with a loading spinner. - The [`hideLoadingScreen`](https://doc.babylonjs.com/api/classes/babylon.engine#hideloadingui) function simply hides the loading screen Learn more about [loading screens](https://doc.babylonjs.com/api/interfaces/babylon.iloadingscreen) and [custom loading screens](https://doc.babylonjs.com/how_to/creating_a_custom_loading_screen) on the Babylon docs The next step is to call `showLoadingScreen` and `hideLoadingScreen` from `main.js`. Firstly, import the functions from `loadingScreen.js` ```js import { showLoadingScreen, hideLoadingScreen } from "./loadingScreen.js"; ``` Now we have to show and hide the loading screen at the right times. Let's use `showLoadingScreen()` right after we create the Babylon `engine`. ```js let canvas = document.getElementById("canvas"); let engine = new BABYLON.Engine(canvas, true); showLoadingScreen(canvas, engine); ... ``` We can't forget to hide the loading screen when everything has been loaded! I'm adding the code right after I create my `sphere`. Note that this will *not* hide the loading screen after you create the `sphere`. Babylon will automatically know when to hide the loading the screen. ```js ... let sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {}, scene); scene.afterRender = function() { hideLoadingScreen(engine) }; ... ``` Now your loading screen should work! It will look something like this ![image](https://storage.googleapis.com/replit/images/1561234222213_96fb156ee08a3a1e728078ddc7d7b4fb.png) View the code in [this repl](https://repl.it/@eankeen/babylon-tutorial-2) ## Making the Camera Move Although we are able to move the sphere with the mouse, we still can't move the camera with the 'WASD' keys. Let's add some extra controls to the camera! Inside of my `gameScene.js`, I added: ```js camera.keysUp.push(87); camera.keysDown.push(83); camera.keysLeft.push(65); camera.keysRight.push(68); ``` - `camera.keysUp` is an array that contain [key codes](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode). (for example, `87` is a keycode for pressing 'W'). We're adding `87` to the array, which means the camera will move 'up' when 'W' is pressed ### Other Camera Properties You can also mess around with other properties of the camera! You can change the speed, inertia, or the field of view (FOV). You can view the whole list of properties on the [Babylon documentation](https://doc.babylonjs.com/api/classes/babylon.camera). These are the values of `fov`, `speed`, and `inertia` I'm using. You can change them whenever you want! ```js camera.speed = 2; camera.fov = 0.8; camera.inertia = 0; ``` Now it works! Below, I set the `fov` to `0.3`. ![image](https://storage.googleapis.com/replit/images/1561234349275_03cf5129943817eed3a5045dec12fbda.png) View the code in [this repl](https://repl.it/@eankeen/babylon-tutorial-3) ## Adding a Pointer Lock Moving around is cool and all, but isn't it bothersome that we have to hold 'left click' if we want to look around? To fix this, we can use the [Pointer Lock API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API). Think of it like this: when we click on the canvas, the mouse is 'locked' in the canvas. When this happens, moving your mouse doesn't move the cursor, but instead directly interacts with the `canvas` element. I created a `pointerLock.js` file and added the following code ```js let createPointerLock = function(scene) { let canvas = scene.getEngine().getRenderingCanvas(); canvas.addEventListener("click", event => { canvas.requestPointerLock = canvas.requestPointerLock || canvas.msRequestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock; if(canvas.requestPointerLock) { canvas.requestPointerLock(); } }, false); }; export { createPointerLock } ``` You might be curious why we are trying to access `requestPointerLock`, `msRequestPointerLock`, `mozRequestPointerLock`, and `webkitRequestPointerLock`. This is because browsers have slightly different implementations and it's not completely standardized yet. Inside of `main.js`, be sure to actually import the `createPointerLock` function and use it. I called the method right after I created the `scene`. ```js import { createPointerLock } from "./pointerLock.js" ... let scene = createGameScene(canvas, engine); createPointerLock(scene); ``` ## Adding Some Ground Players can't really move around if there is no ground. Let's create one! Babylon's `MeshBuilder` lets us do this really easily. Near the end of your `gameScene.js` file, create the `plane`: ```js let plane = BABYLON.MeshBuilder.CreatePlane("ground", { height: 20, width: 20 }, scene) plane.rotate(BABYLON.Axis.X, Math.PI / 2, BABYLON.Space.WORLD); plane.position.y = -2; ``` - I used `plane.rotate()` to rotate the plane so it is completely flat - `plane.position.y` decreases the altitude of the plane so it is below the `sphere` ## Gravity Creating gravity is pretty straightforward. To add gravity, we have to do three things. 1. Choose a Physics engine 2. Enable gravity on the `scene` 3. Enable gravity on the `sphere` (you can add it to other objects as well!) ### Choose a Physics Engine CannonJS is a physics engine well supported by Babylon. In our case, the physics engine will calculate where the sphere falls and how the sphere will interact with the `plane`. We want the `sphere` to bounce. So, let's add our physics engine: CannonJS ```html <script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script> ``` ### Enable gravity on the `scene` To enable gravity in the `scene`, we just have to set the `gravity` property to a vector. For the purposes of this tutorial, think of a vector as an arrow pointing in a direction. In this case, the arrow represents the force of gravity. The force of gravity is pointing down, at `-0.4` units. After creating gravity, then we enable CannonJS. I put the following code right after I created the `scene` variable (in `gameScene.js`) ```js scene.gravity = new BABYLON.Vector3(0, -.4, 0); scene.enablePhysics(scene.gravity, new BABYLON.CannonJSPlugin()); // Enable regular collisions as well scene.collisionsEnabled = true; scene.workerCollisions = true; ``` ### Enable gravity on the `sphere` After enabling gravity on the sphere, it doesn't look like it does anything. This is because the gravity "doesn't have anything to push". Indeed, we want gravity to make the sphere fall, but we have to tell CannonJS that. We're going to tell CannonJS that we want the `sphere` to fall by creating a physics imposter from it. A physics imposter is a simplified model of an object, or mesh. For example, we are simplifying the shape of our `sphere` mesh into a cube (`BABYLON.PhysicsImposter.BoxImposter`). The simpler shapes makes doing physics calculations a lot faster. If you've every played an action games, physics imposters are very similar to hitboxes. With that said, lets create a physics imposter the `sphere` and `plane`. ```js let spherePhysicsImposter = new BABYLON.PhysicsImpostor( sphere, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 1, friction: 0.1, restitution: .85 }, scene ); let planePhysicsImposter = new BABYLON.PhysicsImpostor( plane, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 0, friction: 0.1, restitution: .7 }, scene ); ``` - We set the mass of the `planePhysicsImposter` to `0` because we do not want the `plane` to fall - Feel free to play with the `mass`, `friction` and `restitution` values Below you can see the `sphere` bouncing on the `plane`. Pretty cool, eh? ![ball bouncing](https://i.imgur.com/DsRO24G.gif) View the code in [this repl](https://repl.it/@eankeen/babylon-tutorial-4). ### Walking So we have all the physics in place - wouldn't it be cool to interact with the scene directly? To do this, we can make the camera affected by gravity. Since we already have keyboard controls of the camera, we can simply *walk around*! ```js camera.ellipsoid = new BABYLON.Vector3(1.5, 0.5, 1.5); camera.checkCollisions = true; camera.applyGravity = true; plane.collisionsEnabled = true; plane.checkCollisions = true; ``` - We first create an ellipsoid around the camera. You can think of this like the hitbox. It less collide with objects like the `sphere`. The ellipsoid has a height of `0.5` units and has a length and width of `1.5` units - If you don't seem to be moving, you may want to check `camera.speed` If you want the `camera` to collide with the `sphere`, must enable collisions with the `sphere` ```js sphere.collisionsEnabled = true; sphere.checkCollisions = true; ``` As you can see, I can now walk around! ![walking around](https://i.imgur.com/cOM3zqc.gif) View the code in [this repl](https://repl.it/@eankeen/babylon-tutorial-5) ## 🚀 Now we have a player moving around the `scene`, with gravity enabled! We can collide with some meshes in the `scene`, like the `sphere`. How cool is that? Hopefully you found this BabylonJS introduction tutorial to be helpful! Tell me what you think and post a comment!
0
posted by eankeen (528) 26 days ago
10
How To Make A Puzzle Platformer In HTML5 And Javascript
# PUZZLE PLATFORMER TUTORIAL ## What You Will Make: By the end of this tutorial you should have a functioning puzzle platformer that allows the player to change gravity. It should allow you to change gravity when you jump and have 4 different blocks that all do different things: + Plain Solid Block (does nothing) + One Way Gates (only allows you to pass one way - vertical only) + Sticky Block (no gravity changing) + Anti-grav Block (changes your gravity) **End Result:** https://Puzzle-Platformer--bearbearmo.repl.co **The Code:** https://repl.it/@bearbearmo/Puzzle-Platformer --- ## Disclosure: This is likely not going to be the absolute best way to create a puzzle platformer, however I have done my best to make it simple but effective (which is very difficult). In this tutorial you will **not** learn basic JavaScript concepts. However if you wish to do so, I reccomend going to:https://www.w3schools.com/js/ It can show you almost everything, easily and conveniantly. Also, it will be best if you follow along using a **html + css + js repl** as it automatically sets up everything for you, files, boilerplate stuff and makes it easy for you to code your dreams. --- ## Beginning The Journey To start of with creating a puzzle platformer we have to do the html; go to your index.html file and between the ``` <body> ``` and ``` <script> ``` tags add a canvas element, like so: ```html <body> <canvas id="canvas" width="512" height="512"></canvas> <script src="script.js"></script> </body> ``` The id can be set to anything you want but for simplicity I will use "canvas". The ```width``` and ```height``` can be set to anything you like aslong as it is a multiple of 32 (512 is 16x32).And that is the only but most important html you will need to do. --- ## The First Steps The very first thing to do is to go to your **script.js** file on the **Left Hand Side**. The first few things will be the easiest parts of this tutorial so they will be bundled into 1. ### Defining Your Canvas To define our canvas we will use ```const``` instead of ```var``` or ```let``` because we do not want to change the value.\ To define the canvas we do : ```javascript const c = document.getElementById("canvas").getContext("2d"); ``` I have called the constant c so that it is short as I will be using it **a lot**. The ```getContext("2d")``` lets the program know to make the objects 2d (rather than 3d). ### Creating Your Level Creating your level is incredibley easy, all you have to do is create another constant ``` const ``` and call it level. However, this next part is important to follow or it will create real problems later on. You must make a string and fill it with (your canvas height divided by 32) rows of (your canvas width divided by 32) characters, creating (in my case) a 16 by 16 grid - fill it with "0"s but lines of "1" at the top and bottom and finaly add "bb^^PPvv" somewhere into the grid, like so: ```javascript const level = `1111111111111111 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000bb^^PPvv0000 0000000000000000 0000000000000000 0000000000000000 1111111111111111`; ``` ### Splitting Apart Your Nice New Level To use your new level we need to create our first function, this function needs to: + split each row apart + split each character of each row apart + return the resulting 2d array Luckily enough, javascript has two built in functions ``` split() ``` and ``` map() ``` we are going to utilise both of these to split our level. Like so: ```javascript function parseLevel(lvl) { const toRows = lvl.split("\n"); const toColumns = toRows.map(r => r.split("")); return toColumns; } ``` Putting everything so far together we get this: ```javascript const c = document.getElementById("canvas").getContext("2d"); const level = `1111111111111111 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000bb^^PPvv0000 0000000000000000 0000000000000000 0000000000000000 1111111111111111`; function parseLevel(lvl) { const toRows = lvl.split("\n"); const toColumns = toRows.map(r => r.split("")); return toColumns; } // if you want to see the output of the function do: console.log(parselevel(level)); ``` --- ## Running The Program Now we have a program it would be usefull if we could run it; to do this we just add 2 new functions : ```javascript let currentLevel; // put under the level constant function main() { } //keep at the bottom of the file window.onload = window.onload = function () { currentLevel = parseLevel(level); main(); } ``` The first function is empty but will be filled up later on. The second function is activated when the window is loaded, it activates the entirety of the code. I'm also sure you noticed the ``` let currentlevel; ``` at the top of the extract, this new variable is needs to be introduced in the global domain, this is because we need to use it in many different functions. ```let``` allows us to change the value like a normal variable but dissallows the variable to be used before it is defined, stopping bugs that may arrise later on. The semi-colon allows us to define a new variable but without giving it a value until we are ready. --- ## Getting To See Your Hard Work Now we have our level split up and ready to utilate, we have to utilate it. Obviosly we want to draw it, so it is important to get familiar with ```c.fillStyle``` and ```c.fillRect(x, y, w, h)``` . ```c.fillStyle``` allows you to change the colour of the rectangles you fill. ```c.fillrect(x, y, w, h)``` draws a rectangle onto the canvas in the colour defined in ```c.fillStyle``` the brackets contain the x coordinate, ycoordinate, width, height. Now we know how to draw the level, we can create a function to do so. We are going to have to draw a block at a time and so we will need a for loop, however because our array is 2d we will need two for loops: ```javascript function draw() { for (let row = 0; row < currentLevel.length; row++) { for (let col = 0; col < currentLevel[0].length; col++) { } } } ``` This allows us to access each blocks coordinates seperately, and so we can now easily, check the type of block("1","p","b","^","v"), select the colour for that block("black","pink","blue","grey"&"grey")and draw that block with it's coordinates(x(colx32),y(rowx32)) and size(w(32),h(32)). shown here: ```javascript function draw() { for (let row = 0; row < currentLevel.length; row++) { for (let col = 0; col < currentLevel[0].length; col++) { if (currentLevel[row][col] === "1") { c.fillStyle = "black"; c.fillRect(col * 32, row * 32, 32, 32); } if (currentLevel[row][col] === "v") { c.fillStyle = "grey" c.fillRect(col * 32, (row * 32) + 16, 32, 16); } if (currentLevel[row][col] === "^") { c.fillStyle = "grey" c.fillRect(col * 32, row * 32, 32, 16); } if (currentLevel[row][col] === "P") { c.fillStyle = "pink" c.fillRect(col * 32, row * 32, 32, 32); } if (currentLevel[row][col] === "b") { c.fillStyle = "blue" c.fillRect(col * 32, row * 32, 32, 32); } } } } ``` **note:** It is worth mentoining now that the "^" blocks are one way gates upwards and the "v" blocks are one way gates downwards. This means they have to be half the height of a normal block (16) and the "v" needs to be on the bottom half of the block so has 16(half a block) added to its y coordinate. To see the output of our draw function we just need to add ```draw();``` to our ```main()``` function, and press run. The output should be a picture of your levels constant. --- ## Acting God Once we've got our level, we need to make a player. In this case a red square. This squares gonna need some associated values, for this we're going to create an object: ```javascript const player = { x: 0, y: 0, width: 32, height: 32, //used later on gpe: 0, yke: 0, mass: 64, speed: 3, gfldstr: 9.8 } ``` Whith the players coordinates and size we can easily draw it in the draw() function: ```javascript function draw(){ c.fillStyle = "red" c.fillRect(player.x, player.y, player.width, player.height) ...} ``` This draws in the square when we press run, however you cannot see it because it's coordinates are inside a block. To fix this we just need to add in a few more things. First of all in this example the player will be represented as "&", you need to write your symbol into the level constant(anywhere you like), for my example I'm placing it in the bottom left. Then you just need to write a simple if statement into the draw function, like so: ```javascript function draw() { c.fillStyle = "red"; c.fillRect(player.x, player.y, player.width, player.height); for (let row = 0; row < currentLevel.length; row++) { for (let col = 0; col < currentLevel[0].length; col++) { ... ... if (currentLevel[row][col] === "&") { player.x = col * 32 // sets x player.y = row * 32 // sets y currentLevel[row][col] = "0" // clears the initial position } } } } ``` You should now have a red square appear wherever you placed your "&". The easy part of this tutorial is now complete. This is also a good time to talk about how we are going to make the player move. Well were going to be changing the coordinates of the player and then drawing it again in it's new coordinates, however to do this were going to have to use something that runs the entire code on a loop. The easiest way to do that is run the function main() on a loop by using ```requestAnimationFrame(main);``` basicly this will run main() every frame that the game is running. However, this creates a problem whenever the player is redrawn the old image is still there. To fix this, at the start of the draw() function we just need to put ```c.clearRect(0, 0, canvas.width, canvas.height);``` this wipes everything off the screen before we draw it back just after, this is a little inefficient as the whole level is redrawn every frame. To make this better we **could** just clear the players previous coordinates, but this creates a whole new set of problems that I'm not going to deal with in this tutorial. Just take my word for it, this way is better. --- ## Gravity This is going to be the hardest part of the tutorial but keep whith it and you'll've made your own game. To create gravity we have to create a new function ```calcGPE(obj)``` (it isn't important that you know the physics involved, however a deeper understanding of the physics used here could be helpfull for knowing what I'm doing.) In this function we need to find the vertical kinetic energy (YKE) , to do this we need to find the gravitational potential energy (GPE); the formula for GPE is GPE = GMH (Gravitational Feild Strength (obj.gfldstr/1000000) x Mass (obj.mass) x Height ((512 - obj.height) - (obj.y / 32)). This formula allows us to find the GPE of any object. This means our final function is: ```javascript function calcGPE(obj) { return obj.mass * (obj.gfldstr / 1000000) * ((512 - obj.height) - (obj.y / 32)); } ``` The feild strength is divided by 1000000 because normal the height is in metres but in this measurement is in pixels so we need to account for that by dividing the feild strength by 1 million. To make gravity act upon our player we need to create **another function**, ``` gravity(obj)``` inside this new function we need to do 3 things take the kinetic energy from the y coordinate, take the gpe from the kinetic enrgy, find the gpe: ```javascript function gravity(obj) { obj.y -= obj.yke; obj.yke -= obj.gpe; obj.gpe = calcGPE(obj); } ``` Now, if we add ```gravity(player);``` to the main function our player should fall out of the sky.\ --- ## Vertical Collisions We want our player to **collide** with the blocks and **stop**, to do this we need to check the block bellow our player, check what kind of block it is and then stop the block if neccesary. However, we don't yet have an easy way of checking what a block is, so we create a new function, getTile(x, y): ```javascript function getTile(x, y) { //checks the block your looking for is inside the level if (x < currentLevel.length * 64 && x > 0 && y < currentLevel[0].length * 32 && y > 0) { return currentLevel[Math.floor(y / 32)][Math.floor(x / 32)];//returns the block } } ``` Now we have that, we can easily check the block at whatever coordinates we want.\ To check the block under us is one we want solid, we just need a simple if statement: ```javascript if (getTile(obj.x + 32, (obj.y + 32)) === "1" || getTile(obj.x, (obj.y + 32)) === "1") { } ``` To stop the player we need to set it's yke to 0 and round its y coordinate to the nearest 32: ```javascript if (getTile(obj.x + 32, (obj.y + 32)) === "1" || getTile(obj.x, (obj.y + 32)) === "1") { if (obj.yke <= 0) { obj.yke = 0; obj.y -= (obj.y % 32); } } ``` The if statement just allows the player to leave the block again when we move up. Now put this into the gravity function, make sure it's under everything allready in there.\ We can use this function again for the other blocks (apart from "v" because it allows us to move through it when coming from above.): ```javascript function gravity(obj) { ... ... // has the "^" included because we want it to do exactly the same thing for that block if (getTile(obj.x + 32, (obj.y + 32)) === "1" || getTile(obj.x, (obj.y + 32)) === "1" || getTile(obj.x + 32, (obj.y + 32)) === "^" || getTile(obj.x, (obj.y + 32)) === "^") { if (obj.yke <= 0) { obj.yke = 0; obj.y -= (obj.y % 32); } } else if (getTile(obj.x + 32, obj.y + 32) === "P" || getTile(obj.x, obj.y + 32) === "P") { obj.yke = 0; obj.y -= (obj.y % 32); // no if statement because we don't want the player to leave this block vertically (change gravity) } else if (getTile(obj.x + 32, obj.y + 32) === "b" || getTile(obj.x, obj.y + 32) === "b") { if (obj.yke <= 0) { obj.yke = 0; obj.y -= (obj.y % 32); } obj.gfldstr *= -1 // changes gravity } } ``` But of course being able to change gravity means that we need the blocks to be solid when we move upwards as well This is easy to implement we just need to change the y coordinates that we check from y + 32 to just y. We also need to slow down the player before it meets the block to prevent it clipping in, as the previous rounnding of the coordinates does not work in this situation. This means we need to add another 3 if statements: ```javascript function gravity(obj) { ... ... ... ... else if (getTile(obj.x, obj.y) === "1" || getTile(obj.x + 32, obj.y) === "1" || getTile(obj.x, obj.y) === "v" || getTile(obj.x + 32, obj.y) === "v") { if (obj.yke > 0) { obj.y += obj.yke;// slows down the player obj.yke = 0; } } else if (getTile(obj.x, obj.y) === "P" || getTile(obj.x + 32, obj.y) === "P") { obj.yke = 0; // only has this bit because we don't mind if the player sinks into the block because it is meant to be goo anyway } else if (getTile(obj.x, obj.y) === "b" || getTile(obj.x + 32, obj.y) === "b") { if (obj.yke > 0) { obj.y += obj.yke; obj.yke = 0; } obj.gfldstr *= -1 } } ``` And thats our vertical collisions finnished, the hardest part is done! --- ## Getting Some exercise Now we ahve a solid world, it's about time we moved through it. To do this we need to addEventListeners: ```javascript addEventListener("keydown", function (event) { keysDown[event.keyCode] = true; }); addEventListener("keyup", function (event) { delete keysDown[event.keyCode]; }); ``` If you're not sure about how to use event listeners go Here:https://www.w3schools.com/js/js_htmldom_eventlistener.asp Otherwise, you may have noticed keysdone has not been defined, so at the very top of the file under ```let currentlevel;``` put a new object ```let keysDown = {};``` . The first event listener detects any keys currently pressed down and adds the keycode of that key. the second event listener detects any keys that have stopped being pressed down and removes the keycode of that key. This allows us to create a function that checks if the keys we want to be pressed down (w, a, s, d, spacebar) are pressed down, like so: ```javascript function input() { if (65 in keysDown) { // checks that there is no block in the way if (getTile((player.x - player.speed), player.y + 16) === "0") { player.x -= player.speed;// moves to the left } } if (68 in keysDown) { //checks that there is no block in the way if (getTile(((player.x + player.width) + player.speed), player.y + 16) === "0") { player.x += player.speed;// moves to the right } } //checks if the player is on the floor if (87 in keysDown && player.yke === 0) { player.gfldstr = -9.8;// changes gravity } //checks if the player is on the floor if (83 in keysDown && player.yke === 0) { player.gfldstr = 9.8;// changes gravity } // restarts the level if (32 in keysDown && player.yke === 0) { player.gfldstr = 9.8; currentLevel = parseLevel(level); } } ``` Now, The very last thing we need to do is add input(); to the main() function.\ Run the repl, and enjoy your new puzzle platformer. Remember you can create **whatever** levels you like and create as **many** levels as you like # Thank You Very Much I wish to thank you very much for making it to the end of my tutorial, I hope you enjoyed it. I also want to inform you that this is based off of **Lucadukeys Basic Platformer Tutorial**, but I did ask Lucadukey personally if I could use his bassis to create my own tutorial and we have agreed that any profit I might get from this (but probably not), he will get **50%**. I would also like to say that I did code this **myself** and that I started develpoment before Lucadukey posted his tutorial. If you have any doubts I'm sure he would be happy to reply to your comments. **Please Upvote** it would mean a lot, and once again, Thankyou very Much. created by: Bearbearmo.
5
posted by bearbearmo (100) 9 months ago
8
Coin Flipper with Sound using HTML and JavaScript
This tutorial will teach you how to make a very simple Coin Flipper using HTML and Javascript. This program is so simple to make, it can be completed in around 5-10 minutes at most. The first obvious step is to open a HTML/CSS/JavaScript Repl. Doing so will mean that about *90% or more* of the HTML is already done for us, meaning we only have to add two simple lines inside the body of the HTML file to add a couple of buttons. Before we do that, however, we want all needed files and folders to be in the right place. Now, again, the Repl will really already have this done for us, but personally I like to put the CSS and JavaScript file into a static folder, as that is how I was taught, though you shouldn't need it. If you choose to do this, simply add a folder called "static", move the CSS and JavaScript files into that folder and then make sure that the paths to both files is correct by adding "static/" before style.css in the href on line 7, as well as before script.js in the src of line 12. We also want to add a "sounds" folder to our Repl making sure it's in our static folder if we have one created. With that out of the way, let's get to the coding. We'll start with the HTML as it is by far the simplest. For the HTML, we simply need to add two button tags inside the body tag. One will flip the coin, while the other will remove the coin image, so we can flip it again. The code will look like this: ```html <button id='coin-flip'>Flip Coin</button> <button id='reset'>Reset</button> ``` It is important that each button has a unique id so that we can link these buttons to our JavaScript. Besides that, that's it for our HTML. We just need to add the JavaScript logic, so let's do that. We'll first add this line of code: ```js let coinFlipper = { 'coinSide': ['H', 'T'], } ``` This will allow the computer to pick whether the coin lands on heads or tails and will randomize it a little later on. For now, we want to add the sound next. Before we can do that, we need to get a sound file. I found a simple file called coinfilp.mp3, downloaded it, and dragged the file into the sounds folder on my Repl. We can now use a "let" function to "create" a new Audio variable and associate the sound file with it so we can use it later in the Flip Coin button's function. ```js let coinFlipSound = new Audio('static/sounds/coinflip.mp3') ``` Next we add two Event Listeners, so that the proper functions take place when we click either button. For this program, I named the functions "coinFlip" and "reset." ```js document.querySelector('#coin-flip').addEventListener('click', coinFlip); document.querySelector('#reset').addEventListener('click', reset); ``` We now can create the two functions for our buttons, as well as one we'll need to make the result random. First, the reset function. It's very simple and only serves one purpose, removing the coin image so that our screen doesn't become flooded with them if we flip the coin multiple times. This function is still tied to a button, remember, so we will have to click it before we flip a second time. ```js function reset() { document.querySelector('img').remove(); } ``` All this line of code does is look for an Image and removes the image it finds if it finds one. The next function we'll create is the one that will randomize the result. It'll end up looking like this. ```js function randomCoin() { let randomIndex = Math.floor(Math.random() * 2); return coinFlipper['coinSide'][randomIndex]; } ``` That first line allows the computer of either 0 and 1. We do this because arrays are indexed starting with the value of 0 for the first item, and then 1 for the second, and so on. By using Math.floor, we also make it so that the number is not a decimal and Math.random is how it picks the result. We then make sure that the value it chose is returned to us when we run the function later. Last but not least, the actual coin flip function. I'm going to go over the function line by line, as it is the longest one. It looks like this: ```js function coinFlip() { coinFlipSound.play(); let coin = randomCoin(); let coinImage = document.createElement('img'); if (coin === 'H') { coinImage.src = 'https://random-ize.com/coin-flip/us-half-dollar/us-half-dollar-front.jpg' setTimeout(function() { document.querySelector('body').appendChild(coinImage); }, 1000); }else if (coin === 'T') { coinImage.src = 'https://random-ize.com/coin-flip/us-half-dollar/us-half-dollar-back.jpg' setTimeout(function() { document.querySelector('body').appendChild(coinImage); }, 1000); } } ``` The first line simply plays back the audio we set earlier regardless of what the result is. The next line allows us to set our random function as a variable to use in an if statement, while the third allows us to add an image tag to our HTML when the button is clicked. Next is our if statement. It simply tells the computer that if our random result equals "H" or heads, to set the image source to a picture of a coin with heads side facing up. The images I used we're simply found online, with the image address used so that the computer can locate the image. If the result isn't heads, however, the else if statement kicks in and posts a picture of the tails side instead. To actually post the image, we are using this line of code, to tell the computer to put the image inside the body tag. ```js document.querySelector('body').appendChild(coinImage); ``` However, instead of posting just that line, you'll see it's inside of a setTimeout function. This allows us to prevent the picture from showing up, until the time we want it to show up (in milliseconds). The time of the audio clip is about a second, so having it wait 1000 ms does the trick for us. # **And that's it!** You could of course do more with this, like using CSS to style the buttons, make the image a certain size, or add a background. However, we were able to make this program with only *two* lines of HTML that we wrote ourselves, *three* JavaScript functions, and only *five* additional lines outside the functions themselves. Thank you so much for reading this tutorial, I hope you enjoyed it. If you have any sort of feedback on it, please leave it in the comments, I'd love to hear it. -Aloeb83
4
posted by Aloeb83 (40) 9 months ago
4
JavaScript Games Tutorial #3: the Canvas
# JavaScript Game Tutorial #3: the Canvas ## Introduction In this tutorial, we cover the basics of 2D graphics programming using the JavaScript Canvas object. We start with creating a canvas and displaying text, then move on to drawing lines and shapes, and finally show how to draw "pixies" -- 2D pixel arrays that form the basis of almost every 2D game. ## But First... Before we get into the good stuff, we'll make a small foray into the topic of "code cleanliness". If you read our earlier tutorials, you might recall we discused "scope". Scope is a complex topic, but it boils down to a simple idea: every program running on your computer reserves memory for its data. Some of that memory is visible to other programs. Other bits of memory are visible only to the programs themselves, and even within a program, some of the memory is visible only to certain parts of the program at certain times. The rules defining what memory is visible to which program(s) is called "scope". We don't need to worry about scope too much, but there is one thing we'd like to manage: we don't want the details of our programs to pollute the "global scope," which is the memory visible to all programs. When you are running in a web browser, the global scope is the window in which you are running. Windows are large objects with many variables and functions. Here's a peek at the just the first bits of a Window object: ![window01](https://storage.googleapis.com/replit/images/1551458091220_510bd3579b3e6ff4872ca46d303c3cfd.pn) Every time we execute the following commands: ```js var myVariable; var myFunction = function() { }; ``` we create a new object in the global scope. For instance, suppose we execute the command ```js var aardvark = 3; ``` and once again look at the Window object: ![window02](https://storage.googleapis.com/replit/images/1551458091366_63b6c019338ce2e02936c3ce702bbbed.pn) If you look carefully, you can see our 'aardvark' variable in the list of Window fields. Insert sad emoji here. To minimize our footprint in the global scope, we are going to put all our tutorial code inside an object. We'll call it the 'ct' object, short for "CanvasTutorial": ```js ct = { // Our code goes here. }; ``` As we cover more topics, we will continue to add on to this object as follows: ```js ct.newVariable = 7; ct.newFunction = myFunction() { // Code goes here... }; ``` Also note that, when you look in the tutorial code, you will see commands like: ```js this.ctxt = this.canvas.getContext('2d'); ``` The _this_ keyword means "whatever object I am currently in", which, for us, usually means the _ct_ object. Otherwise, this tutorial should look, walk, and smell just like the others we have done. Let's get to the good stuff! ## Creating a Canvas In JavaScript, the Canvas is an object that manages a region of memory that can be rendered to the screen as an image. The Canvas is an object with its own variables and functions to call. In order to draw to it, you need to obtain a "context" -- an object whose helper functions we'll use to draw 2D primitives. In order to see it, you need to add it to the document in view. Summarizing: 1) create a Canvas: ```js ct.myCanvas = document.createElement("canvas"); ct.myCanvas.width = 1024; // width in pixels ct.myCanvas.height = 768; // height in pixels ``` 2) obtain a context object: ```js ct.myContext = ct.myCanvas.getContext("2d"); ``` 3) add the new canvas to the document: ```js document.body.appendChild(ct.myCanvas); ``` Now, to draw to the canvas, you just call a method inside the context object. For example, to fill the canvas with a solid color: ```js ct.myContext.fillStyle = "black"; ct.myContext.fillRect(0, 0, ct.myCanvas.width, ct.myCanvas.height); ``` where 'fillStyle' is the color the canvas will use on all subsequent 'fill' commands, and 'fillRect' draws a rectangle with the top corner at x = 0 (the left side of the canvas) and y = 0 (the top of the canvas) with a width equal to the width of the canvas and the height equal to the height of the canvas. *Important Point*: the left edge of the canvas has an x coordinate of 0, and increasing 'x' values move across the canvas to the right. The top edge of the canvas has a y coordinate of 0, and increasing 'y' values move down the canvas toward the bottom. If you check out the first half of _createCanvas.js_, you can see all these steps in action. ## Displaying Text Since this is a graphics tutorial, naturally, the first thing we will display is...text??? Well, yes, but the good news is we'll be displaying the text in a canvas-y way, rather than a writeln-y way as we did previously. To draw text to the canvas, we need to specify the font and color, then actually draw the text: ct.myContext.font = "20px Arial"; ct.myContext.fillStyle = "green"; ct.myContext.fillText("My Message", 10, 50); // 10 = x-position, 50 = y-position In the tutorial, we use _fillText_ to display a collection of menu items as follows: ```js // One text size to rule them all... textSize: 20, // An array that contains all our menu elements. menu: ["Draw a grid", "Draw rectangles", "Draw circles", "Draw to image", "Draw Pixie Forest", "Animate an image"], displayMenu: function(textColor) { // Set the text size and color (color is passed in to the function). this.ctxt.font = this.textSize + "px Arial"; this.ctxt.fillStyle = textColor; // Loop through all menu items, auto-generating each entrie's text and position. for (var i=0; i<this.menu.length; ++i) { var entryText = (i + 1) + ") " + this.menu[i]; var textPosY = (i + 1) * this.textSize * 1.25; this.ctxt.fillText(entryText, 10, textPosY); } } ``` ## Drawing Lines Let's draw lines. The 2D context provides 2 methods to make this easy: ```js ct.myContext.moveTo(50, 10); // 50 = x-position, 10 = y-position ct.myContext.lineTo(500, 10); // Draws a line to x=500, y=10 ``` The _moveTo_ method puts the 2D graphics cursor at the specified (x, y) position *without drawing anything*. The _lineTo_ method moves the 2D graphics cursor to a new (x, y) position, drawing a line between the previous position and the new one. Unfortunately, if you just execute these two commands, you won't see anything. For many commands like moveTo and lineTo, the Canvas expects you to move the graphics cursor several times, then draw all the lines in one go. In other words, it wants to process a whole batch of commands in one shot, which is often more efficient that drawing one line at a time. In order to process batches of commands (often called "batch commands"), the canvas provides two methods: ```js ct.myContext.beginPath(); // Tells the canvas that a series of commands is coming. ct.myContext.closePath(); // Tells the canvas that no more commands belong in this batch. ``` Then, to execute all the commands in the batch, the context provides: ```js ct.myContext.stroke(); // Draw all lines in the current batch. ct.myContext.fill(); // Fill all shapes in the current batch. ``` Note that shapes like circles and rectangles can be drawn with either 'stoke' or 'fill' commands, or both -- depending on whether you want just the border, just the interior, or an interior with a border. We'll see how this works in the next section. For now, we just want to draw grid lines across the screen. To do this, we use two loops: one to draw horizontal lines, and one to draw vertical lines. Here is the code (see grid.js): ```js ct.drawGrid = function() { this.fillWith("black"); // Fills the entire canvas with black. this.ctxt.strokeStyle = "red"; // Sets the color of strokes to red. // Start a new batch of lines. this.ctxt.beginPath(); // Create horizontal lines. for (var iRow=0; iRow <= this.canvas.height; iRow += 20) { this.ctxt.moveTo(0, iRow); this.ctxt.lineTo(this.canvas.width, iRow); } for (var iCol=0; iCol <= this.canvas.width; iCol += 20) { // Create vertical lines. this.ctxt.moveTo(iCol, 0); this.ctxt.lineTo(iCol, this.canvas.height); } // End the new batch of lines. this.ctxt.closePath(); // Render the lines. this.ctxt.stroke(); // Wait for the user to press a key, then return to the main menu. input.waitForAnyKey(); }; ``` *Important Point*: if you omit the _beginPath_ and _closePath_ calls, you may see strange artifacts in your graphics. For instance, you might see an errant diagonal line connecting the end of one grid line to the start of the next. _beginPath_ and _closePath_ help the 2D context to track where the graphics cursor is and when it should be moved without drawing versus moved while drawing. Make sure you wrap your batch commands correctly, or you may get unexpected results. ## Drawing Shapes It's easy to draw rectangles and arcs with the 2D canvas. Rectangles: ```js ct.myContext.fillRect(5, 25, 100, 40); // Creates and fills a rectangle with top left corner at (5, 25). // Width = 100, heigh = 40. ct.myContext.beginPath(); ct.myContext.rect(5, 25, 100, 40); // Defines, but doesn't draw, the same rectangle. ct.myContext.closePath(); ct.myContext.stroke(); // Draws the border of the rectangle (doesn't fill). ``` Arcs (partial circles): ```js ct.myContext.beginPath(); ct.myContext.arc(25, 40, 100, 0, 2.0 * Math.PI); // Defines, but doesn't draw, a full circle // centered at (25, 40) with radius 100 // (the last two arguments, 0 & 2.0 * Math.PI, // define the amount of arc to draw. Since 2Pi // == 360 degrees, this command defines the // whole circle). ct.myContext.closePath(); ct.myContext.fill(); // Fills the circle's interior. ct.myContext.stroke(); // Draws the circle's border. ``` For our tutorial, we randomly-generate a collection of rectangles and a collection of circles. The only thing we haven't seen in that code is the use of the _round_, _random_, and _min_ Math functions: _round_ rounds the input to the nearest whole number, _random_ generates a random decimal number between 0 and 1, and _min_ selects the smaller of two input numbers. Combining these functions allows us to create shapes of random sizes and random locations on the screen. Rectangles: ```js ct.drawRectangles = function() { this.fillWith("black"); for (var i=0; i<10; ++i) { var width = Math.round(this.canvas.width * Math.random()); var height = Math.round(this.canvas.height * Math.random()); var left = Math.floor((this.canvas.width - width) * Math.random()); var top = Math.floor((this.canvas.height - height) * Math.random()); var color = this.colors[Math.floor(this.colors.length * Math.random())]; this.ctxt.beginPath(); this.ctxt.fillStyle = color; this.ctxt.fillRect(left, top, width, height); this.ctxt.strokeStyle = "white"; this.ctxt.rect(left, top, width, height); this.ctxt.closePath(); this.ctxt.stroke(); } input.waitForAnyKey(); }; ``` Circles (Arcs): ```js ct.drawCircles = function() { this.fillWith("black"); for (var i=0; i<10; ++i) { var maxRadius = Math.min(this.canvas.width / 2, this.canvas.height / 2); var radius = Math.round(maxRadius * Math.random()); var x = radius + Math.floor((this.canvas.width - 2 * radius) * Math.random()); var y = radius + Math.floor((this.canvas.height - 2 * radius) * Math.random()); var color = this.colors[Math.floor(this.colors.length * Math.random())]; this.ctxt.beginPath(); this.ctxt.fillStyle = color; this.ctxt.arc(x, y, radius, 0, 2.0 * Math.PI); this.ctxt.fill(); this.ctxt.strokeStyle = "white"; this.ctxt.arc(x, y, radius, 0, 2.0 * Math.PI); this.ctxt.closePath(); this.ctxt.stroke(); } input.waitForAnyKey(); }; ``` ## Drawing Images The canvas also allows you do draw arbitrary images (as opposed to just shapes) to the screen. To do this, you create an offscreen canvas, draw into it, and then draw that canvas to the screen with the _drawImage_ context method: ```js // Create a canvas to hold a 100 pixel by 100 pixel image. ct.offscreenCanvas = document.createElement("canvas"); ct.offscreenCanvas.width = 100; ct.offscreenCanvas.height = 100; // Draw this image onto the visible canvas at position x=50, y=200. ct.myContext.drawImage(ct.offscreenCanvas, 50, 200); ``` That's all there is to it! In our tutorial, we'll draw a red box with a white cross into the offscreen canvas, then draw that canvas at a random position on the screen: ```js ct.drawToImage = function() { this.imageCanvas = document.createElement("canvas"); this.imageCanvas.width = 100; this.imageCanvas.height = 100; var imgCtxt = this.imageCanvas.getContext('2d'); imgCtxt.beginPath(); imgCtxt.fillStyle = "red"; imgCtxt.fillRect(0, 0, this.imageCanvas.width, this.imageCanvas.height); var stripeSize = Math.round(this.imageCanvas.width / 8); var x = this.imageCanvas.width / 2 - stripeSize / 2; var y = this.imageCanvas.height / 2 - stripeSize / 2; imgCtxt.fillStyle = "white"; imgCtxt.fillRect(x, 0, stripeSize, this.imageCanvas.height); imgCtxt.fillRect(0, y, this.imageCanvas.width, stripeSize); imgCtxt.closePath(); }; // ...copy the image to the canvas. ct.drawImageToCanvas = function() { this.fillWith("black"); // Draw the image at a random location on the screen. var x = Math.round((this.canvas.width - this.imageCanvas.width) * Math.random()); var y = Math.round((this.canvas.height - this.imageCanvas.height) * Math.random()); this.ctxt.drawImage(this.imageCanvas, x, y); input.waitForAnyKey(); }; ``` ## Drawing "Pixies" Way back in the 80's, the Commodore 64 and Atari home computers introduced the idea of the "sprite" -- a 2D grid of pixels rendered at high speed in the graphics hardware. Sprites made it fast and easy to render thigns like animated characters, and they often supported collision detection in the hardware, too. In the spirit of these early sprites, we introduce the "Pixie" -- a 2D array of pixels drawn via the 2D context. We accomplish this by creating a 2D array that represents the pixels, then looping through this array and drawing the pixels to an offscreen canvas. When we want to display the Pixie, we just draw its offscreen canvas to the visible canvas. Before we look at that code, we need to cover a few new concepts. First, when you define a JavaScript object like this: ```js var myObj = {a: 1, b: "cat", c: 3.14}; ``` we can access the fields like this: ```js console.log("The value of field 'a' is:"); console.log(myObj['a']); ``` This would print out: ```js The value of field 'a' is: 1 ``` Second, if you have defined a string like this: ```js var myString = "ABCD1234"; ``` You can access each character like this: ```js console.log("The 3rd character is:"); console.log(charAt(2)); // Remember: first character is at position 0 ``` which would produce the following output: ```js The 3rd character is: C ``` With these concepts in our pocket, let's take a look at our "Pixie" code: ```js ct.pixieCanvas = null; // Create an object that will map abbreviated colors to the full color name. ct.pixiePalette = { r: "red", g: "green", b: "blue", y: "yellow", o: "orange", p: "purple", w: "white", l: "black", n: "brown", a: "gray", }; // Create the array representing the pixel data. ct.pixieTreeData = [ ". . . . . . . . . . . . . . . . ", ". . . . . . . g g . . . . . . . ", ". . . . . . . g g . . . . . . . ", ". . . . . . g g g g . . . . . . ", ". . . . . . g g g g . . . . . . ", ". . . . . g g g g g g . . . . . ", ". . . . . g g g g g g . . . . . ", ". . . . g g g g g g g g . . . . ", ". . . . g g g g g g g g . . . . ", ". . . g g g g g g g g g g . . . ", ". . . g g g g g g g g g g . . . ", ". . g g g g g g g g g g g g . . ", ". . g g g g g n n g g g g g . . ", ". . . . . . . n n . . . . . . . ", ". . . . . . . n n . . . . . . . ", ". . . . . . . n n . . . . . . . ", ]; ct.initTreePixie = function() { this.pixieCanvas = this.makePixie(this.pixieTreeData, 2); }; // Draw the Pixie into its offscreen canvas by scanning // through the associated 2D array. ct.makePixie = function(data, scale) { var canvas = document.createElement("canvas"); canvas.width = data[0].length * scale; canvas.height = data.length * scale; var pixieCtxt = canvas.getContext('2d'); for (var iRow=0; iRow<data.length; ++iRow) { for (var iCol=0; iCol<data[iRow].length; iCol += 2) { // Get the character at the current point in the string. var colorCode = data[iRow].charAt(iCol); var color = null; // Check for the current character in the pallette object. if (this.pixiePalette.hasOwnProperty(colorCode)) { // If the pallette has this character, get the associated color. color = this.pixiePalette[colorCode]; } // If we found a valid color, draw a rectangle into the off-screen pallette. if (color) { pixieCtxt.beginPath(); pixieCtxt.fillStyle = color; pixieCtxt.fillRect(iCol / 2 * scale, iRow * scale, scale, scale); pixieCtxt.closePath(); } } } return canvas; }; ct.drawPixies = function() { this.fillWith("black"); for (var i=0; i<50; ++i) { var x = Math.round((this.canvas.width - this.pixieCanvas.width) * Math.random()); var y = Math.round((this.canvas.height - this.pixieCanvas.height) * Math.random()); this.ctxt.drawImage(this.pixieCanvas, x, y); } input.waitForAnyKey(); }; ``` ## Animation Now that we have working Pixies, we can animate them. The simplest way to do this is with the _setInterval_ method, which causes JavaScript to repeatedly call a target method at a time interval specified in milliseconds: ```js var myRepeatedFn = function() { console.log("Doin' it!"); } var myCallback = setInterval(myRepeatedFn, 100); ``` In the above example, JavaScript will print "Doin' it!" to the console every 100 milliseconds (or so), forever. To stop JavaScript from calling the function, use the _clearInterval_ command: ```js clearInterval(myCallback); ``` We will use _setInterval_ to call a method that switches back and forth between two frames of an animated helicopter Pixie. We store the frames in an array. Each time we call the 'animate' function, we change to the other frame, clear the screen, and draw the new Pixie: ```js // Animate pixies on the canvas ////////////////////////////////////////////// ct.copterCanvas = [null, null]; ct.animFrame = 0; ct.pixieCopterData01 = [ ". . . . . . . . w w w w w w w w ", ". . . . . . . . w w . . . . . . ", ". . . . . . . . w w w w w . . . ", ". . w . . . w w w w w w . w w . ", ". w w w w w w w w w w w . . w w ", "w . w w w w w w w w w w w w w w ", ". . . . . w w w w w w w w w w . ", ". . . . . . w w w w w w . . . . ", ". . . . . . . w . . w . w w . . ", ". . . . . w w w w w w w w . . . ", ]; ct.pixieCopterData02 = [ ". . w w w w w w w w . . . . . . ", ". . . . . . . . w w . . . . . . ", ". . . . . . . . w w w w w . . . ", "w . . . . . w w w w w w . w w . ", ". w w w w w w w w w w w . . w w ", ". . w w w w w w w w w w w w w w ", ". . . . . w w w w w w w w w w . ", ". . . . . . w w w w w w . . . . ", ". . . . . . . w . . w . w w . . ", ". . . . . w w w w w w w w . . . ", ]; ct.initCopterPixie = function() { this.copterCanvas[0] = this.makePixie(this.pixieCopterData01, 3); this.copterCanvas[1] = this.makePixie(this.pixieCopterData02, 3); }; ct.startAnimation = function() { this.animFrame = 0; this.doAnimation(); this.animCallback = setInterval(ct.doAnimation.bind(this), 67); input.waitForAnimStopKey(); }; ct.doAnimation = function() { var x = Math.round(this.canvas.width / 2 - this.copterCanvas[this.animFrame].width / 2); var y = Math.round(this.canvas.height / 2 - this.copterCanvas[this.animFrame].height / 2); this.fillWith("black"); this.ctxt.drawImage(this.copterCanvas[this.animFrame], x, y); this.animFrame += 1; this.animFrame %= this.copterCanvas.length; }; ``` Notice that we use the '%=' operator, which performs the _modulus_ operation on the frame number. Check out the 2nd tutorial in the series (Awari) for a discussion of that operation. ## Conclusion That's if for this tutorial! Hopefully, you now have a good feel for the basics of JavaScript 2D canvas programming. The Canvas can do much more, but even these basics give you the power to make good games. Go out there and see what you can do!
3
posted by MarkKreitler (33) 5 months ago
5
React: Meteorite landings web app
Link to tutorial [Meteorite landings React app tutorial](https://react-meteorite-landings-tutorial.freddiethefrog.repl.co/)
2
posted by freddiethefrog (5) 9 months ago