Angry Chickens
You may have heard about a game where you shoot birds from a slingshot to destroy targets.
Today let's create a simple version of this game with Easter chickens playing the main role and Easter bunnies being the victims.
You can play it with a mouse or touch devices. Try it at top of this page!
One line of code is an 'Easter Egg' (an undocumented feature) - can you find it?
The full code is below. Here's a recap:
[1-9] set up the HTML5 Canvas in the upper left corner of the screen
[10-15] listeners for mouse and touch events
[17-20] sounds and images. 'img' contains a wide background (a terrible modification of a photo of Easter Island by Bjørn Christian Tørrissen) and the sprite animation frames at the bottom.
[21-22] canvas dimensions
[23-24] coordinates of the slingshot
[25] size of the sprites (chickens and bunnies)
[26-30] auxiliary variables
[31-38] create empty target objects and add them to the 'targets' array. Each target has its own coordinates and status (alive: true or false)
[39-40] kick off the game and initiate the main animation loop
[42-77] This is the heart and soul of the game - the main loop:
[43] Clear the Canvas.
[44] Draw the background. We're copying a rectangle with dimensions equal to the Canvas dimensions. This way the player does not see the sprite animation frames at the bottom.
The scrolling effect is achieved using the screenOffset parameter. It basically determines the horizontal shift from the initial position. In each frame we're only copying the area of the wide background bitmap that starts at the offset value.
Click here to learn more about drawImage.
The game does not interact with the background at all, so you can just delete this line if it's too complicated.
[45] If the slingshot is stretched, we draw the (extremely crude) rubber strip from the top of the slingshot to the center of the chicken.
[51-58] If our chicken is already flying, we recalculate his coordinates as well as the screenOffset.
We're using a crazy formula in [54] as a substitute for a decent ballistic calculation. The position of the chicken is calculated as the point of release plus the force of the stretch at the time of release (divided by 10).
The y-coordinate is additionally increased by a square of the frame counter (divided by four) to reflect the increasing influence of gravity.
I came up with these arbitrary values by trial and error. Feel free to experiment with them to change the trajectory of our kamikaze.
[56] Increase the counter - it tells us how many frames elapsed since the release.
[57] If the chicken falls a certain distance below the screen, it's time for a new one.
[59] Draw the chicken - it is copied from the bottom of the source image. It has two animation frames, one for the flight phase and another one for everything else.
[60-62] Draw the target bunnies. They have two frames: alive and dead.
[63-64] If the bunny is alive, detect collision with the chicken.
[65-67] RIP bunny.
[68] If all bunnies have been taken care of, the game ends (in a manner that is rather abrupt and far from elegant) and the player wins. There is no way to lose, because nobody likes to lose, especially during Easter. But adding a limit of chickens would be a good exercise to make the game more interesting.
[75-76] display the current message in the upper left corner of the screen and wait for the next animation frame.
Now the event listeners.
The behavior of the game is driven by the 'mode' variable.
The game starts in 'ready' mode (awaiting player action). If the player drags the background, the mode switches to 'drag' (scrolling the screen).
If the player drags the chicken, the mode switches to 'stretch' (sling stretching). Once the chicken is released, we switch to 'fly' mode. After the flight, the cycle repeats itself.
The mouseDown [79-83] (for mouse) and touchStart [85-91] (for touchscreen devices) functions just capture the x and y coordinates of the event and pass it on to the 'start' function.
These events are triggered when the player clicks the mouse button or touches the screen but doesn't release it or move the finger/mouse yet.
The 'start' function takes those coordinates and changes the mode to 'stretch' (we'll be stretching the slingshot strip) if the chicken was clicked. Otherwise it sets the mode to 'drag' (we will be scrolling the screen left or right).
Analogically, the mouseMove [105-109] and touchMove [111-116] functions pass the event coordinates to the 'move' function.
This function in turn moves the chicken to the given coordinates if we're in the 'stretch' mode.
If we're in 'drag' mode, it calculates the distance between the first click/touch (dragStartX) and the current mouse position and adjusts the screen offset accordingly.
[90] and [115] prevent the standard page scrolling.
The 'up' function [128-139] is triggered when the touch or mouse button is released.
If we're in 'stretch' mode, it captures the coordinates of the release [133-134], and the force of the stretch (difference between the coordinates of the release and of the sling) which will be the basis of the coordinates in the 'fly' mode.
newChicken [141-148] resets the counters and coordinates for our next fowl.
newGame [150-160] resets the game and randomizes the coordinates of the bunnies.
Happy Easter!
<html> |
<body> |
<canvas id="myCanvas" width="800" height="390"></canvas> |
<script> |
let canvas = document.getElementById("myCanvas"); |
let context = canvas.getContext("2d"); |
context.font = "30px Arial"; |
context.lineWidth = 5; |
canvas.style = "position: absolute; left:0; top:0"; |
canvas.addEventListener('mousedown', mouseDown); |
canvas.addEventListener('mousemove', mouseMove); |
canvas.addEventListener('mouseup', up); |
canvas.addEventListener('touchmove', touchMove); |
canvas.addEventListener('touchstart', touchStart); |
canvas.addEventListener('touchend', up); |
|
let sound1 = new Audio('sling.mp3'); |
let sound2 = new Audio('kill.mp3'); |
let img = new Image(); |
img.src = 'spritesheet.png'; |
let canvasWidth = 800, |
canvasHeight = 390, |
slingX = 400, |
slingY = 200, |
size = 40, |
maxTargets = 5, |
targets = [], |
message = 'Drag the chicken to start'; |
let x, y, stretchX, stretchY, releaseX, releaseY, dragStartX, counter, screenOffset, startOffset, targetsRemaining, frame, mode; |
|
for (let i = 0; i < maxTargets; i++) { |
let target = { |
x: null, |
y: null, |
alive: null |
}; |
targets.push(target); |
} |
newGame(); |
window.requestAnimationFrame(loop); |
|
function loop() { |
context.clearRect(0, 0, canvasWidth, canvasHeight); |
context.drawImage(img, screenOffset, 0, canvasWidth, canvasHeight, 0, 0, canvasWidth, canvasHeight); |
if (mode == 'stretch') { |
context.beginPath(); |
context.moveTo(slingX - screenOffset, slingY); |
context.lineTo(x - screenOffset + size / 2, y + size / 2); |
context.stroke(); |
} |
if (mode == 'fly') { |
message = 'Bunnies remaining: ' + targetsRemaining; |
x = releaseX + stretchX * counter / 10; |
y = releaseY + stretchY * (counter / 10) + (counter / 4) * (counter / 4); |
screenOffset = screenOffset + stretchX / 10; |
counter++; |
if (y > canvasWidth) newChicken(); |
} |
context.drawImage(img, frame, canvasHeight, size, size, x - screenOffset, y, size, size); |
for (let i = 0; i < maxTargets; i++) { |
let target = targets[i]; |
context.drawImage(img, size * (2 + target.alive), canvasHeight, size, size, target.x - screenOffset, target.y, size, size); |
if (target.alive) { |
if (x + size > target.x && x < target.x + size && y + size > target.y && y < target.y + size) { |
target.alive = false; |
sound2.play(); |
targetsRemaining--; |
if (targetsRemaining == 0) { |
message = 'You win! Drag the chicken to start a new game.'; |
newGame(); |
} |
} |
} |
} |
context.fillText(message, 10, 50); |
window.requestAnimationFrame(loop); |
} |
|
function mouseDown(e) { |
let pointerX = e.clientX; |
let pointerY = e.clientY; |
start(pointerX, pointerY); |
} |
|
function touchStart(e) { |
var touchobj = e.changedTouches[0]; |
let pointerX = touchobj.clientX; |
let pointerY = touchobj.clientY; |
start(pointerX, pointerY); |
e.preventDefault(); |
} |
|
function start(pointerX, pointerY) { |
if (mode == 'ready') |
if (pointerX > x - screenOffset && pointerX < x - screenOffset + size && pointerY > y && pointerY < y + size) { |
mode = 'stretch'; |
} |
else { |
dragStartX = pointerX; |
mode = 'drag'; |
startOffset = screenOffset; |
} |
} |
|
function mouseMove(e) { |
let pointerX = e.clientX; |
let pointerY = e.clientY; |
move(pointerX, pointerY); |
} |
|
function touchMove(e) { |
var touchobj = e.changedTouches[0]; |
if (e.touches.length == 4) targets[0].alive = false; |
move(touchobj.clientX, touchobj.clientY); |
e.preventDefault(); |
} |
|
function move(pointerX, pointerY) { |
if (mode == 'stretch') { |
x = pointerX + screenOffset; |
y = pointerY; |
} |
if (mode == 'drag') { |
screenOffset = startOffset + dragStartX - pointerX; |
} |
} |
|
function up(e) { |
if (mode == 'stretch') { |
sound1.play() |
mode = 'fly'; |
frame = size; |
releaseX = x; |
releaseY = y; |
stretchX = (slingX - x); |
stretchY = (slingY - y); |
} |
if (mode == 'drag') mode = 'ready'; |
} |
|
function newChicken() { |
frame = 0; |
x = slingX; |
y = slingY; |
screenOffset = 0; |
counter = 0; |
mode = 'ready'; |
} |
|
function newGame() { |
targetsRemaining = maxTargets; |
for (let i = 0; i < maxTargets; i++) { |
targets[i] = { |
x: slingX * 2 + Math.random() * 2 * canvasWidth, |
y: Math.random() * (canvasHeight - size), |
alive: true |
}; |
} |
newChicken(); |
} |
</script> |
</body> |
</html> |