上一篇博文講了如何造一條蛇,如今蛇有了,要讓它自由的活動起來,就得有個地圖啊,並且只能走也不行呀,還得有點吃的,因此還得加點食物,這一篇博文就來說講如何添加地圖和食物。javascript
當前項目最新效果:http://whxaxes.github.io/slither/ (因爲代碼一直在更新,效果可能會比本文所述的更多)java
slither.io的地圖是相似於rpg遊戲的大地圖,因此,咱們須要兩個新的類,一個是地圖類:Map,一個是視窗類:Frame,地圖類就是整個大地圖的抽象,視窗類就是可視界面的抽象。git
而怎麼作成蛇動的時候,繪製位置不動,而是地圖動呢。其實原理也很簡單,若是看過上一篇文章的讀者,應該還記得Base類裏有兩個參數:paintX
以及paintY
,這兩個是繪製座標,跟蛇的座標不一樣的就是,繪製座標是蛇的實際座標減去視窗的座標。github
get paintX() { return this.x - frame.x; } get paintY() { return this.y - frame.y; }
每次render的時候,繪製的座標就是用的這兩個參數,同時適當的調整一下視窗的座標,就能夠作成相對於視窗中蛇沒移動,可是看上去蛇移動了的效果。canvas
Base類裏還有一個參數叫visible:dom
/** * 在視窗內是否可見 * @returns {boolean} */ get visible() { const paintX = this.paintX; const paintY = this.paintY; const halfWidth = this.width / 2; const halfHeight = this.height / 2; return (paintX + halfWidth > 0) && (paintX - halfWidth < frame.width) && (paintY + halfHeight > 0) && (paintY - halfHeight < frame.height); }
用於判斷實例在視窗frame中是否可見,若是不可見,就不須要調用繪製接口了,從而提高遊戲性能。函數
而食物類就比較簡單了,繼承Base類後,在地圖中隨機出必定數量,再進行一下蛇頭與食物的碰撞檢測便可。性能
接着再細講一下各個類的實現。動畫
由於地圖類也依賴視窗類,因此先看視窗類。代碼量至關少,因此直接所有貼出:this
// 視窗類 class Frame { init(options) { this.x = options.x; this.y = options.y; this.width = options.width; this.height = options.height; } /** * 跟蹤某個對象 */ track(obj) { this.translate( obj.x - this.x - this.width / 2, obj.y - this.y - this.height / 2 ); } /** * 移動視窗 * @param x * @param y */ translate(x, y) { this.x += x; this.y += y; } } export default new Frame();
因爲視窗在整個遊戲中只有一個,因此作成了單例的。視窗類就只有幾個屬性,x座標,y座標,寬度和高度。x座標和y座標是相對於地圖左上角的值,width和height通常就是canvas的大小。
track方法是跟蹤某個對象,也就是視窗跟着對象的移動而移動。在main.js中調用跟蹤蛇類:
// 讓視窗跟隨蛇的位置更改而更改 frame.track(snake);
地圖類跟視窗類同樣也是整個遊戲裏只有一個,因此也作成單例的,並且因爲,整個遊戲的元素,都是基於地圖上的,因此我也把canvas的2d繪圖對象掛載到了地圖類上。先看地圖類的部分代碼:
constructor() { // 背景塊的大小 this.block_w = 150; this.block_h = 150; } /** * 初始化map對象 * @param options */ init(options) { this.canvas = options.canvas; this.ctx = this.canvas.getContext('2d'); // 地圖大小 this.width = options.width; this.height = options.height; } /** * 清空地圖上的內容 */ clear() { this.ctx.clearRect(0, 0, frame.width, frame.height); }
構造函數中,定義一下地圖背景的方格塊的大小,而後就是init方法,給外部初始化用的,由於地圖的位置是固定的,因此不須要座標值,只須要寬度和高度便可。clear是給外部調用用來清除畫布。
再看地圖類的渲染方法:
/** * 渲染地圖 */ render() { const beginX = (frame.x < 0) ? -frame.x : (-frame.x % this.block_w); const beginY = (frame.y < 0) ? -frame.y : (-frame.y % this.block_h); const endX = (frame.x + frame.width > this.width) ? (this.width - frame.x) : (beginX + frame.width + this.block_w); const endY = (frame.y + frame.height > this.height) ? (this.height - frame.y) : (beginY + frame.height + this.block_h); // 鋪底色 this.ctx.fillStyle = '#999'; this.ctx.fillRect(beginX, beginY, endX - beginX, endY - beginY); // 畫方格磚 this.ctx.strokeStyle = '#fff'; for (let x = beginX; x <= endX; x += this.block_w) { for (let y = beginY; y <= endY; y += this.block_w) { const cx = endX - x; const cy = endY - y; const w = cx < this.block_w ? cx : this.block_w; const h = cy < this.block_h ? cy : this.block_h; this.ctx.strokeRect(x, y, w, h); } } }
其實就是根據視窗的位置,來進行局部繪製,若是進行整個地圖的繪製,會超級消耗性能。因此只繪製須要展現的那一塊。
按照slither.io的功能,大地圖有了,還得畫個小地圖:
/** * 畫小地圖 */ renderSmallMap() { // 小地圖外殼, 圓圈 const margin = 30; const smapr = 50; const smapx = frame.width - smapr - margin; const smapy = frame.height - smapr - margin; // 地圖在小地圖中的位置和大小 const smrect = 50; const smrectw = this.width > this.height ? smrect : (this.width * smrect / this.height); const smrecth = this.width > this.height ? (this.height * smrect / this.width) : smrect; const smrectx = smapx - smrectw / 2; const smrecty = smapy - smrecth / 2; // 相對比例 const radio = smrectw / this.width; // 視窗在小地圖中的位置和大小 const smframex = frame.x * radio + smrectx; const smframey = frame.y * radio + smrecty; const smframew = frame.width * radio; const smframeh = frame.height * radio; this.ctx.save(); this.ctx.globalAlpha = 0.8; // 畫個圈先 this.ctx.beginPath(); this.ctx.arc(smapx, smapy, smapr, 0, Math.PI * 2); this.ctx.fillStyle = '#000'; this.ctx.fill(); this.ctx.stroke(); // 畫縮小版地圖 this.ctx.fillStyle = '#999'; this.ctx.fillRect(smrectx, smrecty, smrectw, smrecth); // 畫視窗 this.ctx.strokeRect(smframex, smframey, smframew, smframeh); // 畫蛇蛇位置 this.ctx.fillStyle = '#f00'; this.ctx.fillRect(smframex + smframew / 2 - 1, smframey + smframeh / 2 - 1, 2, 2); this.ctx.restore(); }
這個也沒什麼難度,就是疊圖層而已。再也不解釋
最後再export出去:export default new Map();
便可。
在main.js中,直接初始化一下:
// 初始化地圖對象 map.init({ canvas, width: 5000, height: 5000 }); // 初始化視窗對象 frame.init({ x: 1000, y: 1000, width: canvas.width, height: canvas.height });
而後在動畫循環中,讓視窗跟隨蛇的實例snake
,而後再進行相應的render便可,render的順序關係到元素的層級,因此小地圖是最後才render :
// 讓視窗跟隨蛇的位置更改而更改 frame.track(snake); map.render(); snake.render(); map.renderSmallMap();
再講一下食物類,也是很是的簡單,直接繼承Base類,而後作個簡單的發光動畫效果便可,代碼量很少,也所有貼出:
export default class Food extends Base { constructor(options) { super(options); this.point = options.point; this.r = this.width / 2; // 食物的半徑, 發光半徑 this.cr = this.width / 2; // 食物實體半徑 this.lightDirection = true; // 發光動畫方向 } update() { const lightSpeed = 1; this.r += this.lightDirection ? lightSpeed : -lightSpeed; // 當發光圈到達必定值再縮小 if (this.r > this.cr * 2 || this.r < this.cr) { this.lightDirection = !this.lightDirection; } } render() { this.update(); if (!this.visible) { return; } map.ctx.fillStyle = '#fff'; // 繪製光圈 map.ctx.globalAlpha = 0.2; map.ctx.beginPath(); map.ctx.arc(this.paintX, this.paintY, this.r, 0, Math.PI * 2); map.ctx.fill(); // 繪製實體 map.ctx.globalAlpha = 1; map.ctx.beginPath(); map.ctx.arc(this.paintX, this.paintY, this.cr, 0, Math.PI * 2); map.ctx.fill(); } }
而後在main.js中,進行食物生成:
// 食物生成方法 const foodsNum = 100; const foods = []; function createFood(num) { for (let i = 0; i < num; i++) { const point = ~~(Math.random() * 30 + 50); const size = ~~(point / 3); foods.push(new Food({ x: ~~(Math.random() * (map.width + size) - 2 * size), y: ~~(Math.random() * (map.height + size) - 2 * size), size, point })); } }
而後在動畫循環中進行循環而且渲染便可:
// 渲染食物, 以及檢測食物與蛇頭的碰撞 foods.slice(0).forEach(food => { food.render(); if (food.visible && collision(snake.header, food)) { foods.splice(foods.indexOf(food), 1); snake.eat(food); createFood(1); } });
渲染的同時,也跟蛇頭進行一下碰撞檢測,若是產生了碰撞,則從食物列表中刪掉吃掉的實物,而且調用蛇類的eat方法,而後再隨機生成一個食物補充。
由於食物是圓,蛇頭也是圓,因此碰撞檢測就很簡單了:
/** * 碰撞檢測 * @param dom * @param dom2 * @param isRect 是否爲矩形 */ function collision(dom, dom2, isRect) { const disX = dom.x - dom2.x; const disY = dom.y - dom2.y; if (isRect) { return Math.abs(disX) < (dom.width + dom2.width) && Math.abs(disY) < (dom.height + dom2.height); } return Math.hypot(disX, disY) < (dom.width + dom2.width) / 2; }
而後再看一下蛇的eat方法:
/** * 吃掉食物 * @param food */ eat(food) { this.point += food.point; // 增長分數引發蟲子體積增大 const newSize = this.header.width + food.point / 50; this.header.setSize(newSize); this.bodys.forEach(body => { body.setSize(newSize); }); // 同時每吃一個食物, 都增長身軀 const lastBody = this.bodys[this.bodys.length - 1]; this.bodys.push(new SnakeBody({ x: lastBody.x, y: lastBody.y, size: lastBody.width, color: lastBody.color, tracer: lastBody })); }
調用該方法後,會使蛇的分數增長,同時增長體積,以及身軀長度。
至此,地圖以及食物都作好了。
照例貼出github地址:https://github.com/whxaxes/slither