Simple small game production, only two or three hundred lines of code. The game can be extended by itself.
Source code has been released to Github, like a little star, source entry: game- Snake
The game has been released, the game entrance: Snake. Game. Yanjd.top
Step 1 – Create an idea
How the game will be implemented is the first thing to think about, and here are my thoughts:
- Use canvas to draw maps (grid).
- Using canvas to draw snake is occupying map grid. Make the snake move, i.e. update the snake coordinates and redraw.
- Create four direction buttons that control the next direction of the snake.
- Random fruit is drawn on the map, and snakes “eat” fruit as they move, increasing their length and “speed”.
- Start key and end key configuration, score display, history records
Step 2 – Frame selection
From the first step, I want to achieve this game, only need to use canvas drawing, no physics engine, no advanced UI effects. You can choose a simpler one to facilitate the operation of canvas drawing. EaselJS is selected after careful selection, which is lightweight and used to draw canvas and dynamic effect of canvas.
Step 3 – Development
To prepare
Directories and files:
| – index.html
| – js
| – | – main.js
| – css
| – | – stylesheet.css
Index.html imports the associated dependencies, as well as style files and script files. The design is that 80% of the screen height is canvas drawing area, 20% height is action bar and display score area.
<html lang="zh">
<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">
<title>snake</title>
<link rel="stylesheet" href="css/stylesheet.css">
<meta name="viewport"
content="Width =device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal- UI">
</head>
<body>
<div id="app">
<div class="content-canvas">
<canvas></canvas>
</div>
<div class="control">
</div>
</div>
<script src="https://cdn.bootcss.com/EaselJS/1.0.2/easeljs.min.js"></script>
<! Load jquery for DOM manipulation -->
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<! -- Sweetalert
<script src="https://cdn.bootcss.com/sweetalert/2.1.2/sweetalert.min.js"></script>
<script src="js/main.js"></script>
</body>
</html>
Copy the code
stylesheet.css
* {
padding: 0;
margin: 0;
}
body {
position: fixed;
width: 100%;
height: 100%;
}
#app {
max-width: 768px;
margin-left: auto;
margin-right: auto;
}
/* Canvas drawing area */
.content-canvas {
width: 100%;
max-width: 768px;
height: 80%;
position: fixed;
overflow: hidden;
}
.content-canvas canvas {
position: absolute;
width: 100%;
height: 100%;
}
/* Operation area */
.control {
position: fixed;
width: 100%;
max-width: 768px;
height: 20%;
bottom: 0;
background-color: #aeff5d;
}
Copy the code
main.js
$(function() {
// Main coding area
})
Copy the code
1. Draw a grid
Points to note (problems encountered and solutions) :
- The line drawn by canvas has no width, but the line has width. For example, if you draw a 10px line from (0, 0) to (0, 100), half of the line will be invisible outside the area. For example, draw a line with a width of 10px from (0, 0) to (0, 100). Instead, draw a line from (5,0) to (5,100) with an offset of half the line width.
- The width and height coordinate of canvas defined with the style will be stretched. The solution is to set the width and height attribute of canvas element, and the value is its current actual width and height.
code
main.js
$(function () {
var LINE_WIDTH = 1 // Line width
var LINE_MAX_NUM = 32 // The number of rows
var canvasHeight = $('canvas').height() // Get the canvas height
var canvasWidth = $('canvas').width() // Get the canvas width
var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // The width of the grid is calculated by 32 grids in a row
var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // Calculate the number of horizontal and vertical grids, i.e., the maximum abscissa and the maximum ordinate
/** * Draw a grid map * @param graphics */
function drawGrid(graphics) {
var wNum = num.w
var hNum = num.h
graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')
// Draw horizontal lines
for (var i = 0; i <= hNum; i++) {
if (i === hNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
if (i === 1) graphics.setStrokeStyle(0.1)
graphics.moveTo(LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
.lineTo(gridWidth * wNum + LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
}
graphics.setStrokeStyle(LINE_WIDTH)
// Draw vertical lines
for (i = 0; i <= wNum; i++) {
if (i === wNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
if (i === 1) graphics.setStrokeStyle(1.)
graphics.moveTo(i * gridWidth + LINE_WIDTH / 2, LINE_WIDTH / 2)
.lineTo(i * gridWidth + LINE_WIDTH / 2, gridWidth * hNum + LINE_WIDTH / 2)}}function init() {$('canvas').attr('width', canvasWidth) // Assign the width and height of the current canvas to the canvas property.
$('canvas').attr('height', canvasHeight)
var stage = new createjs.Stage($('canvas') [0])
var grid = new createjs.Shape()
drawGrid(grid.graphics)
stage.addChild(grid)
stage.update()
}
init()
})
Copy the code
rendering
When your browser opens index.html, you can see:
2. Draw a snake
The snake can be thought of as a series of coordinate points (arrays), adding new coordinates to the head of the array “as it moves” and removing the tail coordinates. It’s like a queue, first in, first out.
code
main.js
$(function () {
var LINE_WIDTH = 1 // Line width
var LINE_MAX_NUM = 32 // The number of rows
var SNAKE_START_POINT = [[0.3], [1.3], [2.3], [3.3]] // Initial snake coordinates
var DIR_ENUM = { UP: 1.DOWN: - 1.LEFT: 2.RIGHT: 2 - } // Enumerates the four directions of movement, the sum of the two opposing directions equals 0
var GAME_STATE_ENUM = { END: 1.READY: 2 } // Game state enumeration
var canvasHeight = $('canvas').height() // Get the canvas height
var canvasWidth = $('canvas').width() // Get the canvas width
var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // The width of the grid is calculated by 32 grids in a row
var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // Calculate the number of horizontal and vertical grids, i.e., the maximum abscissa and the maximum ordinate
var directionNow = null // The current movement direction
var directionNext = null // Next move direction
var gameState = null // Game state
/** * Draw a grid map * @param graphics */
function drawGrid(graphics) {
var wNum = num.w
var hNum = num.h
graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')
// Draw horizontal lines
for (var i = 0; i <= hNum; i++) {
if (i === hNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
if (i === 1) graphics.setStrokeStyle(0.1)
graphics.moveTo(LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
.lineTo(gridWidth * wNum + LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
}
graphics.setStrokeStyle(LINE_WIDTH)
// Draw vertical lines
for (i = 0; i <= wNum; i++) {
if (i === wNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
if (i === 1) graphics.setStrokeStyle(1.)
graphics.moveTo(i * gridWidth + LINE_WIDTH / 2, LINE_WIDTH / 2)
.lineTo(i * gridWidth + LINE_WIDTH / 2, gridWidth * hNum + LINE_WIDTH / 2)}}/** * coordinate class */
function Point(x, y) {
this.x = x
this.y = y
}
/** * get the next coordinate of the current coordinate based on the direction of movement * @param direction Direction of movement */
Point.prototype.nextPoint = function nextPoint(direction) {
debugger
var point = new Point(this.x, this.y)
switch (direction) {
case DIR_ENUM.UP:
point.y -= 1
break
case DIR_ENUM.DOWN:
point.y += 1
break
case DIR_ENUM.LEFT:
point.x -= 1
break
case DIR_ENUM.RIGHT:
point.x += 1
break
}
return point
}
Initialization / * * * * @ returns the coordinates of the snake {[Point, Point, Point, Point, Point... } * @private */
function initSnake() {
return SNAKE_START_POINT.map(function (item) {
return new Point(item[0], item[1])})}/** * Snake * @param graphics * @param snakes // Snake coordinates */
function drawSnake(graphics, snakes) {
graphics.clear()
graphics.beginFill("#a088ff")
var len = snakes.length
for (var i = 0; i < len; i++) {
if (i === len - 1) graphics.beginFill("#ff6ff9")
graphics.drawRect(
snakes[i].x * gridWidth + LINE_WIDTH / 2,
snakes[i].y * gridWidth + LINE_WIDTH / 2,
gridWidth, gridWidth)
}
}
/** * change the snake coordinate * @param direction */
function updateSnake(snakes, direction) {
var oldHead = snakes[snakes.length - 1]
var newHead = oldHead.nextPoint(direction)
// Out of bounds game over
if (newHead.x < 0 || newHead.x >= num.w || newHead.y < 0 || newHead.y >= num.h) {
gameState = GAME_STATE_ENUM.END
} else if (snakes.some(function (p) { // eat until your game is over
return newHead.x === p.x && newHead.y === p.y
})) {
gameState = GAME_STATE_ENUM.END
} else {
snakes.push(newHead)
snakes.shift()
}
}
/** * engine * @param graphics * @param snakes */
function move(graphics, snakes, stage) {
clearTimeout(window._engine) // Shut down the previous engine during restart
run()
function run() {
directionNow = directionNext
updateSnake(snakes, directionNow) // Update the snake coordinates
if (gameState === GAME_STATE_ENUM.END) {
end()
} else {
drawSnake(graphics, snakes)
stage.update()
window._engine = setTimeout(run, 500)}}}/** * Game end callback */
function end() {
console.log('Game over')}function init() {$('canvas').attr('width', canvasWidth) // Assign the width and height of the current canvas to the canvas property.
$('canvas').attr('height', canvasHeight)
directionNow = directionNext = DIR_ENUM.DOWN // Initializes the snake's movement direction
var snakes = initSnake()
var stage = new createjs.Stage($('canvas') [0])
var grid = new createjs.Shape()
var snake = new createjs.Shape()
drawGrid(grid.graphics) // Draw the grid
drawSnake(snake.graphics, snakes)
stage.addChild(grid)
stage.addChild(snake)
stage.update()
move(snake.graphics, snakes, stage)
}
init()
})
Copy the code
rendering
Rendering (GIF) :
3. Move the snake
Make 4 buttons to control the direction of movement
code
index.html
.<div class="control">
<div class="row">
<div class="btn">
<button id="UpBtn">on</button>
</div>
</div>
<div class="row clearfix">
<div class="btn half-width left">
<button id="LeftBtn">On the left</button>
</div>
<div class="btn half-width right">
<button id="RightBtn">right</button>
</div>
</div>
<div class="row">
<div class="btn">
<button id="DownBtn">Under the</button>
</div>
</div>
</div>
</div>.Copy the code
stylesheet.css
..control .row {
position: relative;
height: 33%;
text-align: center;
}
.control .btn {
box-sizing: border-box;
height: 100%;
padding: 4px;
}
.control button {
display: inline-block;
height: 100%;
background-color: white;
border: none;
padding: 3px 20px;
border-radius: 3px;
}
.half-width {
width: 50%;
}
.btn.left {
padding-right: 20px;
float: left;
text-align: right;
}
.btn.right {
padding-left: 20px;
float: right;
text-align: left;
}
.clearfix:after {
content: ' ';
display: block;
clear: both;
}
Copy the code
mian.js
./** * change the direction of the snake * @param dir */
function changeDirection(dir) {
/* In the same direction */
if (directionNow + dir === 0 || directionNow === dir) return
directionNext = dir
}
/** * bind the related element to the event */
function bindEvent() {$('#UpBtn').click(function () { changeDirection(DIR_ENUM.UP) })
$('#LeftBtn').click(function () { changeDirection(DIR_ENUM.LEFT) })
$('#RightBtn').click(function () { changeDirection(DIR_ENUM.RIGHT) })
$('#DownBtn').click(function () { changeDirection(DIR_ENUM.DOWN) })
}
function init() {
bindEvent()
...
}
Copy the code
rendering
Rendering (GIF) :
4. Paint the fruit
Randomly take two coordinate points to draw fruit, determine if “eat”, do not delete the tail. Shortening the timer interval increases the difficulty.
Points to note (problems encountered and solutions) : adding a new fruit cannot occupy the snake’s coordinates, the initial consideration is to generate a random coordinate, if the coordinates are already occupied, then continue to generate random coordinates. The problem with this, then, is that when the entire screen has two remaining coordinates available (in extreme cases, the snake takes up the entire screen only two squares away), it takes a lot of time to randomly pick up the last two coordinates. Later changed the method, first count all coordinates, and then cycle snake body coordinates, one by one to exclude the unusable coordinates, and then randomly select one of the available coordinates.
code
main.js
$(function () {
var LINE_WIDTH = 1 // Line width
var LINE_MAX_NUM = 32 // The number of rows
var SNAKE_START_POINT = [[0.3], [1.3], [2.3], [3.3]] // Initial snake coordinates
var DIR_ENUM = { UP: 1.DOWN: - 1.LEFT: 2.RIGHT: 2 - } // Enumerates the four directions of movement, the sum of the two opposing directions equals 0
var GAME_STATE_ENUM = { END: 1.READY: 2 } // Game state enumeration
var canvasHeight = $('canvas').height() // Get the canvas height
var canvasWidth = $('canvas').width() // Get the canvas width
var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // The width of the grid is calculated by 32 grids in a row
var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // Calculate the number of horizontal and vertical grids, i.e., the maximum abscissa and the maximum ordinate
var directionNow = null // The current movement direction
var directionNext = null // Next move direction
var gameState = null // Game state
var scope = 0 / / score
/** * Draw a grid map * @param graphics */
function drawGrid(graphics) {
var wNum = num.w
var hNum = num.h
graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')
// Draw horizontal lines
for (var i = 0; i <= hNum; i++) {
if (i === hNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
if (i === 1) graphics.setStrokeStyle(0.1)
graphics.moveTo(LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
.lineTo(gridWidth * wNum + LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
}
graphics.setStrokeStyle(LINE_WIDTH)
// Draw vertical lines
for (i = 0; i <= wNum; i++) {
if (i === wNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
if (i === 1) graphics.setStrokeStyle(1.)
graphics.moveTo(i * gridWidth + LINE_WIDTH / 2, LINE_WIDTH / 2)
.lineTo(i * gridWidth + LINE_WIDTH / 2, gridWidth * hNum + LINE_WIDTH / 2)}}/** * coordinate class */
function Point(x, y) {
this.x = x
this.y = y
}
/** * get the next coordinate of the current coordinate based on the direction of movement * @param direction Direction of movement */
Point.prototype.nextPoint = function nextPoint(direction) {
var point = new Point(this.x, this.y)
switch (direction) {
case DIR_ENUM.UP:
point.y -= 1
break
case DIR_ENUM.DOWN:
point.y += 1
break
case DIR_ENUM.LEFT:
point.x -= 1
break
case DIR_ENUM.RIGHT:
point.x += 1
break
}
return point
}
Initialization / * * * * @ returns the coordinates of the snake {[Point, Point, Point, Point, Point... } * @private */
function initSnake() {
return SNAKE_START_POINT.map(function (item) {
return new Point(item[0], item[1])})}/** * Snake * @param graphics * @param snakes // Snake coordinates */
function drawSnake(graphics, snakes) {
graphics.clear()
graphics.beginFill("#a088ff")
var len = snakes.length
for (var i = 0; i < len; i++) {
if (i === len - 1) graphics.beginFill("#ff6ff9")
graphics.drawRect(
snakes[i].x * gridWidth + LINE_WIDTH / 2,
snakes[i].y * gridWidth + LINE_WIDTH / 2,
gridWidth, gridWidth)
}
}
/** * change the snake coordinate * @param direction */
function updateSnake(snakes, fruits, direction, fruitGraphics) {
var oldHead = snakes[snakes.length - 1]
var newHead = oldHead.nextPoint(direction)
// Out of bounds game over
if (newHead.x < 0 || newHead.x >= num.w || newHead.y < 0 || newHead.y >= num.h) {
gameState = GAME_STATE_ENUM.END
} else if (snakes.some(function (p) { // eat until your game is over
return newHead.x === p.x && newHead.y === p.y
})) {
gameState = GAME_STATE_ENUM.END
} else if (fruits.some(function (p) { // Eat fruit
return newHead.x === p.x && newHead.y === p.y
})) {
scope++
snakes.push(newHead)
var temp = 0
fruits.forEach(function (p, i) {
if (newHead.x === p.x && newHead.y === p.y) {
temp = i
}
})
fruits.splice(temp, 1)
var newFruit = createFruit(snakes, fruits)
if (newFruit) {
fruits.push(newFruit)
drawFruit(fruitGraphics, fruits)
}
} else {
snakes.push(newHead)
snakes.shift()
}
}
/** * engine * @param graphics * @param snakes */
function move(snakeGraphics, fruitGraphics, snakes, fruits, stage) {
clearTimeout(window._engine) // Shut down the previous engine during restart
run()
function run() {
directionNow = directionNext
updateSnake(snakes, fruits, directionNow, fruitGraphics) // Update the snake coordinates
if (gameState === GAME_STATE_ENUM.END) {
end()
} else {
drawSnake(snakeGraphics, snakes)
stage.update()
window._engine = setTimeout(run, 500 * Math.pow(0.9, scope))
}
}
}
/** * Game end callback */
function end() {
console.log('Game over')}/** * change the direction of the snake * @param dir */
function changeDirection(dir) {
/* In the same direction */
if (directionNow + dir === 0 || directionNow === dir) return
directionNext = dir
}
/** * bind the related element to the event */
function bindEvent() {$('#UpBtn').click(function () { changeDirection(DIR_ENUM.UP) })
$('#LeftBtn').click(function () { changeDirection(DIR_ENUM.LEFT) })
$('#RightBtn').click(function () { changeDirection(DIR_ENUM.RIGHT) })
$('#DownBtn').click(function () { changeDirection(DIR_ENUM.DOWN) })
}
/** * Create fruit * @returns Point * @param fruits */
function createFruit(snakes, fruits) {
var totals = {}
for (var x = 0; x < num.w; x++) {
for (var y = 0; y < num.h; y++) {
totals[x + The '-' + y] = true
}
}
snakes.forEach(function (item) {
delete totals[item.x + The '-' + item.y]
})
fruits.forEach(function (item) {
delete totals[item.x + The '-' + item.y]
})
var keys = Object.keys(totals)
if (keys.length) {
var temp = Math.floor(keys.length * Math.random())
var key = keys[temp].split(The '-')
return new Point(Number(key[0]), Number(key[1))}else {
return null}}/** * Draw fruit * @param graphics * @param fruits fruit coordinate set */
function drawFruit(graphics, fruits) {
graphics.clear()
graphics.beginFill("#16ff16")
for (var i = 0; i < fruits.length; i++) {
graphics.drawRect(
fruits[i].x * gridWidth + LINE_WIDTH / 2,
fruits[i].y * gridWidth + LINE_WIDTH / 2,
gridWidth, gridWidth)
}
}
function init() {
bindEvent()
$('canvas').attr('width', canvasWidth) // Assign the width and height of the current canvas to the canvas property.
$('canvas').attr('height', canvasHeight)
directionNow = directionNext = DIR_ENUM.DOWN // Initializes the snake's movement direction
var snakes = initSnake()
var fruits = []
fruits.push(createFruit(snakes, fruits))
fruits.push(createFruit(snakes, fruits))
var stage = new createjs.Stage($('canvas') [0])
var grid = new createjs.Shape()
var snake = new createjs.Shape()
var fruit = new createjs.Shape()
drawGrid(grid.graphics) // Draw the grid
drawSnake(snake.graphics, snakes)
drawFruit(fruit.graphics, fruits)
stage.addChild(grid)
stage.addChild(snake)
stage.addChild(fruit)
stage.update()
move(snake.graphics, fruit.graphics, snakes, fruits, stage)
}
init()
})
Copy the code
rendering
Rendering (GIF) :
5. Score display, game end prompt, leaderboard
This part is relatively easy, dealing with the presentation of the data. I’m not going to show you this part of the code.
rendering
conclusion
The interface is relatively rough, mainly learning logic operation. There were a few small problems, but they all worked out. Createjs is a relatively simple game engine to learn, using only the graphics API.