Canvas在超級(jí)瑪麗游戲中的應(yīng)用詳解
前言
在上一篇文章中, 我們基于DOM體系構(gòu)建了超級(jí)瑪麗, 那么在本篇文章中我們使用canvas對(duì)整個(gè)架構(gòu)進(jìn)行升級(jí), 從而提升游戲的視覺(jué)體驗(yàn)。 有需要的同學(xué)可以查看 源碼 學(xué)習(xí).
考慮到有些同學(xué)對(duì)canvas不是很熟悉。本文將會(huì)對(duì)canvas的一些基礎(chǔ)做一些大致的講解。
canvas基礎(chǔ)知識(shí)
畫(huà)布元素
canvas標(biāo)簽可以讓我們能夠使用JavaScript在網(wǎng)頁(yè)上繪制各種樣式的圖形。要訪問(wèn)實(shí)際的繪圖接口, 首先我們需要?jiǎng)?chuàng)建一個(gè)上下文(context), 它是一個(gè)對(duì)象, 提供了繪圖的接口。目前有兩種廣受繪圖的樣式: 用于二維圖形的”2d“以及通過(guò) OpenGL 接口的三維圖形的 webgl 。
比如, 我們可以使用 <canvas /> DOM元素上的 getContext 方法創(chuàng)建上下文。
<body>
<canvas width="500" height="500" />
</body>
<script>
let canvas = document.querySelector('canvas');
let context = canvas.getContext('2d');
context.fillStyle = "yellow";
context.fillRect(10, 10, 400, 400);
</script>

我們繪制了一個(gè)寬度和高度都為400像素的黃色正方形, 并且其左上角頂點(diǎn)處的坐標(biāo)為(10, 10)。canvas的坐標(biāo)系(0, 0)在其左上角.
邊框的繪制
在畫(huà)布的接口中, fillRect 方法用于填充矩形。 fillStyle 用于控制填充形狀的方法。比如
單色
context.fillStyle = "yellow";
漸變色
let canvas = document.querySelector('canvas');
let context = canvas.getContext('2d');
let grd = context.createLinearGradient(0,0,170,0);
grd.addColorStop(0,"black");
grd.addColorStop(1,"red");
context.fillStyle = grd;
context.fillRect(10, 10, 400, 400);
pattern圖案對(duì)象
let canvas = document.querySelector('canvas');
let context = canvas.getContext('2d');
let img = document.createElement('img');
img.src = "https://dss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3112798566,2640650199&fm=26&gp=0.jpg";
img.onload = () => {
let pattern = context.createPattern(img, 'no-repeat');
context.fillStyle = pattern;
context.fillRect(10,10,400,400)
}
strokeStyle屬性與fillStyle屬性類似, 但是 strokeStyle 作用與描邊線的顏色。線條的寬度由 lineWidth 屬性決定。
比如我想繪制一個(gè)邊框?qū)挾葹?的黃色正方形。
let canvas = document.querySelector('canvas');
let context = canvas.getContext('2d');
context.strokeStyle = "yellow";
context.lineWidth = 6;
context.strokeRect(10,10, 400, 400);
路徑
路徑是很多線條的組合。如果想要繪制各種各樣的形狀,我們會(huì)頻繁用到 moveTo 和 lineTo 兩個(gè)函數(shù)。
let canvas = document.querySelector('canvas');
let context = canvas.getContext('2d');
context.beginPath();
for (let index = 0; index < 400; index+=10) {
context.moveTo(10, index);
context.moveTo(index, 0);
context.lineTo(390, index);
}
context.stroke();
moveTo 表示我們當(dāng)前畫(huà)筆起點(diǎn)的位置, lineTo 表示我們畫(huà)筆從起點(diǎn)到終點(diǎn)的連線。以上代碼執(zhí)行后就是如下所示:

當(dāng)然我們可以為線條繪制的圖形進(jìn)行填充。
let canvas = document.querySelector('canvas');
let context = canvas.getContext('2d');
context.beginPath();
context.moveTo(50, 10);
context.lineTo(10, 70);
context.lineTo(90, 70);
context.fill();
context.closePath();
繪制圖片
在計(jì)算機(jī)圖形學(xué)中, 通常需要對(duì)矢量圖形和位圖圖形進(jìn)行區(qū)分。 矢量圖形是指: 通過(guò)給出形狀的邏輯來(lái)描述指定的圖片。而位圖圖形是指使用像素?cái)?shù)據(jù), 而不指定實(shí)際形狀。
canvas中的 drawImage 方法允許我們將像素?cái)?shù)據(jù)繪制到畫(huà)布上。像素的數(shù)據(jù)可以來(lái)自于元素或者另外一個(gè)畫(huà)布。
drawImage支持傳遞9個(gè)參數(shù), 第2到5個(gè)參數(shù)表明源圖像中被復(fù)制的(x, y, 高度, 寬度), 第6到9個(gè)參數(shù)給出被復(fù)制的圖像在canvas畫(huà)布上的位置以及寬高。
下圖是瑪麗多個(gè)姿勢(shì)的匯總圖, 我們使用 drawImage 先讓他能夠正常跑起來(lái)。

let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
let img = document.createElement('img');
img.src = './player_big.png'
let spriteW = 47, spriteH = 58;
img.onload = () => {
let cycle = 0;
setInterval(() => {
ctx.clearRect(0, 0, spriteW, spriteH);
ctx.drawImage(img,
cycle*spriteW, 0, spriteW, spriteH,
0, 0, spriteW, spriteH,
);
cycle = (cycle + 1) % 10;
}, 120);
}

我們需要大致截取瑪麗的大小, 通過(guò) cycle 鎖定瑪麗在動(dòng)畫(huà)中的位置。在合成中, 我們只需要讓前面8個(gè)動(dòng)作循環(huán)播放即可實(shí)現(xiàn)瑪麗的一個(gè)奔跑動(dòng)作了。
控制轉(zhuǎn)換
現(xiàn)在我們已經(jīng)可以讓瑪麗朝著右邊跑了, 但是在實(shí)際的游戲中 瑪麗是可以左右跑的。這里的話 有兩個(gè)方案: 1. 我們?cè)倮L制一組朝著左邊跑的組合圖 2.控制畫(huà)布反過(guò)來(lái)繪制圖片。第一種方案比較簡(jiǎn)單, 因此我們就選擇第二種比較復(fù)雜一點(diǎn)的方案。
canvas中可以調(diào)用scale方法按照比例尺調(diào)整然后繪制。此方法有兩個(gè)參數(shù), 第一個(gè)參數(shù)用于設(shè)置水平方向比例尺, 另外一個(gè)設(shè)置垂直方向的比例尺。
let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
ctx.scale(3, .5);
ctx.beginPath();
ctx.arc(50, 50, 40, 0, 7);
ctx.lineWidth = 3;
ctx.stroke();
上面是對(duì) scale 的簡(jiǎn)單應(yīng)用。我們調(diào)用了 scale 使得圓的水平方向被拉伸了3倍, 垂直方向被縮小了0.5倍。
如果scale中的參數(shù)為負(fù)數(shù)-1時(shí), 在x位置為100的位置繪制的形狀最終會(huì)被繪制到-100的位置。因此為了轉(zhuǎn)化圖片, 我們不能僅僅在drawImage的之前調(diào)用 ctx.scale(-1, 1) , 因?yàn)樵诋?dāng)前畫(huà)布中是看不到轉(zhuǎn)化后的圖片的。這里有兩種方案: 1. 調(diào)用 drawImage 的時(shí)候設(shè)置x為-50的時(shí)候來(lái)繪制圖形 2.通過(guò)調(diào)整坐標(biāo)軸, 這種做法的好處在于我們編寫(xiě)的繪圖不需要關(guān)心比例尺的變化。
我們采用 rotate 來(lái)渲染繪制的圖形, 并且通過(guò) translate 方法移動(dòng)他們。
function flip(context, around) {
context.translate(around, 0);
context.scale(-1, 1);
context.translate(-around, 0);
}
我們的思路大概是這樣子:

如果我們?cè)谡齲處繪制三角形, 默認(rèn)情況下它會(huì)位于1位置。調(diào)用flip函數(shù)后首先進(jìn)行右邊平移, 得到三角形2. 然后通過(guò)調(diào)用 scale 進(jìn)行翻轉(zhuǎn)得到三角形3。最后再次通過(guò)調(diào)用 translate 方法, 對(duì)三角形3進(jìn)行平移得到三角形4, 也就是最后我們想要的圖案。
let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
let img = document.createElement('img');
img.src = './player_big.png'
let spriteW = 47, spriteH = 58;
img.onload = () => {
ctx.clearRect(100, 0, spriteW, spriteH);
flip(ctx, 100 + spriteW / 2);
ctx.drawImage(img,
0, 0, spriteW, spriteH,
100, 0, spriteW, spriteH,
);
}
看, 他已經(jīng)被我們轉(zhuǎn)過(guò)來(lái)了!

升級(jí)超級(jí)瑪麗游戲
在上一篇文章中, 我們所有的元素都是直接通過(guò)DOM來(lái)顯示的, 那么在我們學(xué)完canvas之后, 我們可以使用drawImage來(lái)繪制元素。
我們定義CanvasDisplay替換掉之前的DOMDisplay, 除此之外, 我們新增了跟蹤自己視圖窗口, 他可以告訴我們當(dāng)前正在那部分的關(guān)卡, 此外我還新增了 flipPlayer 屬性, 這樣即使瑪麗不動(dòng), 它仍然面對(duì)著它最后移動(dòng)的方向。
var CanvasDisplay = class CanvasDisplay {
constructor(parent, level) {
this.canvas = document.createElement("canvas");
this.canvas.width = Math.min(600, level.width * scale);
this.canvas.height = Math.min(450, level.height * scale);
parent.appendChild(this.canvas);
this.cx = this.canvas.getContext("2d");
this.flipPlayer = false;
this.viewport = {
left: 0,
top: 0,
width: this.canvas.width / scale,
height: this.canvas.height / scale
};
}
clear() {
this.canvas.remove();
}
}
syncState方法首先計(jì)算新視圖窗口, 然后在適當(dāng)?shù)奈恢美L制。
CanvasDisplay.prototype.syncState = function(state) {
this.updateViewport(state);
this.clearDisplay(state.status);
this.drawBackground(state.level);
this.drawActors(state.actors);
};
DOMDisplay.prototype.syncState = function(state) {
if (this.actorLayer) this.actorLayer.remove();
this.actorLayer = drawActors(state.actors);
this.dom.appendChild(this.actorLayer);
this.dom.className = `game ${state.status}`;
this.scrollPlayerIntoView(state);
};
在之前的更新相反, 我們現(xiàn)在必須在每次更新的時(shí)候, 重新繪制背景。因?yàn)楫?huà)布上的形狀只是像素, 所以我們?cè)诶L制完后沒(méi)有好的方法來(lái)移動(dòng)或者刪除他們。因此更新畫(huà)布的唯一方法是清除并且重繪。
updateViewport 方法跟 scrollPlayerIntoView 方法一樣。它會(huì)檢查玩家是否太靠近視圖邊緣。
CanvasDisplay.prototype.updateViewport = function(state) {
let view = this.viewport, margin = view.width / 3;
let player = state.player;
let center = player.pos.plus(player.size.times(0.5));
if (center.x < view.left + margin) {
view.left = Math.max(center.x - margin, 0);
} else if (center.x > view.left + view.width - margin) {
view.left = Math.min(center.x + margin - view.width,
state.level.width - view.width);
}
if (center.y < view.top + margin) {
view.top = Math.max(center.y - margin, 0);
} else if (center.y > view.top + view.height - margin) {
view.top = Math.min(center.y + margin - view.height,
state.level.height - view.height);
}
};
當(dāng)我們成功或者失敗的時(shí)候, 我們需要清除當(dāng)前場(chǎng)景, 因?yàn)槿绻×? 我們需要重新來(lái), 如果成功了, 我們需要?jiǎng)h除當(dāng)前場(chǎng)景, 重新繪制一個(gè)新的場(chǎng)景。
CanvasDisplay.prototype.clearDisplay = function(status) {
if (status == "won") {
this.cx.fillStyle = "rgb(68, 191, 255)";
} else if (status == "lost") {
this.cx.fillStyle = "rgb(44, 136, 214)";
} else {
this.cx.fillStyle = "rgb(52, 166, 251)";
}
this.cx.fillRect(0, 0,
this.canvas.width, this.canvas.height);
};
接下來(lái), 我們需要繪制墻壁和熔巖。首先, 我們遍歷當(dāng)前視圖中所有的墻壁和磚頭。我們使用 sprites.png 繪制所有非空的墻磚(墻、熔巖、金幣)。在提供的素材中, 我們墻壁是20px * 20px, 偏移量是0,熔巖也是 20px * 20px, 但是偏移量是20px.

let otherSprites = document.createElement("img");
otherSprites.src = "img/sprites.png";
CanvasDisplay.prototype.drawBackground = function(level) {
let {left, top, width, height} = this.viewport;
let xStart = Math.floor(left);
let xEnd = Math.ceil(left + width);
let yStart = Math.floor(top);
let yEnd = Math.ceil(top + height);
for (let y = yStart; y < yEnd; y++) {
for (let x = xStart; x < xEnd; x++) {
let tile = level.rows[y][x];
if (tile == "empty") continue;
let screenX = (x - left) * scale;
let screenY = (y - top) * scale;
let tileX = tile == "lava" ? scale : 0;
this.cx.drawImage(otherSprites,
tileX, 0, scale, scale,
screenX, screenY, scale, scale);
}
}
};
最后我們需要繪制玩家的模型。
在前面的8個(gè)圖像中, 是一個(gè)完整的運(yùn)動(dòng)過(guò)程。第九個(gè)畫(huà)像是玩家靜止不動(dòng)的狀態(tài), 第10個(gè)畫(huà)像是玩家在離地時(shí)候的狀態(tài)。因此當(dāng)玩家移動(dòng)的時(shí)候, 我們需要每60ms切換一幀。當(dāng)玩家不動(dòng)的時(shí)候繪制第九個(gè)畫(huà)面, 當(dāng)玩家跳躍的時(shí)候繪制第十個(gè)畫(huà)面。

CanvasDisplay.prototype.drawPlayer = function(player, x, y,
width, height){
width += playerXOverlap * 2;
x -= playerXOverlap;
if (player.speed.x != 0) {
this.flipPlayer = player.speed.x < 0;
}
let tile = 8;
if (player.speed.y != 0) {
tile = 9;
} else if (player.speed.x != 0) {
tile = Math.floor(Date.now() / 60) % 8;
}
this.cx.save();
if (this.flipPlayer) {
flipHorizontally(this.cx, x + width / 2);
}
let tileX = tile * width;
this.cx.drawImage(playerSprites, tileX, 0, width, height,
x, y, width, height);
this.cx.restore();
};
對(duì)于不是玩家的模型, 我們根據(jù)對(duì)應(yīng)模型的偏移量找到對(duì)應(yīng)的圖像。
CanvasDisplay.prototype.drawActors = function(actors) {
for (let actor of actors) {
let width = actor.size.x * scale;
let height = actor.size.y * scale;
let x = (actor.pos.x - this.viewport.left) * scale;
let y = (actor.pos.y - this.viewport.top) * scale;
if (actor.type === "player") {
this.drawPlayer(actor, x, y, width, height);
} else {
let tileX = (actor.type === "coin" ? 2 : 1) * scale;
this.cx.drawImage(otherSprites,
tileX, 0, width, height,
x, y, width, height);
}
}
};
最后
ok! 至此, 我們的超級(jí)瑪麗就改造完成, 后面會(huì)陸續(xù)加上一些其他的地圖元素 ~ 有興趣的小伙伴可以關(guān)注一下哦 ~

到此這篇關(guān)于Canvas在超級(jí)瑪麗游戲中的應(yīng)用詳解的文章就介紹到這了,更多相關(guān)Canvas超級(jí)瑪麗游戲內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持腳本之家!
相關(guān)文章

HTML5實(shí)現(xiàn)的移動(dòng)端購(gòu)物車自動(dòng)結(jié)算功能示例代碼
本文介紹HTML5實(shí)現(xiàn)移動(dòng)端購(gòu)物車自動(dòng)結(jié)算,通過(guò)WebStorage、事件監(jiān)聽(tīng)、DOM操作等技術(shù),確保實(shí)時(shí)更新與數(shù)據(jù)同步,優(yōu)化性能及無(wú)障礙性,提升用戶體驗(yàn),感興趣的朋友一起看看吧2025-06-18- 在HTML5中,<button>標(biāo)簽用于定義一個(gè)可點(diǎn)擊的按鈕,它是創(chuàng)建交互式網(wǎng)頁(yè)的重要元素之一,本文將深入解析HTML5中的<button>標(biāo)簽,詳細(xì)介紹其屬性、樣式以及實(shí)際2025-06-18
基于 HTML5 Canvas 實(shí)現(xiàn)圖片旋轉(zhuǎn)與下載功能(完整代碼展示)
本文將深入剖析一段基于 HTML5 Canvas 的代碼,該代碼實(shí)現(xiàn)了圖片的旋轉(zhuǎn)(90 度和 180 度)以及旋轉(zhuǎn)后圖片的下載功能,通過(guò)對(duì)代碼的解讀,我們可以學(xué)習(xí)到如何利用 Canvas API2025-06-18
HTML5 getUserMedia API網(wǎng)頁(yè)錄音實(shí)現(xiàn)指南示例小結(jié)
本教程將指導(dǎo)你如何利用這一API,結(jié)合Web Audio API,實(shí)現(xiàn)網(wǎng)頁(yè)錄音功能,從獲取音頻流到處理和保存錄音,整個(gè)過(guò)程將逐步詳解,此外,還討論了getUserMedia API的使用限制和最2025-06-16- HTML5的搜索框是一個(gè)強(qiáng)大的工具,能夠有效提升用戶體驗(yàn),通過(guò)結(jié)合自動(dòng)補(bǔ)全功能和適當(dāng)?shù)臉邮?,可以?chuàng)建出既美觀又實(shí)用的搜索界面,這篇文章給大家介紹HTML5 搜索框Search Box2025-06-13
- Checkbox是HTML5中非常重要的表單元素之一,通過(guò)合理使用其屬性和樣式自定義方法,可以為用戶提供豐富多樣的交互體驗(yàn),這篇文章給大家介紹HTML5中Checkbox標(biāo)簽的深入全面解2025-06-13
HTML5無(wú)插件拖拽圖片上傳功能實(shí)現(xiàn)過(guò)程
本實(shí)例展示了一種基于HTML5技術(shù)的圖片上傳功能,無(wú)需外部插件即可通過(guò)拖放圖片實(shí)現(xiàn)上傳,涉及到HTML5的拖放API和File API,以及使用CSS來(lái)增強(qiáng)用戶界面的交互性和視覺(jué)反饋,2025-05-16HTML5 定位大全之相對(duì)定位、絕對(duì)定位和固定定位
在HTML5和CSS中,定位(positioning)是控制元素在頁(yè)面上位置的重要機(jī)制,主要有四種定位方式:靜態(tài)定位(static)、相對(duì)定位(relative)、絕對(duì)定位(absolute)和固定定位(fixed),2025-05-13- Microdata作為HTML5新增的一個(gè)特性,它允許開(kāi)發(fā)者在HTML文檔中添加更多的語(yǔ)義信息,以便于搜索引擎和瀏覽器更好地理解頁(yè)面內(nèi)容,本文將探討HTML5中Microdata的使用方法以及2025-04-21
在HTML語(yǔ)法中,表格主要通過(guò)< table >、< tr >和< td >3個(gè)標(biāo)簽構(gòu)成,本文通過(guò)實(shí)例代碼講解HTML5表格語(yǔ)法格式,感興趣的朋友一起看看吧2025-04-21




