Professional Documents
Culture Documents
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
DEV.OPERA
Log in Add-ons TV Web Mobile Labs
Introduction
This is my second article about creating Wolfenstein-like games with JavaScript, DOM and HTML 5 canvas; the techniques discussed are similar to those used in my WolfenFlickr project. In the first article, I created a basic map for the player to walk around in and a pseudo-3D rendering of the game world using raycasting techniques. In this article I'm first going to improve on the codebase I've already built, optimizing the rendering process to gain better performance and making the collision detection between the player and the walls better. In the second half, I'll implement static sprites to give the castle a bit of atmosphere and finally add an enemy or two. The final game example looks like this:
The full (MIT licensed) sample code is available for download here.
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
1/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
Optimizing
Without further ado, let's get on with optimizing the existing code base.
var lastGameCycleTime = 0; var gameCycleDelay = 1000 / 30; // aim for 30 fps for game logic function gameCycle() { var now = new Date().getTime(); // time since last game logic var timeDelta = now - lastGameCycleTime; move(timeDelta); var cycleDelay = gameCycleDelay; // the timer will likely not run that fast due to the rendering cycle hogging the cpu // so figure out how much time was lost since last cycle if (timeDelta > cycleDelay) { cycleDelay = Math.max(1, cycleDelay - (timeDelta - cycleDela y)) } lastGameCycleTime = now; setTimeout(gameCycle, cycleDelay); }
In the gameCycle function, I compensate for the lag introduced by the rendering functions by comparing the time since gameCycle was last called to the ideal gameCycleDelay time. I then adjust the delay for the next setTimeout call accordingly. This time difference is now also used when calling the move function (the one taking care of moving our player).
function move(timeDelta) { // time timeDelta has passed since we moved last time. We should have moved after time game CycleDelay, // so calculate how much we should multiply our movement to ensure game speed is constant var mul = timeDelta / gameCycleDelay; var moveStep = mul * player.speed * player.moveSpeed; // player will mo ve this far along the current direction vector
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
2/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
player.rotDeg += mul * player.dir * player.rotSpeed; // add rotation if pla yer is rotating (player.dir != 0) player.rotDeg %= 360; var snap = (player.rotDeg+360) % 90 if (snap < 2 || snap > 88) { player.rotDeg = Math.round(player.rotDeg / 90) * 90; } player.rot = player.rotDeg * Math.PI / 180; ... }
I can now use the timeDelta time to determine how much time has passed compared to how much should have passed. If you multiply movement and rotation by this factor, the player will move at a steady speed even if the game isn't running at a perfect 30 fps. Note that one drawback of this approach is that if there's enough lag, there is a risk that the player will be able to walk through a wall, unless we either get better collision detection or change the gameCycle so move is called several times, chipping away at the lag. Since the gameCycle function now only takes care of game logic (for now, only moving the player), a new renderCycle function has been made with the same time managing measures. Check the sample code to see this function.
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
3/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
var strip = dc("img"); strip.style.position = "absolute"; strip.style.height = "0px"; strip.style.left = strip.style.top = "0px"; if (useSingleTexture) { strip.src = (window.opera ? "walls_19color.png" : "w alls.png"); } strip.oldStyles = { left : 0, top : 0, width : 0, height : 0, clip : "", src : "" }; screenStrips.push(strip); screen.appendChild(strip); } }
You can see how only one DOM element (an img) is created per strip and how I'm creating a pseudo-style object to store the current values. Next I'll modify the castSingleRay function to work with these new strip objects. In order to use CSS clipping instead of div masking, you don't actually have to change any of the values; they're just used to set different style properties. Instead of creating a rectangular mask using the div, I now use the clip property to create a clipping mask. The image now needs to be positioned relative to the screen instead of relative to the containing div, so I'll simply add what used to be the div position to the position of the image. The position and dimensions of the div are then used to define the clipping rectangle. In the code below you can also see how the new values are checked against the oldStyles values before touching the actual element styles.
function castSingleRay(rayAngle, stripIdx) { ... if (dist) { ... var styleHeight; if (useSingleTexture) { // then adjust the top placement according to which wall texture we need
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
4/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
imgTop = Math.floor(height * (wallType-1)); var styleHeight = Math.floor(height * numTextures); } else { var styleSrc = wallTextures[wallType-1]; if (strip.oldStyles.src != styleSrc) { strip.src = styleSrc; strip.oldStyles.src = styleSrc } var styleHeight = height; } if (strip.oldStyles.height != styleHeight) { strip.style.height = styleHeight + "px"; strip.oldStyles.height = styleHeight } var texX = Math.round(textureX*width); if (texX > width - stripWidth) texX = width - stripWidth; var styleWidth = Math.floor(width*2); if (strip.oldStyles.width != styleWidth) { strip.style.width = styleWidth +"px"; strip.oldStyles.width = styleWidth; } var styleTop = top - imgTop; if (strip.oldStyles.top != styleTop) { strip.style.top = styleTop + "px"; strip.oldStyles.top = styleTop; } var styleLeft = stripIdx*stripWidth - texX; if (strip.oldStyles.left != styleLeft) { strip.style.left = styleLeft + "px"; strip.oldStyles.left = styleLeft; } var styleClip = "rect(" + imgTop + ", " + (texX + stripWidth + ", " + (imgTop + height) + ", " + texX + ")"; if (strip.oldStyles.clip != styleClip) { strip.style.clip = styleClip; strip.oldStyles.clip = styleClip; } ... } ... }
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
5/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
Try the new optimized demo now. You should get a much smoother experience compared to the earlier version: Demo 1 - Improved performance.
Collision detection
Let's take a look at the collision detection now. In the first article I solved the problem by simply stopping the player if he moved into a wall. While this does make sure you can't walk through walls it doesn't feel very elegant. First of all, it would be nice to keep a bit of distance between the player and the walls, otherwise you can move so close that the textures get super-stretched, which doesn't look very nice. Secondly, we should be able to slide along the walls instead of coming to a dead stop every time you so much as touch a wall. To solve the distance problem we have to think of something other than simply checking the player position against the map. One solution is to just think of the player as a circle and the walls as line segments. By making sure the circle doesn't intersect any of the line segments, the player will always be kept at a distance of at least the radius of this circle. Fortunately the map is restricted to the simple grid-based layout, so our calculations can be kept quite simple. Specifically, I just have to make sure that the distance between the player and the closest point on each surrounding wall is equal to or greater than the radius, and since the walls are all horizontal or vertical due to their alignment on the grid, the distance calculation becomes trivial. So, I'll replace the old isBlocking function with a new checkCollision function. Instead of returning a true/false value indicating whether or not the player can move to the desired position, this function returns the new adjusted position. The isBlocking function is still used inside the checkCollision function to check whether or not a certain tile is solid or not.
function checkCollision(fromX, fromY, toX, toY, radius) { var pos = { x : fromX, y : fromY }; if (toY < 0 || toY >= mapHeight || toX < 0 || toX >= mapWidth) return pos pos; var blockX = Math.floor(toX); var blockY = Math.floor(toY); if (isBlocking(blockX,blockY)) { return pos pos; } pos x = toX; pos.x pos y = toY; pos.y var var var var blockTop = isBlocking(blockX,blockY-1); blockBottom = isBlocking(blockX,blockY+1); blockLeft = isBlocking(blockX-1,blockY); blockRight = isBlocking(blockX+1,blockY);
if (blockTop != 0 && toY - blockY < radius) { toY = pos y = blockY + radius; pos.y
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
6/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
} ... // .. do the same for right, left and bottom tiles // is tile to the top-left a wall if (isBlocking(blockX-1,blockY-1) != 0 && !(blockTop != 0 && blockLe ft != 0)) { var dx = toX - blockX; var dy = toY - blockY; if (dx*dx+dy*dy < radius*radius) { if (dx*dx > dy*dy) toX = pos x = blockX + radius; pos.x else toY = pos y = blockY + radius; pos.y } } // .. do the same for top-right, bottom-left and bottom right tiles ... return pos pos; }
The player can now smoothly slide along the walls and will retain a minimum distance between them and the walls, keeping both performance and visual quality reasonable even when close to the walls. Try out the new wall collision code: Demo 2 - Wall collision.
Sprites
With that out of the way, let's turn to adding a bit of detail to the world. So far it's just been open space and walls, so it's about time we got some interior decorating done. I'll be using the sprite images shown below:
First I'll define the available item types. This can be done with a simple array of objects containing two pieces of info, the path to the image and a boolean value that defines whether or not this item type blocks the player from going through it.
= : : : :
[ "sprites/tablechairs.png", block : true }, "sprites/armor.png", block : true }, "sprites/plantgreen.png", block : true }, "sprites/lamp.png", block : false }
// 0 // 1 // 2 // 3
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
7/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
Then I'll place a few of these around the map. Again the data structure is an array of simple objects.
var mapItems = [ // lamps in center area {type:3, x:10, y:7}, {type:3, x:15, y:7}, // lamps in bottom corridor {type:3, x:5, y:22}, {type:3, x:12, y:22}, {type:3, x:19, y:22}, // tables in long bottom room {type:0, x:10, y:18}, {type:0, x:15, y:18}, // lamps in long bottom room {type:3, x:8, y:18}, {type:3, x:17, y:18} ];
I've added a few lamps around the castle and set up a dining room at the bottom of the map. In the zip file linked in the beginning of the article, you will also find sprites for a plant and a suit of armour for you play around with, if you so wish. Now I'll create an initSprites function to be called from the init function along with initScreen and the other initialization code. This function creates a two-dimensional array corresponding to the map and fills it with the sprite objects defined above in the mapItems array. The sprite objects are also given a few extra properties: its img element, a visible flag and the blocking information mentioned earlier.
var spriteMap; function initSprites() { spriteMap = []; for (var y=0;y<map.length;y++) { var spriteMap[y] = []; } var screen = $("screen"); for (var i=0;i<mapItems.length;i++) { var var sprite = mapItems[i]; var itemType = itemTypes[sprite.type]; var img = dc("img"); img.src = itemType.img; img.style.display = "none"; img.style.position = "absolute"; sprite.visible = false; sprite.block = itemType.block; sprite.img = img; spriteMap[sprite.y][sprite.x] = sprite; screen.appendChild(img); } }
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
8/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
So now I can do a simple spriteMap[y][x] lookup anywhere on the map and check if there's a sprite in that tile. As you can see in the code above, I've added all the img elements as children of the screen element. The trick now is to determine which ones are visible and where on the screen they should go. To do so, I'll tap into the raycasting function castSingleRay:
var visibleSprites = []; function castSingleRay(rayAngle, stripIdx) { ... while (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) { var wallX = Math.floor(x + (right ? 0 : -1)); var wallY = Math.floor(y); // new sprite checking code if (spriteMap[wallY][wallX] && !spriteMap[wallY][wallX].visi ble) { spriteMap[wallY][wallX].visible = true; visibleSprites.push(spriteMap[wallY][wallX]); } ... ... }
As you might recall, this function is called once every frame for each of the vertical strips on the screen. When the rays are cast, it moves outward in steps that make sure it touches all the tiles that the ray goes through, so I can simply check against the sprite map at every step and check if there's a sprite there. If there is, the sprite's visibility is toggled (if we haven't done so already) and it is added to the visibleSprites array. This is of course done for both the horizontal and the vertical run. In the renderCycle I will now add two new calls, one to clear the list of visible sprites and one to render the newly marked visible sprites. The former is done before the raycasting and the latter is done after.
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
9/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
function clearSprites() { // clear the visible sprites array but keep a copy in oldVisibleSprites for later. // also mark all the sprites as not visible so they can be added to visibleSprites again during ra ycasting. oldVisibleSprites = []; for (var i=0;i<visibleSprites.length;i++) { var var sprite = visibleSprites[i]; oldVisibleSprites[i] = sprite; sprite.visible = false; } visibleSprites = []; }
And now, finally, I'll turn my attention to the actual rendering of the sprites. U'll loop through all the sprites found during the raycasting, ie the ones that are now in the visibleSprites array. For each visible sprite I first translate its position into the viewer space so I have its position relative to where the player is. Note that 0.5 is added to the x and y coordinates to get the center of the tile. Simply using the sprite's x and y would give us the top-left corner of the map tile. Knowing the player's current rotation angle, the rest is calculated with simple trigonometry.
function renderSprites() { for (var i=0;i<visibleSprites.length;i++) { var var sprite = visibleSprites[i]; var img = sprite.img; img.style.display = "block"; // translate position to viewer space var dx = sprite.x + 0.5 - player.x; var dy = sprite.y + 0.5 - player.y; // distance to sprite var dist = Math.sqrt(dx*dx + dy*dy); // sprite angle relative to viewing angle var spriteAngle = Math.atan2(dy, dx) - player.rot; // size of the sprite var size = viewDist / (Math.cos(spriteAngle) * dist); // x-position on screen var x = Math.tan(spriteAngle) * viewDist; img.style.left = (screenWidth/2 + x - size/2) + "px"; // y is constant since we keep all sprites at the same height and vertical position img.style.top = ((screenHeight-size)/2)+"px"; var dbx = sprite.x - player.x;
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
10/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
var dby = sprite.y - player.y; img.style.width = size + "px"; img.style.height = size + "px"; var blockDist = dbx*dbx + dby*dby; img.style.zIndex = -Math.floor(blockDist*1000); } // hide the sprites that are no longer visible for (var i=0;i<oldVisibleSprites.length;i++) { var var sprite = oldVisibleSprites[i]; if (visibleSprites.indexOf(sprite) < 0) { sprite.visible = false; sprite.img.style.display = "none"; } } }
Optionally, an approach similar to the one used in the raycasting with an oldStyles object can be implemented for sprites as well, possibly gaining a bit of extra performance. Anyway, now the sprites are placed correctly on the screen and only those that are in the player's view are shown. However, as seen in Figure 1 things are a bit messed up since I haven't dealt with the z-order of the elements on the screen yet.
Figure 1: Sprites with z-index issues. If we were actually drawing the walls and sprites pixel by pixel, we would have to sort these objects according to how far away there were and draw the most distant ones first to keep objects that should be occluded from rendering in front of closer objects. Fortunately the situation is much simpler as we are dealing with HTML elements. This means we have a powerful tool to solve this problem, the CSS zIndex property. I can simply set the zIndex property to a value proportional to the distance to the sprite or wall strip in question. Then the browser will take care of the rest and save us from having to do any sorting at all.
function renderSprites() { for (var i=0;i<visibleSprites.length;i++) { var ... var blockDist = dbx*dbx + dby*dby; img.style.zIndex = -Math.floor(blockDist*1000); }
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
11/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
} function castSingleRay(rayAngle, stripIdx) { ... if (dist) { ... var wallDist = dwx*dwx + dwy*dwy; strip.style.zIndex = -Math.floor(wallDist*1000); } }
And now the sprites and walls are layered in the correct order, as seen in Figure 2. Since a high zIndex means the DOM element will be displayed on top of lower-indexed elements we use the negative value of the distance. Since the distances are rather small numerically, we also multiply by 1000 (or some other high number) to get sufficiently different integer values.
Figure 2: Sprites and walls in z-index harmony. Finally, the isBlocking function is altered to also take blocking sprites into account, making sure you can't run through the tables.
function isBlocking(x,y) { ... if (spriteMap[iy][ix] && spriteMap[iy][ix].block) return true; return false; }
Enemies
So far our little castle has been quite safe, so how about we add a little excitement? To do this, the first thing I need to do is add a second kind of sprite, one that is capable of moving around the level in the same way as
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
12/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
the player. Figure 3 shows the enemy sprite image I'll be using (this is a set of CSS sprites - all one image):
Figure 3: Guard sprite with 13 states. I will define the enemy types and the locations of enemies on the map in much same way as I did with the static sprites. Each enemy type (so far there's just the one guard type) has a few properties such as movement speed, rotation speed and the total number of "states". The states correspond to each image in the sprite set above - so an enemy in state 0 is standing still while an enemy in state 10 is lying dead on the floor. In this article I'll only be using the first 5 states to make the guards chase us around the map. I'll save combat for another day.
var enemyTypes = [ { img : "guard.png", moveSpeed : 0.05, rotSpeed : 3, totalStates : 1 3 } ]; var mapEnemies = [ {type : 0, x : 17.5, y : 4.5}, {type : 0, x : 25.5, y : 16.5} ];
Next I'll need an initEnemies function, which will be called from init along with the rest. This function works a bit like the initSprites function I just made, but it is also different in a number of ways. Whereas the static sprites could all be tied to a specific tile on the map, the enemies are of course free to go whereever they want so we can't use the same two-dimensional map structure to store their locations. Instead I'll take the easy way out and simply keep all the enemies in a single array, even if this does mean I'll have to traverse this array on each frame to determine which ones to render. Since I won't be dealing with a lot of enemies (yet, at least) this shouldn't too big of a problem for now.
var enemies = []; function initEnemies() { var screen = $("screen"); for (var i=0;i<mapEnemies.length;i++) { var enemy = mapEnemies[i]; var type = enemyTypes[enemy.type]; var img = dc("img"); img.src = type.img; img.style.display = "none"; img.style.position = "absolute"; enemy.state = 0; enemy.rot = 0; enemy.dir = 0; enemy.speed = 0; enemy.moveSpeed = type.moveSpeed; enemy.rotSpeed = type.rotSpeed;
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
13/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
enemy.totalStates = type.totalStates; enemy.oldStyles = { left : 0, top : 0, width : 0, height : 0, clip : "", display : "none", zIndex : 0 }; enemy.img = img; enemies.push(enemy); screen.appendChild(img); } }
In the same way as I did for the sprites, I'll create an img element for each enemy and add some extra information to the enemy object. The next thing I need to do is create a renderEnemies function, which is called from renderCycle. The basic idea here is to loop through the enemies and determine if they're in front of us by looking at the relative angle between them and the direction we're looking in (we should actually be using the field-of-view for this). If they are, the code will render him in much the same way as the sprites are rendered. If they are not in front of us, our code simply hides the sprite images. See the code comments below for more details.
function renderEnemies() { for (var i=0;i<enemies.length;i++) { var var enemy = enemies[i]; var img = enemy.img; var dx = enemy.x - player.x; var dy = enemy.y - player.y; var angle = Math.atan2(dy, dx) - player.rot; // angle relative t o player direction if (angle < -Math.PI) angle += 2*Math.PI; // make angle from +/- P I if (angle >= Math.PI) angle -= 2*Math.PI; // is enemy in front of player? if (angle > -Math.PI*0.5 && angle < Math.PI*0.5) { var distSquared = dx*dx + dy*dy; var dist = Math.sqrt(distSquared); var size = viewDist / (Math.cos(angle) * dist); var x = Math.tan(angle) * viewDist; var style = img.style; var oldStyles = enemy.oldStyles; // height is equal to the sprite size if (size != oldStyles.height) { style.height = size + "px"; oldStyles.height = size; } // width is equal to the sprite size times the total number of states
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
14/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
var styleWidth = size * enemy.totalStates; if (styleWidth != oldStyles.width) { style.width = styleWidth + "px"; oldStyles.width = styleWidth; } // top position is halfway down the screen, minus half the sprite height var styleTop = ((screenHeight-size)/2); if (styleTop != oldStyles.top) { style.top = styleTop + "px"; oldStyles.top = styleTop; } // place at x position, adjusted for sprite size and the current sprite stat e var styleLeft = (screenWidth/2 + x - size/2 - size*e nemy.state); if (styleLeft != oldStyles.left) { style.left = styleLeft + "px"; oldStyles.left = styleLeft; } var styleZIndex = -(distSquared*1000)>>0; if (styleZIndex != oldStyles.zIndex) { style.zIndex = styleZIndex; oldStyles.zIndex = styleZIndex; } var styleDisplay = "block"; if (styleDisplay != oldStyles.display) { style.display = styleDisplay; oldStyles.display = styleDisplay; } var styleClip = "rect(0, " + (size*(enemy.state+1)) + ", " + size + ", " + (size*(enemy.state)) + ")"; if (styleClip != oldStyles.clip) { style.clip = styleClip; oldStyles.clip = styleClip; } } else { var styleDisplay = "none"; if (styleDisplay != enemy.oldStyles.display) { img.style.display = styleDisplay; enemy.oldStyles.display = styleDisplay; } } } }
As you can see, the oldStyles object is once again used to make sure the style properties are only set if
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
15/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
the values have actually changed. The x position on the screen is determined as if it were a static sprite, only now I'm taking into account the current state of the sprite. For example, if the current state is 3 (part of the walk cycle) the sprite image is positioned [3 * sprite_size] to the left. A CSS clipping rectangle then makes sure that only the current state is visible. So, that gives us a couple of enemies standing around, looking at us suspiciously but not doing much else, as shown in Figure 4.
Figure 4: The guards don't want to move yet. Time for some AI! Ok, intelligence might be stretching it, but let's see if we can't at least get them to move a bit. In the gameCycle I'll add a call to an ai function, which will take care of evaluating the enemy actions. Next I'll make a small modification to the move function. Until now, it's been tied to the player object so let's change it so it takes two arguments, the timeDelta I introduced earlier and a new entity, which is any object that has the properties needed to move it (ie x, y, moveSpeed, rot, etc). The move function is then modified to use this object instead of the player object and our call in gameCycle is changed accordingly. This means that I can now use the same function to move other things - like enemies.
Now for the actual ai function. For each enemy, I'll calculate the distance to the player and if it's above a certain value (I've used a distance of 4), the enemy will be made to chase the player. I'll do that by setting the enemy's rotation equal to the angle between him and the player and setting his speed to 1. Then I'll call the same move that I used to move the player, only now with the enemy object instead of the player, of course. The same collision rules and such will apply since the move doesn't care what we're moving.
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
16/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
var dx = player.x - enemy.x; var dy = player.y - enemy.y; // distance from enemy to to player var dist = Math.sqrt(dx*dx + dy*dy); // if distance is more than X, then enemy must chase player if (dist > 4) { var angle = Math.atan2(dy, dx); enemy.rotDeg = angle * 180 / Math.PI; enemy.rot = angle; enemy.speed = 1; var walkCycleTime = 1000; var numWalkSprites = 4; enemy.state = Math.floor((new Date() % walkCycleTime new ) / (walkCycleTime / numWalkSprites)) + 1; // if not, then stop. } else { enemy.state = 0; enemy.speed = 0; } move(enemies[i], timeDelta); } }
This is also where I set the state property used above in the renderEnemies function. If the enemy is not moving, the state is simply 0 (the "standing still" image). If the enemy is moving, then I'll make it cycle through states 1 through 4. By using the % (modulo) operator on the current time with the time for a complete walk cycle as the divisor, we've got a nice time-based walk cycle. And there we have it! As illustrated in Figure 5, the guards will now run after the player until they are within a certain distance. Admittedly, this is not the most advanced AI yet, but it's a start. Trying to trap them in corners makes for good fun, for a few minutes anyway!
Next time
Thanks for reading - I hope you've had fun so far. In the next article I will probably look at some of the following topics: Weapons / shooting. Now that we have enemies, we need a simple, efficient way of getting rid of them, and
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
17/18
12/23/11
Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2 - Dev.Opera
what better way to do that than with guns! Pickups (gold, ammo, etc). This would tie in with adding player stats like score and health. Interface / HUD. Once we have numbers, we need to display them somewhere. Sounds. The gunning down of enemies should be accompanied by delicious sounds. _ Previous articleCreating pseudo 3D games with HTML 5 canvas and raycasting: Part 1
This article is licensed under a Creative Commons Attribution, Non Commercial - Share Alike 2.5 license.
Comments
The forum archive of this article is still available on My Opera.
You must be logged in to write a comment. If you're not a registered member, please sign up.
Author: Jacob Seidelin Date: Friday, March 13, 2009 Tags: canvas html5 javascript raycasting sprites
dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/
18/18