Dinosaur game is a browser game embedded into Google Chrome browser creates as an Easter Egg by Chrome UX team in 2014.
It appears when a user is trying to browse internet without an internet connection. In this case you can play this game. Alternatively you can trigger it by going to chrome://dino or chrome://network-error/-106.
The idea of the game is very simple - the dinosaur (sometimes called as Lonely T-Rex) is running through a desert and should avoid obstacles like cacti by jumping or ducking.
I find this a very simple and fun game, so let's make own version of it in just 5 minutes.
We will use JavaScript and will draw everything on HTML canvas. As always this will be a simplified version and you can extend it further by yourself.
Link to Wikipedia: https://en.wikipedia.org/wiki/Dinosaur_Game
Here is a live coding session that shows how it can be done.
Duration: 4:57 (coding - 4:34)
Disclaimer: never use this code in production. It was created for fun.
Let's break down the solution and comment on some complex or interesting things.
0:14 - starting with HTML template. As usual it is very simple - just an empty canvas
element.
Next in script
block we get reference to the canvas and set it's size - 600 by 250 pixels.
This is quite frustrating by HTML canvas does not have a simple way how to draw a line between to points. Basically it's a 4 step process: begin drawing path, move the point to the line start, define where the line end is and finally draw it. As we are planning to draw some lines in this project I decided that it makes sense to introduce a helper function called line
that does exactly these 4 steps.
0:40 - now we need to think a bit how we will deal with our game objects. There are 2 types of game objects: the dinosaur and a cactus (an obstacle).
Both of these objects have some common properties (e.g. location and size) and should have similar functions (e.g. they can be drawn on the screen). We can call them sprites (despite Wikipedia define sprite as a bitmap that is integrated into bigger scene, we will call the whole game object as Sprite
).
The constructor of the Sprite
has 5 parameters: coordinates of top-left corner, dimension and draw function.
Also we are defining a couple of functions that Sprite
will have:
getX
and getY
- return coordinates of top-left corner. Having them as functions (especially getY
) will pay off at a later stagedraw
- logics to draw the sprite. It does a very interesting thing - it moves the canvass' (0, 0)
so that it corresponds to top-left corner of the sprite! This makes drawing a lot simplier as we don't need to think where our sprite is located - we just assume that we should start with (0, 0)
!
drawFn
function (remember that it was passed as constructor parameter) that does the actual drawing. Here we pass 4 parameters: graphical context, sprite's dimensions and the frame number. Why such strange parameters? Graphical context is clear - we need to draw somewhere. Dimensions - we need to know how big our image should be. Notice that we don't need x
and y
coordinates due to canvass' repositioning to (0, 0)
. And finally we have frame
as frame number - remember that the dyno is running and on each frame we need to draw slightly different image to simulate the running.
move
- moves the sprint by some dx
by X axis
1:23 - we need a sprite map for the dinosaur. It's not very hard to find one on the web.
For reference - this is the one we are using in this project.
Source - https://www.quora.com/How-would-I-deploy-a-game-like-Agar-io-or-Slither-io
Nice thing is that it has 6 frames each of them is 88 by 94 pixels. This means that we can have an easy approach how to understand which sub-image we should take.
Imagine that we are on a frame 123
right now. As there are total 6 frames that are cyclic, this means that we are intereted in frame number 123 % 6 = 3
(zero-based).
Then we should cut the sub-image of size 88 by 94 where top-left corner is (264, 0)
(where 264 = 88 * 3
).
This is exactly what is happening in drawDyno
function.
1:48 - now it's time for the game loop implementation.
The main game object will be dyno
- it is an instance of a Sprite
object with defined dimensions (dynoW
and dynoH
) and logics how to draw itself (using drawDyno
function).
The game loop will run every 50 milliseconds and will consist of following steps:
x
variable - it is how much the game field has moved (by X axis).
2:18 - let's draw a cactus.
To simplfy, it can be done by combining 3 shapes:
w
and h
variables - this makes the logics a bit more difficult to read. You can replace it with some specific numbers (e.g. w = 90
and h = 120
) to see how it looks.
2:42 - once we know how to draw a cactus we can add it to the game.
We define a cacti
array that will contain sprites for each cactus.
Pay attention to lastCactus
variable. It contains an x
coordinate of a cactus generated last. On each game loop cycle we can check if it's time to generate a new cactus or not.
Remember that we have x
that represents the offset for the game scene. If the last cactus is leaving the scene - it's time to generate a new cactus. We add some randomness here and we generate new cactus on some distance away from the dinosaur, basically on a distance from w/3
to 4/3 * w
.
You can check in the browser to see how it looks. Expected result is that the dyno is running and seeing new cacti approaching it.
For now T-Rex is just running though the cacti and we will fix it soon.
3:08 - to avoid ostacles the dinosaur should be able to jump over them.
The idea is when a user is pressing space key (with key code 32
) the dinosaur should jump. I will not go into details about physics of the jump - refer to other project about fireworks that explains all used formulae.
Basically jumpDy
function calculates how high the dinosaur should be at time t
after the jump has started.
If you recall, initially our Sprite
object had a function called getY
- now it comes really handly and we can enhance it and add a logics to offset the y
coordinate during the jump.
For this we maintain 2 additional variables: jumping
(if the jump is initiated) and jumpTime
(how long the jump is taking already). Once we call jump
function we start a new jump (and notice that we cannot jump while jumping). Then on each move, if we are jumping, we need to increase the jumpTime
. In case we see that jumpDy
returned a negative value - it means that the jump is over and the dyno has hit the ground after the jump.
After this step we can verify everything in the browser and see that the dyno can jump.
But nothing is happening when dinosaur hits a cactus. This will be our next and final task.
3:56 - to detect collisions between dinosaur and a cactus we can use multiple strategies, but as always we'll go with most simple approach.
Consider that both dinosaur and cactus sprites are rectangles. We can take a center point of the dyno's sprite and check if it is inside the dimensions of a cactus sprite. If yes - then we have a collision.
Refer to images below. Red dot indicates dyno sprite's center. Left and right situations are considered as collisions. The middle one is ok and dyno's center is not within cactuss' bounds.
This is not a perfect solution, but it is good enough for this game.
This logics is implemented in the Sprite
's hits
function - it checks that center point is between [x, x + w]
and [y, y + h]
And finally - if there is a cactus
for which dyno.hits(cactus)
then the game should be over. We'll do it in simple way - show an alert and just reload the browser.
This was the final step of the implementation and you can check the final result in the browser.
This was a fun and exciting project. I'm really happy how it all turned out. As usual, there are way how to improve the game and I will leave it to you as a homework:
Sources: https://github.com/5minute/dyno-game
See live results:https://jsfiddle.net/1rz7m2nc/