<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Canvas在超級瑪麗游戲中的應(yīng)用

          共 15770字,需瀏覽 32分鐘

           ·

          2021-04-16 22:07

          前言

          在上一篇文章中, 我們基于 DOM 體系構(gòu)建了超級瑪麗, 那么在本篇文章中我們使用 canvas 對整個(gè)架構(gòu)進(jìn)行升級, 從而提升游戲的視覺體驗(yàn)。有需要的同學(xué)可以查看源碼學(xué)習(xí).

          線上體驗(yàn)地址

          考慮到有些同學(xué)對 canvas 不是很熟悉。本文將會(huì)對 canvas 的一些基礎(chǔ)做一些大致的講解。

          canvas 基礎(chǔ)知識

          畫布元素

          canvas 標(biāo)簽可以讓我們能夠使用 JavaScript 在網(wǎng)頁上繪制各種樣式的圖形。要訪問實(shí)際的繪圖接口, 首先我們需要?jiǎng)?chuàng)建一個(gè)上下文 (context), 它是一個(gè)對象, 提供了繪圖的接口。目前有兩種廣受繪圖的樣式: 用于二維圖形的”2d“以及通過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>
          復(fù)制代碼

          我們繪制了一個(gè)寬度和高度都為 400 像素的黃色正方形, 并且其左上角頂點(diǎn)處的坐標(biāo)為 (10, 10)。canvas 的坐標(biāo)系(0, 0) 在其左上角.

          邊框的繪制

          在畫布的接口中, fillRect 方法用于填充矩形。fillStyle 用于控制填充形狀的方法。比如

          • 單色
          context.fillStyle = "yellow";
          復(fù)制代碼
          • 漸變色
          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);
          復(fù)制代碼
          • pattern 圖案對象
          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)
          }
          復(fù)制代碼

          strokeStyle 屬性與 fillStyle 屬性類似, 但是 strokeStyle 作用與描邊線的顏色。線條的寬度由 lineWidth屬性決定。

          比如我想繪制一個(gè)邊框?qū)挾葹?6 的黃色正方形。

          let canvas = document.querySelector('canvas');
          let context = canvas.getContext('2d');
          context.strokeStyle = "yellow";
          context.lineWidth = 6;
          context.strokeRect(10,10, 400, 400);
          復(fù)制代碼

          路徑

          路徑是很多線條的組合。如果想要繪制各種各樣的形狀, 我們會(huì)頻繁用到 moveTolineTo 兩個(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();
          復(fù)制代碼

          moveTo 表示我們當(dāng)前畫筆起點(diǎn)的位置, lineTo 表示我們畫筆從起點(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();
          復(fù)制代碼

          繪制圖片

          在計(jì)算機(jī)圖形學(xué)中, 通常需要對矢量圖形和位圖圖形進(jìn)行區(qū)分。矢量圖形是指: 通過給出形狀的邏輯來描述指定的圖片。而位圖圖形是指使用像素?cái)?shù)據(jù), 而不指定實(shí)際形狀。

          canvas 中的 drawImage 方法允許我們將像素?cái)?shù)據(jù)繪制到畫布上。像素的數(shù)據(jù)可以來自于元素或者另外一個(gè)畫布。

          drawImage 支持傳遞 9 個(gè)參數(shù), 第 2 到 5 個(gè)參數(shù)表明源圖像中被復(fù)制的 (x, y, 高度, 寬度), 第 6 到 9 個(gè)參數(shù)給出被復(fù)制的圖像在 canvas 畫布上的位置以及寬高。

          下圖是瑪麗多個(gè)姿勢的匯總圖, 我們使用 drawImage 先讓他能夠正常跑起來。

          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);
          }
          復(fù)制代碼

          我們需要大致截取瑪麗的大小, 通過 cycle 鎖定瑪麗在動(dòng)畫中的位置。在合成中, 我們只需要讓前面 8 個(gè)動(dòng)作循環(huán)播放即可實(shí)現(xiàn)瑪麗的一個(gè)奔跑動(dòng)作了。

          控制轉(zhuǎn)換

          現(xiàn)在我們已經(jīng)可以讓瑪麗朝著右邊跑了, 但是在實(shí)際的游戲中 瑪麗是可以左右跑的。這里的話 有兩個(gè)方案: 1. 我們再繪制一組朝著左邊跑的組合圖 2. 控制畫布反過來繪制圖片。第一種方案比較簡單, 因此我們就選擇第二種比較復(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();
          復(fù)制代碼

          上面是對 scale 的簡單應(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)前畫布中是看不到轉(zhuǎn)化后的圖片的。這里有兩種方案: 1. 調(diào)用 drawImage 的時(shí)候設(shè)置 x 為 - 50 的時(shí)候來繪制圖形 2. 通過調(diào)整坐標(biāo)軸, 這種做法的好處在于我們編寫的繪圖不需要關(guān)心比例尺的變化。

          我們采用 rotate 來渲染繪制的圖形, 并且通過translate方法移動(dòng)他們。

          function flip(context, around) {
              context.translate(around, 0);
              context.scale(-1, 1);
              context.translate(-around, 0);
            }
          復(fù)制代碼

          我們的思路大概是這樣子:

          如果我們在正 x 處繪制三角形, 默認(rèn)情況下它會(huì)位于 1 位置。調(diào)用 flip 函數(shù)后首先進(jìn)行右邊平移, 得到三角形 2. 然后通過調(diào)用 scale 進(jìn)行翻轉(zhuǎn)得到三角形 3。最后再次通過調(diào)用 translate 方法, 對三角形 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,
                );
            }
          復(fù)制代碼

          看, 他已經(jīng)被我們轉(zhuǎn)過來了!

          升級超級瑪麗游戲

          在上一篇文章中, 我們所有的元素都是直接通過 DOM 來顯示的, 那么在我們學(xué)完 canvas 之后, 我們可以使用 drawImage 來繪制元素。

          我們定義 CanvasDisplay 替換掉之前的 DOMDisplay, 除此之外, 我們新增了跟蹤自己視圖窗口, 他可以告訴我們當(dāng)前正在那部分的關(guān)卡, 此外我還新增了 flipPlayer 屬性, 這樣即使瑪麗不動(dòng), 它仍然面對著它最后移動(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();
            }
          }
          復(fù)制代碼

          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);
          };
          復(fù)制代碼
          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);
          };
          復(fù)制代碼

          在之前的更新相反, 我們現(xiàn)在必須在每次更新的時(shí)候, 重新繪制背景。因?yàn)楫嫴忌系男螤钪皇窍袼? 所以我們在繪制完后沒有好的方法來移動(dòng)或者刪除他們。因此更新畫布的唯一方法是清除并且重繪。

          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);
            }
          };
          復(fù)制代碼

          當(dāng)我們成功或者失敗的時(shí)候, 我們需要清除當(dāng)前場景, 因?yàn)槿绻×? 我們需要重新來, 如果成功了, 我們需要?jiǎng)h除當(dāng)前場景, 重新繪制一個(gè)新的場景。

          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);
          };
          復(fù)制代碼

          接下來, 我們需要繪制墻壁和熔巖。首先, 我們遍歷當(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);
              }
            }
          };
          復(fù)制代碼

          最后我們需要繪制玩家的模型。

          在前面的 8 個(gè)圖像中, 是一個(gè)完整的運(yùn)動(dòng)過程。第九個(gè)畫像是玩家靜止不動(dòng)的狀態(tài), 第 10 個(gè)畫像是玩家在離地時(shí)候的狀態(tài)。因此當(dāng)玩家移動(dòng)的時(shí)候, 我們需要每 60ms 切換一幀。當(dāng)玩家不動(dòng)的時(shí)候繪制第九個(gè)畫面, 當(dāng)玩家跳躍的時(shí)候繪制第十個(gè)畫面。

          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();
          };
          復(fù)制代碼

          對于不是玩家的模型, 我們根據(jù)對應(yīng)模型的偏移量找到對應(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);
              }
             }
           };
          復(fù)制代碼

          最后

          ok! 至此, 我們的超級瑪麗就改造完成, 后面會(huì)陸續(xù)加上一些其他的地圖元素 ~ 有興趣的小伙伴可以關(guān)注一下哦 ~


          瀏覽 36
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  丁香五月亚洲无码 | 自拍超碰网 | 91丨豆花丨国产熟女 熟女 | 国产精品爽爽久久久久久 | 天天好逼av |