簡單的小遊戲製做,代碼量只有兩三百行。遊戲可自行擴展延申。css
源碼已發佈至github,喜歡的點個小星星,源碼入口:game-snakehtml
遊戲已發佈,遊戲入口:http://snake.game.yanjd.topjquery
遊戲如何實現是首要想的,這裏個人想法以下:git
從第一步可知,我想實現這個遊戲,只須要用到canvas繪製就能夠了,沒有物理引擎啥的,也沒有高級的UI特效。能夠選個簡單點的,用來方便操做canvas繪製。精挑細選後選的是EaselJS,比較輕量,用於繪製canvas,以及canvas的動態效果。github
目錄和文件準備:canvas
| - index.htmlapi
| - js數組
| - | - main.js瀏覽器
| - cssapp
| - | - stylesheet.css
index.html 導入相關的依賴,以及樣式文件和腳本文件。設計是屏幕80%高度爲canvas繪製區域,20%高度是操做欄以及展現分數區域.
<!DOCTYPE html> <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>貪吃蛇</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> <!-- 載入jquery 方便dom操做 --> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <!-- sweetalert 美化alert用的 --> <script src="https://cdn.bootcss.com/sweetalert/2.1.2/sweetalert.min.js"></script> <script src="js/main.js"></script> </body> </html>
stylesheet.css
* { padding: 0; margin: 0; } body { position: fixed; width: 100%; height: 100%; } #app { max-width: 768px; margin-left: auto; margin-right: auto; } /* canvas繪製區域 */ .content-canvas { width: 100%; max-width: 768px; height: 80%; position: fixed; overflow: hidden; } .content-canvas canvas { position: absolute; width: 100%; height: 100%; } /* 操做區域 */ .control { position: fixed; width: 100%; max-width: 768px; height: 20%; bottom: 0; background-color: #aeff5d; }
main.js
$(function() { // 主代碼編寫區域 })
注意的點(遇到的問題以及解決方案):
main.js
$(function () { var LINE_WIDTH = 1 // 線條寬度 var LINE_MAX_NUM = 32 // 一行格子數量 var canvasHeight = $('canvas').height() // 獲取canvas的高度 var canvasWidth = $('canvas').width() // 獲取canvas的寬度 var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // 格子寬度,按一行32個格子計算 var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // 計算橫向和縱向多少個格子,即:橫座標的最大值和縱座標的最大值 /** * 繪製格子地圖 * @param graphics */ function drawGrid(graphics) { var wNum = num.w var hNum = num.h graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52') // 畫橫向的線條 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) // 畫縱向的線條 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) // 給canvas設置寬高屬性賦值上當前canvas的寬度和高度(單用樣式配置寬高會被拉伸) $('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() })
瀏覽器打開index.html
,能夠看到效果:
蛇能夠想象成一串座標點(數組),「移動時」在數組頭部添加新的座標,去除尾部的座標。相似隊列,先進先出。
main.js
$(function () { var LINE_WIDTH = 1 // 線條寬度 var LINE_MAX_NUM = 32 // 一行格子數量 var SNAKE_START_POINT = [[0, 3], [1, 3], [2, 3], [3, 3]] // 初始蛇座標 var DIR_ENUM = { UP: 1, DOWN: -1, LEFT: 2, RIGHT: -2 } // 移動的四個方向枚舉值,兩個對立方向相加等於0 var GAME_STATE_ENUM = { END: 1, READY: 2 } // 遊戲狀態枚舉 var canvasHeight = $('canvas').height() // 獲取canvas的高度 var canvasWidth = $('canvas').width() // 獲取canvas的寬度 var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // 格子寬度,按一行32個格子計算 var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // 計算橫向和縱向多少個格子,即:橫座標的最大值和縱座標的最大值 var directionNow = null // 當前移動移動方向 var directionNext = null // 下一步移動方向 var gameState = null // 遊戲狀態 /** * 繪製格子地圖 * @param graphics */ function drawGrid(graphics) { var wNum = num.w var hNum = num.h graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52') // 畫橫向的線條 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) // 畫縱向的線條 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 Point(x, y) { this.x = x this.y = y } /** * 根據移動的方向,獲取當前座標的下一個座標 * @param direction 移動的方向 */ 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 } /** * 初始化蛇的座標 * @returns {[Point,Point,Point,Point,Point ...]} * @private */ function initSnake() { return SNAKE_START_POINT.map(function (item) { return new Point(item[0], item[1]) }) } /** * 繪製蛇 * @param graphics * @param snakes // 蛇座標 */ 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) } } /** * 改變蛇身座標 * @param snakes 蛇座標集 * @param direction 方向 */ function updateSnake(snakes, direction) { var oldHead = snakes[snakes.length - 1] var newHead = oldHead.nextPoint(direction) // 超出邊界 遊戲結束 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) { // ‘吃’到本身 遊戲結束 return newHead.x === p.x && newHead.y === p.y })) { gameState = GAME_STATE_ENUM.END } else { snakes.push(newHead) snakes.shift() } } /** * 引擎 * @param graphics * @param snakes */ function move(graphics, snakes, stage) { clearTimeout(window._engine) // 重啓時關停以前的引擎 run() function run() { directionNow = directionNext updateSnake(snakes, directionNow) // 更新蛇座標 if (gameState === GAME_STATE_ENUM.END) { end() } else { drawSnake(graphics, snakes) stage.update() window._engine = setTimeout(run, 500) } } } /** * 遊戲結束回調 */ function end() { console.log('遊戲結束') } function init() { $('canvas').attr('width', canvasWidth) // 給canvas設置寬高屬性賦值上當前canvas的寬度和高度(單用樣式配置寬高會被拉伸) $('canvas').attr('height', canvasHeight) directionNow = directionNext = DIR_ENUM.DOWN // 初始化蛇的移動方向 var snakes = initSnake() var stage = new createjs.Stage($('canvas')[0]) var grid = new createjs.Shape() var snake = new createjs.Shape() drawGrid(grid.graphics) // 繪製格子 drawSnake(snake.graphics, snakes) stage.addChild(grid) stage.addChild(snake) stage.update() move(snake.graphics, snakes, stage) } init() })
效果圖(gif):
製做4個按鈕,控制移動方向
index.html
... <div class="control"> <div class="row"> <div class="btn"> <button id="UpBtn">上</button> </div> </div> <div class="row clearfix"> <div class="btn half-width left"> <button id="LeftBtn">左</button> </div> <div class="btn half-width right"> <button id="RightBtn">右</button> </div> </div> <div class="row"> <div class="btn"> <button id="DownBtn">下</button> </div> </div> </div> </div> ...
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; }
mian.js
... /** * 改變蛇行進方向 * @param dir */ function changeDirection(dir) { /* 逆向及同向則不改變 */ if (directionNow + dir === 0 || directionNow === dir) return directionNext = dir } /** * 綁定相關元素點擊事件 */ 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() ... }
效果圖(gif):
隨機取兩個座標點繪製果子,斷定若是「吃到」,則不刪除尾巴。縮短定時器的時間間隔增長難度。
注意的點(遇到的問題以及解決方案):新增一個果子不能佔用蛇的座標,一開始考慮的是隨機生成一個座標,若是座標已被佔用,那就繼續生成隨機座標。而後發現這樣作有個問題就是整個界面剩餘兩個座標可用時(極端狀況,蛇佔了整個屏幕就差兩個格子了),那這樣的話,不停隨機取座標,要取到這最後兩個座標要耗很多時間。後面改了方法,先統計全部座標,而後循環蛇身座標,一一排除不可用座標,而後再隨機抽取可用座標的其中一個。
main.js
$(function () { var LINE_WIDTH = 1 // 線條寬度 var LINE_MAX_NUM = 32 // 一行格子數量 var SNAKE_START_POINT = [[0, 3], [1, 3], [2, 3], [3, 3]] // 初始蛇座標 var DIR_ENUM = { UP: 1, DOWN: -1, LEFT: 2, RIGHT: -2 } // 移動的四個方向枚舉值,兩個對立方向相加等於0 var GAME_STATE_ENUM = { END: 1, READY: 2 } // 遊戲狀態枚舉 var canvasHeight = $('canvas').height() // 獲取canvas的高度 var canvasWidth = $('canvas').width() // 獲取canvas的寬度 var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // 格子寬度,按一行32個格子計算 var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // 計算橫向和縱向多少個格子,即:橫座標的最大值和縱座標的最大值 var directionNow = null // 當前移動移動方向 var directionNext = null // 下一步移動方向 var gameState = null // 遊戲狀態 var scope = 0 // 分數 /** * 繪製格子地圖 * @param graphics */ function drawGrid(graphics) { var wNum = num.w var hNum = num.h graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52') // 畫橫向的線條 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) // 畫縱向的線條 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 Point(x, y) { this.x = x this.y = y } /** * 根據移動的方向,獲取當前座標的下一個座標 * @param direction 移動的方向 */ 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 } /** * 初始化蛇的座標 * @returns {[Point,Point,Point,Point,Point ...]} * @private */ function initSnake() { return SNAKE_START_POINT.map(function (item) { return new Point(item[0], item[1]) }) } /** * 繪製蛇 * @param graphics * @param snakes // 蛇座標 */ 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) } } /** * 改變蛇身座標 * @param snakes 蛇座標集 * @param direction 方向 */ function updateSnake(snakes, fruits, direction, fruitGraphics) { var oldHead = snakes[snakes.length - 1] var newHead = oldHead.nextPoint(direction) // 超出邊界 遊戲結束 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) { // ‘吃’到本身 遊戲結束 return newHead.x === p.x && newHead.y === p.y })) { gameState = GAME_STATE_ENUM.END } else if (fruits.some(function (p) { // ‘吃’到水果 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() } } /** * 引擎 * @param graphics * @param snakes */ function move(snakeGraphics, fruitGraphics, snakes, fruits, stage) { clearTimeout(window._engine) // 重啓時關停以前的引擎 run() function run() { directionNow = directionNext updateSnake(snakes, fruits, directionNow, fruitGraphics) // 更新蛇座標 if (gameState === GAME_STATE_ENUM.END) { end() } else { drawSnake(snakeGraphics, snakes) stage.update() window._engine = setTimeout(run, 500 * Math.pow(0.9, scope)) } } } /** * 遊戲結束回調 */ function end() { console.log('遊戲結束') } /** * 改變蛇行進方向 * @param dir */ function changeDirection(dir) { /* 逆向及同向則不改變 */ if (directionNow + dir === 0 || directionNow === dir) return directionNext = dir } /** * 綁定相關元素點擊事件 */ 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) }) } /** * 建立水果座標 * @returns Point * @param snakes * @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 + '-' + y] = true } } snakes.forEach(function (item) { delete totals[item.x + '-' + item.y] }) fruits.forEach(function (item) { delete totals[item.x + '-' + item.y] }) var keys = Object.keys(totals) if (keys.length) { var temp = Math.floor(keys.length * Math.random()) var key = keys[temp].split('-') return new Point(Number(key[0]), Number(key[1])) } else { return null } } /** * 繪製水果 * @param graphics * @param fruits 水果座標集 */ 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) // 給canvas設置寬高屬性賦值上當前canvas的寬度和高度(單用樣式配置寬高會被拉伸) $('canvas').attr('height', canvasHeight) directionNow = directionNext = DIR_ENUM.DOWN // 初始化蛇的移動方向 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) // 繪製格子 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() })
效果圖(gif):
這一部分就比較簡單了,處理下數據的展現便可。這部分代碼就不展現出來了。
界面比較粗糙,主要是學習邏輯操做。中間出現一些小問題,但都一一的解決了。createjs這個遊戲引擎仍是比較簡單易學的,總體只用了繪製圖形的api。