Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 欧美综合网站,在线播放你懂,国产91精品在线播放

          整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          用 TypeScript 編寫一個(gè) 2D 格斗游戲

          文來源于:前端新世界 ;作者:前端新世界

          如有侵權(quán),聯(lián)系刪除


          今天我將介紹如何用JavaScript編寫一個(gè)簡單的2D格斗游戲。


          項(xiàng)目設(shè)置


          此項(xiàng)目在CSS方面非常簡單,所以我將使用原生CSS并省去構(gòu)建相關(guān)CSS的步驟。


          默認(rèn)使用TypeScript。所以還需要一個(gè)構(gòu)建工具來編譯JavaScript。我選擇的是ESBuild。


          #!/usr/bin/env node
          
          const watchFlag = process.argv.indexOf("--watch") > -1;
          
          require("esbuild")
              .build({
                  entryPoints: ["src/ts/main.ts", "src/ts/sw.ts"],
                  bundle: true,
                  outdir: "public",
                  watch: watchFlag,
              })
              .catch(() => process.exit(1));
          


          ?  attacke git:(main) yarn build
          yarn run v1.22.10
          $ ./esbuild.js
          ?  Done in 0.47s.
          


          HTML基礎(chǔ)構(gòu)建


          提供<canvas>的網(wǎng)站并沒有什么特別之處。唯一重要的元素是canvas本身。它本身不能獲得焦點(diǎn),需要tabindex才能通過鍵盤訪問。點(diǎn)擊鍵盤上下鍵將上下移動頁面。而我們需要避免canvas有焦點(diǎn)的這種情況,否則頁面會隨著角色移動而上下跳躍。寬度和高度也是固定的,canvas可能不會以全高清顯示,但其尺寸是畫布坐標(biāo)系的端點(diǎn),因此需要計(jì)算位置。


          我還添加了一個(gè)Loading加載器,以獲得更流暢的游戲啟動體驗(yàn)。


          <div class="loader">
              <progress value="0" max="100"></progress>
          </div>
          <canvas tabindex="0" id="canvas" width="1920" height="1080"></canvas>
          


          游戲循環(huán)


          JavaScript的實(shí)時(shí)游戲需要游戲循環(huán):遞歸函數(shù)在每一幀調(diào)用自身。即如果我們想保持在60fps或達(dá)到每33ms 30fps的目標(biāo),那么渲染一幀的性能預(yù)算是16ms。循環(huán)本身沒有游戲邏輯。因而我打算每一幀發(fā)送一個(gè)tick事件。游戲的所有其他部分都可以偵聽該事件。


          然后,第一次嘗試我失敗了。


          export class Renderer {
              ctx: CanvasRenderingContext2D;
              ticker: number;
          
              constructor(ctx: CanvasRenderingContext2D) {
                  this.ctx = ctx;
                  this.ticker = setInterval(() => {
                      const tick = new Event("tick", {
                          bubbles: true,
                          cancelable: true,
                          composed: false,
                      });
                      ctx.canvas.dispatchEvent(tick);
                  }, 1000 / 60); // aim for 60fps
              }
          }
          


          我使用了一個(gè)定時(shí)器來調(diào)用游戲循環(huán)。這在Chrome上運(yùn)行良好,但在Firefox和Safari上就崩潰了。Firefox在drawImage()方面表現(xiàn)不佳,因此我決定繪制sprites。不過,雖然Safari即使在每幀繪制大圖像時(shí),也能夠以60fps的速度渲染,但有時(shí)卻實(shí)現(xiàn)不了。原因是Macbooks默認(rèn)啟用節(jié)電模式,只要未連接電源線,Safari就會限制為30fps。我花了一段時(shí)間才搞清楚這一點(diǎn)。


          這兩個(gè)問題的解決方法都是使用requestAnimationFrame取代setInterval。



          constructor(ctx: CanvasRenderingContext2D, theme: Theme) {
              this.ctx = ctx;
              this.theme = theme;
              this.fps = 60; // aim for 60fps
              this.counter = 0;
              this.initTicker();
          }
          
          private initTicker() {
              window.requestAnimationFrame(() => {
                  this.tick();
                  this.initTicker();
              });
          }
          


          現(xiàn)在雖然游戲在這些瀏覽器中都能流暢運(yùn)行,但是游戲速度仍然不同。30fps的瀏覽器將以一半的速度運(yùn)行游戲。下面我將通過測量幀之間的時(shí)間并將跳過的幀數(shù)注入計(jì)算來解決這個(gè)問題。


          private tick() {
              const timeStamp = performance.now();
              const secondsPassed = (timeStamp - this.oldTimeStamp) / 1000;
              this.oldTimeStamp = timeStamp;
          
              // Calculate fps
              const fps = Math.round(1 / secondsPassed);
              const frameSkip = clamp(Math.round((60 - fps) / fps), 0, 30);
          
              // to allow for animations lasting 1s
              if (this.counter >= this.fps * 2) {
                  this.counter = 0;
              }
          
              const tick: TickEvent = new CustomEvent("tick", {
                  bubbles: true,
                  cancelable: true,
                  composed: false,
                  detail: {
                      frameCount: this.counter,
                      frameSkip: frameSkip,
                  },
              });
              this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
              this.ctx.canvas.dispatchEvent(tick);
          
              this.counter++;
          }
          


          玩家角色


          每個(gè)游戲角色都會在各自的character類實(shí)例中被調(diào)用。它控制玩家的行為、動作、外觀和聲音。


          角色行為


          在現(xiàn)實(shí)世界中的對象,當(dāng)角色四處移動時(shí),不是立即從零加速到最高速度。這中間有一個(gè)加速和減速的過程。且以一定的速度移動。這些要求反映到類上就是:


          class Character {
              position: coordinates;
              orientation: number;
              speed: number;
              maxVelocity: number;
              velocity: coordinates;
              obstacle: Obstacle;
              action: {
                  movingX: number;
                  movingY: number;
              };
              //...
          }
          


          當(dāng)按下移動鍵時(shí),action.movingX|Y屬性設(shè)置為+-1。釋放鍵時(shí),該屬性設(shè)置為0。這可作為玩家開始或繼續(xù)移動的指示器。


          // 向左移動
          config.controls[this.player].left.forEach((key: string) => {
              document.addEventListener("keydown", (event: KeyboardEvent) => {
                  this.captureEvent(event);
                  if (event.code === key && event.repeat === false) {
                      this.action.movingX = -1;
                  }
              });
              document.addEventListener("keyup", (event: KeyboardEvent) => {
                  this.captureEvent(event);
                  if (event.code === key) {
                      this.action.movingX = 0;
                  }
              });
          });
          
          // 向左、向上、向下也是類似的方式
          


          注意,按鍵的映射關(guān)系作為數(shù)組存儲在config.controls中,每個(gè)玩家都有自己的控制鍵。


          我們現(xiàn)在可以忽略captureEvent。這只用來防止頁面在按下光標(biāo)鍵時(shí)滾動。還記得如何為每一幀發(fā)送一個(gè)tick事件嗎?我們將在這里偵聽這個(gè)事件。對于每一幀,我都會在重新繪制角色之前更新位置。


          private move(): void {
              const { position, velocity, action } = this;
              const newX = position.x + action.movingX * this.speed + velocity.x * this.speed;
              const newY = position.y + action.movingY * this.speed + velocity.y * this.speed;
          
              position.x = newX;
              position.y = newY;
          
              if (position.x < 0) {
                  position.x = 0;
              } else if (newX > this.ctx.canvas.width - this.size) {
                  position.x = this.ctx.canvas.width - this.size;
              }
          
              if (position.y < 0) {
                  position.y = 0;
              } else if (newY > this.ctx.canvas.height - this.size) {
                  position.y = this.ctx.canvas.height - this.size;
              }
          
              this.velocity.x = clamp(
                  (action.movingX ? this.velocity.x + action.movingX : this.velocity.x * 0.8) * this.speed,
                  this.maxVelocity * -1,
                  this.maxVelocity
              );
              this.velocity.y = clamp(
                  (action.movingY ? this.velocity.y + action.movingY : this.velocity.y * 0.8) * this.speed,
                  this.maxVelocity * -1,
                  this.maxVelocity
              );
          }
          


          這里出現(xiàn)了速度(velocity)。速度是一個(gè)值,隨著玩家持續(xù)按住移動鍵,它會不斷增加,最高可達(dá)maxVelocity。當(dāng)玩家松開移動鍵時(shí),角色不會突然停止而是減速直到停止。速度又平緩地回到0。


          角色不僅可以四處移動,還可以轉(zhuǎn)身。玩家應(yīng)該專注于攻擊,而不是總是需要轉(zhuǎn)身對著對手。


          private turn(): void {
              const otherPlayer = this.player === 0 ? 1 : 0;
              const orientationTarget: coordinates = this.players[otherPlayer]?.position || { x: 0, y: 0 };
              const angle = Math.atan2(orientationTarget.y - this.position.y, orientationTarget.x - this.position.x);
              this.orientation = angle;
          }
          


          小格斗游戲現(xiàn)在是這樣的!好像在跳舞。



          角色動作


          角色需要能夠互相攻擊。同時(shí)為了增加游戲的深度,還應(yīng)該有防守。兩者都被定義為角色動作,并且都有冷卻時(shí)間來防止濫發(fā)。


          class Character {
              range: number;
              attackDuration: number;
              blockDuration: number;
              cooldownDuration: number;
              action: {
                  attacking: boolean;
                  blocking: boolean;
                  cooldown: boolean;
              };
              // ...
          }
          


          觸發(fā)這些動作的工作方式與移動相同——通過偵聽鍵盤事件,然后將動作值設(shè)置為true……


          // attack
          config.controls[this.player].attack.forEach((key: string) => {
              document.addEventListener("keydown", (event: KeyboardEvent) => {
                  if (
                      this.active &&
                      event.code === key &&
                      event.repeat === false &&
                      !this.action.cooldown
                  ) {
                      this.action.attacking = true;
                  }
              });
          });
          
          // block
          config.controls[this.player].block.forEach((key: string) => {
              document.addEventListener("keydown", (event: KeyboardEvent) => {
                  if (
                      this.active &&
                      event.code === key &&
                      event.repeat === false &&
                      !this.action.cooldown
                  ) {
                      this.action.blocking = true;
                  }
              });
          });
          


          最后在游戲循環(huán)中執(zhí)行動作。


          private attack(): void {
              if (!this.active || !this.action.attacking || this.action.cooldown) {
                  return;
              }
          
              this.action.cooldown = true;
          
              // strike duration
              window.setTimeout(() => {
                  this.action.attacking = false;
              }, this.attackDuration);
          
              // cooldown to next attack/block
              window.setTimeout(() => {
                  this.action.cooldown = false;
              }, this.cooldownDuration);
          
              this.strike();
          }
          


          攻擊只實(shí)現(xiàn)了一半工作。另一半是確保對手被擊中——這意味著對方不能阻擋攻擊并且武器在射程內(nèi)。我們在trike()方法中處理。


          private strike(): void {
              const otherPlayerId = this.player === 0 ? 1 : 0;
              const otherPlayer: rectangle = this.players[otherPlayerId].obstacle?.getObject();
          
              const blocked = this.players[otherPlayerId].action.blocking;
              if (blocked) {
                  // opponent blocked the attack
                  return;
              }
          
              // attack hits
          
              const otherPlayerPolygon = new Polygon(new Vector(0, 0), [
                  new Vector(otherPlayer.a.x, otherPlayer.a.y),
                  new Vector(otherPlayer.b.x, otherPlayer.b.y),
                  new Vector(otherPlayer.c.x, otherPlayer.c.y),
                  new Vector(otherPlayer.d.x, otherPlayer.d.y),
              ]);
          
              const weaponPosition = this.getWeaponPosition();
              const weaponPolygon = new Polygon(new Vector(0, 0), [
                  new Vector(weaponPosition.a.x, weaponPosition.a.y),
                  new Vector(weaponPosition.b.x, weaponPosition.b.y),
                  new Vector(weaponPosition.c.x, weaponPosition.c.y),
                  new Vector(weaponPosition.d.x, weaponPosition.d.y),
              ]);
          
              const hit = this.collider.testPolygonPolygon(weaponPolygon, otherPlayerPolygon) as boolean;
              if (hit) {
                  // finish this round
                  this.finish();
              }
          }
          


          這會在玩家周圍同時(shí)向?qū)κ值姆较蜓由?50%創(chuàng)建一個(gè)碰撞框。如果武器碰撞框與對手的碰撞箱發(fā)生撞擊,則攻擊落地且玩家贏得該回合。


          那么碰撞框是怎么回事?


          碰撞檢測


          碰撞檢測并不像我想象的那么簡單。假設(shè)canvas上有兩個(gè)矩形,可以直接比較它們的x和y坐標(biāo)。但是,一旦旋轉(zhuǎn)矩形,比較x和y坐標(biāo)就沒什么用了。于是我嘗試著從矩形的邊界線創(chuàng)建線性函數(shù)并檢查交叉點(diǎn)。但仍然會產(chǎn)生一些邊緣情況,也非常低效。


          然后我轉(zhuǎn)而上網(wǎng)搜索解決方案。并在StackOverflow上找到了:


          ?這個(gè)解決方案聰明、優(yōu)雅、高效,而且——最重要的是——遠(yuǎn)高于我的幾何技能水平。就是它了!


          yarn add collider2d
          


          我在每個(gè)相關(guān)對象周圍添加了碰撞多邊形作為碰撞框,相關(guān)對象包括玩家角色、畫布邊界和競技場中可能存在的障礙物。這些多邊形由描述周長的向量組成。角色多邊形存儲在角色類的一個(gè)屬性中,并在move()、turn()和stroke()方法中更新。


          // inside character.strike()
          const otherPlayerPolygon = new Polygon(new Vector(0, 0), [
              new Vector(otherPlayer.a.x, otherPlayer.a.y),
              new Vector(otherPlayer.b.x, otherPlayer.b.y),
              new Vector(otherPlayer.c.x, otherPlayer.c.y),
              new Vector(otherPlayer.d.x, otherPlayer.d.y),
          ]);
          
          const weaponPosition = this.getWeaponPosition();
          const weaponPolygon = new Polygon(new Vector(0, 0), [
              new Vector(weaponPosition.a.x, weaponPosition.a.y),
              new Vector(weaponPosition.b.x, weaponPosition.b.y),
              new Vector(weaponPosition.c.x, weaponPosition.c.y),
              new Vector(weaponPosition.d.x, weaponPosition.d.y),
          ]);
          
          const hit = this.collider.testPolygonPolygon(
              weaponPolygon,
              otherPlayerPolygon
          ) as boolean;
          


          現(xiàn)在我們來看看實(shí)際的游戲玩法!


          角色可以你來我往地相互碰撞。Collider2D可以返回一些關(guān)于碰撞的信息,比如向量和位置。這與我之前確定的速度解決方案配合得很好。我可以直接將現(xiàn)有速度指向碰撞的方向:


          private collide(): void {
              const obstacles = this.obstacles.filter((obstacle) => obstacle.getId() !== this.obstacle.getId());
              obstacles.forEach((obstacle) => {
                  const collision = this.obstacle.collidesWith(obstacle);
                  const friction = 0.8;
          
                  if (!collision) {
                      return;
                  }
          
                  this.velocity.x = (this.velocity.x + collision.overlapV.x * -1) * friction;
                  this.velocity.y = (this.velocity.y + collision.overlapV.y * -1) * friction;
              });
          }
          


          現(xiàn)在,可以在游戲循環(huán)中調(diào)用collide()與move()、turn(),每一幀都有一個(gè)用于碰撞檢測的輪詢。



          圖形


          這樣的跳舞方塊可能很實(shí)用,但并不漂亮。我想要制作成復(fù)古的像素藝術(shù)風(fēng)格,因而選擇了灰綠色屏幕(后來我設(shè)置為灰藍(lán)色)和放大像素上的陰影效果。


          角色尺寸為16x16px。武器射程為150%,也就是能達(dá)到40x16px。所有的sprites設(shè)置為角色居中,其Photoshop畫布是64x64px。導(dǎo)出圖像時(shí),放大到100x100px字符大小,因?yàn)槿咔迤聊簧系?6px字符太小了。按方向在分組層中對sprites進(jìn)行排序,每個(gè)sprite都需要八種變化——每個(gè)羅盤方向一個(gè)。然后將其乘以動畫sprites的幀數(shù)。



          我需要控制每個(gè)像素,而鋸齒是我最大的敵人,因?yàn)樗鼤鶕?jù)定義影響相鄰像素。當(dāng)我需要變換、縮放或旋轉(zhuǎn)某些東西時(shí),我就使用鋼筆工具而不是畫筆,并使用像素重復(fù)模式。


          導(dǎo)出圖像有點(diǎn)麻煩。我需要導(dǎo)出8位png。它們有一個(gè)alpha通道,并且比gifs甚至是webp的字節(jié)更小。由于某種原因,Photoshop的批量導(dǎo)出不支持8bit png。而且也不能自動裁剪單層。所以我只能手動導(dǎo)出。



          主題


          目前,我只有一組sprites。在某些時(shí)候,我想每輪加載不同的集合。這意味著每個(gè)集合都需要遵守一套特定的規(guī)則。因此我需要定義主題。



          一堆JavaScript和一堆png,需要相互匹配,同時(shí)還要達(dá)到一些次要目標(biāo):


          • 所有sprites必須能實(shí)現(xiàn)動畫
          • 與主題相關(guān)的所有內(nèi)容都必須是可互換的。我希望以后能夠切換整個(gè)樣式。


          在畫布中動畫sprites并不像加載gif那樣簡單。drawImage()只會繪制第一幀。有些技術(shù)可以在畫布內(nèi)實(shí)現(xiàn)gif查看器,但對于這里的用例來說過于復(fù)雜。因此我選擇直接使用帶有單個(gè)幀的數(shù)組。


          declare type Sprite = {
              name: string;
              images: string[];
              animationSpeed: number; // use next image every N frames, max 60
              offset: coordinates;
          };
          


          然后為drawImage()編寫包裝器,它將使用合并的sprites并根據(jù)幀數(shù)切換動畫步驟:


          public drawSprite(ctx: CanvasRenderingContext2D, name: string, pos: coordinates, frameCount = 0) {
              const sprite = this.sprites.find((x) => x.name === name);
              if (!sprite) {
                  return;
              }
          
              const spriteFrame = Math.floor((frameCount / sprite.animationSpeed) % sprite.images.length);
          
              const img = this.images.find((x) => x.src.endsWith(`${sprite.images[spriteFrame].replace("./", "")}`));
          
              if (!img) {
                  return;
              }
          
              ctx.drawImage(img, pos.x + sprite.offset.x, pos.y + sprite.offset.y);
          }
          


          很好,我們現(xiàn)在可以制作動畫了!


          ?互換性需要一致。下面創(chuàng)建主題配置,定義使用哪些sprites以及如何使用。


          declare type SpriteSet = {
              n: Sprite; // sprite facing north
              ne: Sprite; // sprite facing north-east
              e: Sprite; // etc
              se: Sprite;
              s: Sprite;
              sw: Sprite;
              w: Sprite;
              nw: Sprite;
          };
          
          declare type themeConfig = {
              name: string; // has to match folder name
              scene: Sprite; // scene image, 1920x1080
              obstacles: rectangle[]; // outline obsacles within the scene
              turnSprites?: boolean; // whether to turn sprites with characters
              players: {
                  default: SpriteSet; // player when standing still, 100x100
                  move: SpriteSet; // player when moving, 100x100
                  attack: SpriteSet; // player when attacking, 250x100
                  block: SpriteSet; // player when blocking, 100x100
              }[]; // provide sprites for each player, else player 1 sprites will be re-used
          };
          


          這個(gè)配置表示我們處理的主題并從中選擇資源。例如,character類現(xiàn)在可以像這樣繪制主題資源:


          this.theme.drawSprite(
              this.ctx,
              "p1_move_s",
              { x: this.size / -2, y: this.size / -2 },
              frameCount
          );
          


          還記得我在移動角色中添加了轉(zhuǎn)向部分嗎?這對于轉(zhuǎn)動的主題可能很有用——例如小行星。但在我的這個(gè)例子中,轉(zhuǎn)動sprite看起來有點(diǎn)傻。


          我需要一種分配sprite方向值的方法。我必須將8個(gè)羅盤方向映射到一整圈方向值。一段圓弧表示一個(gè)方向。由于起點(diǎn)和終點(diǎn)正好在同一個(gè)方向的中間,所以這個(gè)重疊的方向需要分配兩次——第一次和最后一次。



          private getSprite(): Sprite {
              const directions = ["w", "nw", "n", "ne", "e", "se", "s", "sw", "w"];
              const zones = directions.map((z, i) => ({
                  zone: z,
                  start: (Math.PI * -1) - (Math.PI / 8) + (i * Math.PI) / 4,
                  end: (Math.PI * -1) - (Math.PI / 8) + ((i + 1) * Math.PI) / 4,
              }));
          
              const direction = zones.find((zone) => this.orientation >= zone.start && this.orientation < zone.end);
          
              // action refers to moving, attacking, blocking...
              return this.theme.config.players[this.player][action][direction.zone];
          }
          


          最后,我在character類中使用this.theme.config.turnSprites以便在基于轉(zhuǎn)向和方向的主題之間切換。



          音效


          視覺效果只是主題的一個(gè)方面。另一方面是聲音。我想要用特定的聲音來表示攻擊、阻擋、撞到東西,以及還有背景音樂。


          我采用了一個(gè)簡單直接的方式,使用<audio>元素。每當(dāng)需要聲音時(shí),創(chuàng)建一個(gè)元素,自動播放,然后刪除。


          const audio = new Audio("./sound.mp3");
          audio.play();
          


          這很好用,至少在Chrome和Firefox中是這樣。但是Safari在播放聲音之前總是有延遲。


          我為聲音設(shè)置了AudioContext:一個(gè)由游戲的所有其他部分共享的上下文。


          Web Audio API的構(gòu)建就像一個(gè)真正的模塊化合成器。我們需要將一個(gè)設(shè)備連接到下一個(gè)設(shè)備。在這種情況下,我們使用音頻文件作為輸入源,緩沖,連接到Gain Node設(shè)置音量,最后播放出來。


          this.ctx = new (window.AudioContext || window.webkitAudioContext)();
          
          async function play(sound: string): Promise<void> {
              if (this.sounds[this.getAudioUrl(sound)].playing) {
                  return;
              }
          
              this.sounds[this.getAudioUrl(sound)].playing = true;
          
              const arrayBuffer = await this.getSoundFile(this.getAudioUrl(sound));
              const source = this.ctx.createBufferSource();
          
              this.ctx.decodeAudioData(arrayBuffer, (audioBuffer) => {
                  source.buffer = audioBuffer;
                  source.connect(this.vol);
                  source.loop = false;
                  source.onended = () => {
                      this.terminateSound(source);
                      this.sounds[this.getAudioUrl(sound)].playing = false;
                  };
                  source.start();
              });
          }
          


          以那樣的方式我可以注冊聲音:


          // theme config
          {
              // ...
              bgAudio: "./assets/bgm.mp3",
              attackAudio: "./assets/attack.mp3",
              blockAudio: "./assets/block.mp3",
              collideAudio: "./assets/bump.mp3",
              winAudio: "./assets/win.mp3",
          }
          


          并調(diào)用它們:


          this.audio.play(this.theme.config.collideAudio);
          


          現(xiàn)在,即使是Safari也會在我需要的時(shí)候播放聲音了。


          使用Gamepad


          這里我使用Gamepad API,它與多達(dá)四個(gè)連接的Gamepad接口。


          不過,Gamepad API使用起來有點(diǎn)笨拙。與鍵盤和鼠標(biāo)等更常見的輸入方法不同,Gamepads不發(fā)送事件。相反,一旦站點(diǎn)檢測到Gamepad交互,就會填充Gamepad對象。


          interface Gamepad {
              readonly axes: ReadonlyArray<number>;
              readonly buttons: ReadonlyArray<GamepadButton>;
              readonly connected: boolean;
              readonly hapticActuators: ReadonlyArray<GamepadHapticActuator>;
              readonly id: string;
              readonly index: number;
              readonly mapping: GamepadMappingType;
              readonly timestamp: DOMHighResTimeStamp;
          }
          
          interface GamepadButton {
              readonly pressed: boolean;
              readonly touched: boolean;
              readonly value: number;
          }
          


          每次交互都會改變對象。由于沒有發(fā)送瀏覽器原生事件,因此我需要監(jiān)聽gamead對象的變化。


          if (
              this.gamepads[gamepadIndex]?.buttons &&
              gamepadButton.button.value !==
                  this.gamepads[gamepadIndex]?.buttons[gamepadButton.index]?.value &&
              gamepadButton.button.pressed
          ) {
              // send press event
              this.pressButton(gamepadIndex, b.index, gamepadButton.button);
          } else if (
              this.gamepads[gamepadIndex]?.buttons &&
              gamepadButton.button.value !==
                  this.gamepads[gamepadIndex]?.buttons[gamepadButton.index]?.value &&
              !gamepadButton.button.pressed
          ) {
              // send release event
              this.releaseButton(gamepadIndex, b.index, gamepadButton.button);
          }
          


          pressButton和releaseButton發(fā)送自定義事件,我可以在character類中使用這些事件并擴(kuò)展我輸入法以識別游戲手柄。


          ?因?yàn)槲抑挥蠿box 360控制器,所以我就用這個(gè)來構(gòu)建和測試了。據(jù)我所知,鍵盤映射對于游戲機(jī)控制器的工作方式相同。Xbox的A B X Y按鈕的映射方式與Playstation的幾何形狀相同。


          我無法讓GamepadHapticActuator(也稱為Rumble或Vibration)與我的360控制器一起工作。也不確定Chrome和Firefox是否可以配合這個(gè)控制器或根本不支持它。我可能還需要使用更新的控制器來進(jìn)行測試。但現(xiàn)在,就先這樣吧。


          游戲玩法


          現(xiàn)在我們雖然可以做一些攻擊命中和移動的動作,但到目前為止還做不了其他任何事情,游戲顯得非常無聊。因此我在想,要是誰贏了就給出反饋,然后重新開始就好了。由于這是一款節(jié)奏非常快的游戲,回合數(shù)很短,所以最好還能顯示分?jǐn)?shù)。


          character.strike()方法確定一輪的獲勝者。誰調(diào)用該方法并注冊一個(gè)實(shí)際命中,誰就贏。我打算發(fā)送一個(gè)包含這部分信息的事件并觸發(fā)以下調(diào)用:


          • 顯示獲勝者
          • 增加分?jǐn)?shù)計(jì)數(shù)器
          • 重置字符
          • 開始新一輪倒計(jì)時(shí)


          declare interface FinishEvent extends Event {
              readonly detail?: {
                  winner: number;
              };
          }
          
          this.ctx.canvas.addEventListener("countdown", ((e: FinishEvent) => {
              if (typeof e.detail?.winner === "number") {
                  this.gui.incrementScore(e.detail.winner);
              }
          
              this.startCountdown(e.detail?.winner);
              this.togglePlayers(false);
          }) as EventListener);
          
          this.ctx.canvas.addEventListener("play", () => {
              this.togglePlayers(true);
          });
          


          此時(shí)的事件機(jī)制還不夠復(fù)雜,不足以讓我厭煩重構(gòu)。如果用圖表表示就是這樣:



          游戲加載


          啟動游戲并開始第一輪時(shí),聲音和圖形不但加載滯后,而且會在登陸瀏覽器緩存時(shí)不斷彈出。因此我需要一個(gè)加載策略。


          我通過創(chuàng)建新的Image原型并為其提供src來加載圖像。瀏覽器將開始自動獲取圖像。


          private loadImage(src: string): Promise<HTMLImageElement> {
              const url = `./themes/${this.config.name}/${src}`;
              return fetch(url).then(() => {
                  const img = new Image();
                  img.src = url;
                  if (!this.images.includes(img)) {
                      this.images.push(img);
                  }
                  return img;
              });
          }
          


          現(xiàn)在我可以遍歷主題配置中找到的每個(gè)圖像并加載所有內(nèi)容。圖像存儲在一個(gè)數(shù)組中。


          this.config.players.forEach((player) => {
              const spriteSets = ["default", "move", "attack", "block"];
              spriteSets.forEach((spriteSet) => {
                  Object.keys(player[spriteSet]).forEach((key: string) => {
                      player[spriteSet][key].images.forEach(async (image: string) => {
                          const imageResp = await this.loadImage(image);
                          if (toLoad.includes(imageResp)) {
                              return;
                          }
                          imageResp.onload = () => {
                              this.onAssetLoaded(toLoad);
                          };
                          toLoad.push(imageResp);
                      });
                      this.sprites.push(player[spriteSet][key]);
                  });
              });
          });
          


          每次加載圖像,我都會檢查數(shù)組中的所有promise是否都已解決。如果是,則所有圖像都已加載,發(fā)送一個(gè)事件告知已經(jīng)加載了多少游戲資源。


          private onAssetLoaded(assetList: HTMLImageElement[]) {
              const loadComplete = assetList.every((x) => x.complete);
              const progress = Math.floor(
                  ((assetList.length - assetList.filter((x) => !x.complete).length) / assetList.length) * 100
              );
              const loadingEvent: LoadingEvent = new CustomEvent("loadingEvent", { detail: { progress } });
              this.ctx.canvas.dispatchEvent(loadingEvent);
          
              if (loadComplete) {
                  this.assetsLoaded = true;
              }
          }
          


          映射進(jìn)度信息到<progress>元素。每當(dāng)它達(dá)到100%時(shí),則淡入<canvas>并開始游戲。



          收尾工作


          嚴(yán)格來說,游戲已經(jīng)結(jié)束了。但它仍然是一個(gè)網(wǎng)站,因此應(yīng)該盡力保持其快速、兼容和可訪問性。


          Lighthouse和驗(yàn)證器


          我還沒有添加描述<meta>標(biāo)簽。我將畫布tabindex設(shè)置為1,而它應(yīng)該為0(只是為了讓它可聚焦)。我還有一個(gè)不受Safari支持的SVG favicon,因此當(dāng)我使用它時(shí),添加了一個(gè)apple-touch-icon。并且<input>也缺少一個(gè)<label>。



          漸進(jìn)式web app


          遺漏了一個(gè)Lighthouse類別:PWA。向這個(gè)項(xiàng)目添加PWA功能是有意義的。游戲應(yīng)該允許安裝和離線。



          第一步是清單。這不需要做太多,只需要包含必要的圖標(biāo)、顏色和標(biāo)題字符串,以便在安裝時(shí)格式化主屏幕圖標(biāo)、啟動屏幕和瀏覽器 UI。指定PWA在全屏模式下運(yùn)行,從而隱藏所有瀏覽器UI元素。


          {
              "theme_color": "#1e212e",
              "background_color": "#1e212e",
              "display": "fullscreen",
              "scope": "/",
              "start_url": "/",
              "name": "Attacke!",
              "short_name": "Attacke!",
              "icons": [
                  {
                      "src": "assets/icon-192x192.png",
                      "sizes": "192x192",
                      "type": "image/png"
                  },
                  ...
              ]
          }
          



          我希望游戲PWA只是游戲本身。只要在全屏視圖中打開,任何其他鏈接,例如版權(quán)頁和指向源代碼的鏈接都應(yīng)該在新的瀏覽器窗口中打開。當(dāng)app在常規(guī)瀏覽器窗口中打開時(shí),我非常喜歡讓用戶控制鏈接的行為方式。


          下面的代碼段詢問瀏覽器是否處于全屏模式,如果處于全屏模式,則在新選項(xiàng)卡中打開所有標(biāo)有data-link='external'的鏈接:


          if (window.matchMedia("(display-mode: fullscreen)").matches) {
              document.querySelectorAll("[data-link='external']").forEach((el) => {
                  el.setAttribute("target", "_blank");
                  el.setAttribute("rel", "noopener noreferrer");
              });
          }
          


          離線模式


          下一步是Service Worker。對于有效的PWA,它只需要注冊并為離線請求提供answer即可。我想創(chuàng)建包含所有游戲資源的離線緩存。如此一來在安裝時(shí)會產(chǎn)生相當(dāng)多的網(wǎng)絡(luò)流量。



          緩存進(jìn)來的離線請求相對容易,響應(yīng)也是如此。但是由于需要下載網(wǎng)絡(luò)上的大量資源,因此我只想在用戶安裝app時(shí)才緩存這些資源。否則,在需要時(shí)流式傳輸資源才是更好的選擇。由于我所有的主題都遵循相同的模式,因此我可以遍歷資源,然后返回一個(gè)資源列表:


          export const getGameAssets = (): string[] => {
              const assets = [];
          
              Object.keys(themes).forEach((theme) => {
                  const themeConfig = themes[theme] as themeConfig;
          
                  // add player sprites
                  ["p1", "p2"].forEach((player, pi) => {
                      ["default", "move", "attack", "block"].forEach((action) => {
                          const spriteSet = themeConfig.players[pi][action] as SpriteSet;
          
                          ["n", "ne", "e", "se", "s", "sw", "w", "nw"].forEach(
                              (direction) => {
                                  const images = spriteSet[direction].images as string[];
                                  const paths = images.map(
                                      (image) => `/themes/${theme}/${image}`
                                  );
                                  assets.push(...paths);
                              }
                          );
                      });
                  });
          
                  // add background sprite
                  themeConfig.scene.images.forEach((image) => {
                      assets.push(`/themes/${theme}/${image}`);
                  });
          
                  // add sounds
                  [
                      "bgAudio",
                      "attackAudio",
                      "blockAudio",
                      "collideAudio",
                      "winAudio",
                  ].forEach((audio) => {
                      assets.push(`/themes/${theme}/${themeConfig[audio]}`);
                  });
              });
          
              // return uniques only
              return [...new Set(assets)];
          };
          


          這個(gè)函數(shù)在Service Worker中被調(diào)用,并緩存運(yùn)行全功能游戲所需的一切。


          const cacheAssets = () => {
              const assets = [
                  "/index.html",
                  "/styles.css",
                  "/main.js",
                  "/assets/PressStart2P.woff2",
                  ...getGameAssets(),
              ];
          
              caches.open(cacheName).then(function (cache) {
                  cache.addAll(assets);
              });
          };
          
          channel.addEventListener("message", (event) => {
              switch (event.data.message) {
                  case "cache-assets":
                      cacheAssets();
                      break;
              }
          });
          


          這是什么?cache-assets消息嗎?來自哪里?為什么不是安裝事件監(jiān)聽器呢?


          哈,因?yàn)槲也幌矚gPWA安裝提示的當(dāng)前狀態(tài)。


          自定義安裝按鈕


          Chrome on Android會展示一個(gè)又大又丑的安裝橫幅。Chrome on Desktop也是如此,會彈出窗口。Firefox on Android將安裝按鈕隱藏在瀏覽器菜單中,好吧,至少明確標(biāo)記“安裝”了。最差勁的是Safari,為什么要在共享菜單中隱藏安裝按鈕??


          Chrome提供了實(shí)現(xiàn)自己安裝UX的方法(請注意,這部分的內(nèi)容不符合規(guī)范。出于道德原因,你可能會嗤之以鼻)。安裝提示由事件監(jiān)聽器觸發(fā),可以連接。我可以完全隱藏提示并將其事件綁定到自定義按鈕。單擊此按鈕,將安裝PWA以及附帶的所有資源。


          window.addEventListener("appinstalled", () => {
              button.setAttribute("hidden", "hidden");
              deferredPrompt = null;
              channel.postMessage({ message: "cache-assets" });
          });
          


          沒有未經(jīng)請求的安裝提示,也不會在沒有警告的情況下向用戶的設(shè)備發(fā)送大量下載請求,只有一個(gè)老式的安裝按鈕。非常好。


          總結(jié)


          現(xiàn)在我們完成了一款游戲,完全用typescript編寫并在<canvas>中渲染,甚至可以在所有主流瀏覽器上流暢運(yùn)行,并打包在PWA中。


          構(gòu)建游戲邏輯和繪制圖形讓我獲得了很多樂趣。掌握Photoshop對我?guī)椭艽蟆U页鰡栴}讓我頗費(fèi)一番功夫(說的就是你,Safari),但最后的成果也是最好的回報(bào)。

          擊查看乒乓球特效

          程序員HTML5/javascript打造智能乒乓球?qū)Υ颍?br>

          這個(gè)乒乓球項(xiàng)目是基于html5/canvas畫布,雖然看起來布局是非常簡單的,但是里面的邏輯關(guān)系有點(diǎn)兒復(fù)雜,就比如右邊的電腦怎么接球,在哪個(gè)位置接球,接球后,球的運(yùn)動軌跡,這都是較為復(fù)雜的邏輯判斷,計(jì)算了,當(dāng)然這部分內(nèi)容我們肯定是用javascript去完成,在這個(gè)案例里用的是原生javascript,復(fù)雜才用原生javascript,這樣對邏輯的理解以及對于javascript的運(yùn)用才能更加熟練!想要更加深入一點(diǎn)的,可以把原生javascript封裝成插件,再調(diào)用使用。

          如果有需要學(xué)習(xí)前端的,需要更多HTML5/javascript特效,項(xiàng)目可以關(guān)注頭條號,在往期文章!

          程序員HTML5/javascript打造智能乒乓球源碼:

          代碼過長需要文檔版源碼來我的前端群570946165,源碼已經(jīng)上傳了!

          源:九九互娛


          近日,九九互娛作為CAPCOM(喀普康)官方認(rèn)證的CAPCOM Pro Tour(以下簡稱“CPT” https://capcomprotour.com)2022國內(nèi)賽事授權(quán)方,聯(lián)合ALIENWARE外星人和超威半導(dǎo)體產(chǎn)品(中國)有限公司(以下簡稱“AMD”),共同舉辦CPT中國2022系列賽事。ALIENWARE外星人與AMD將以CPT中國賽區(qū)首席合作伙伴的身份參與到賽事之中。


          CPT中國2022系列賽事,是圍繞《街霸Ⅴ》舉辦的專業(yè)電競格斗賽事,也是中國(大陸)賽區(qū)直通CAPCOM CUP IX的唯一渠道。賽事含括經(jīng)典的CPT中國2022白金賽與全新設(shè)立的CPT 中國 2022 冠軍賽兩段賽道,分別為線上賽和線上+線下賽,兩段賽道的冠軍可以斬獲直通CAPCOM CUP IX資格。首先要舉行的是CPT中國2022冠軍賽,賽程涵蓋7月至11月,總計(jì)6個(gè)場次,賽制采用隨機(jī)抽簽的方式,5場線上賽按CPT賽制進(jìn)行(CPT賽制:雙敗賽制,預(yù)選BO3,8強(qiáng)后BO5),決賽暫定于中國西部國際博覽會線下舉行,最終角逐出冠軍。CPT中國2022冠軍賽不但為冠軍選手準(zhǔn)備了直通CAPCOM CUP IX資格,還有加磅賽事獎(jiǎng)金。此次比賽在給予頂級玩家與世界級選手同臺競技的機(jī)會的同時(shí),還可以讓更多游戲愛好者了解到格斗電競不俗的觀賞性。


          此次賽事合作,ALIENWARE外星人與AMD旨在推動電競文化與電競格斗賽事在中國的發(fā)展。ALIENWARE外星人作為一線游戲PC品牌,將發(fā)揮其在玩家圈層的影響,為CPT中國2022賽事助力。ALIENWARE外星人一直以來堅(jiān)持追求更強(qiáng)的性能,不斷刷新視覺、聽覺和觸覺等多維度的感官享受,為玩家?guī)砣掠螒蝮w驗(yàn)。旗下無論是筆記本、主機(jī)還是外設(shè)產(chǎn)品都得到了玩家們的廣泛喜愛,其中M系列產(chǎn)品一直以硬核產(chǎn)品力詮釋何謂“生來強(qiáng)悍”。至高搭載AMD 銳龍9 6900HX標(biāo)壓處理器ALIENWARE m17 R5游戲本與至高搭載AMD 銳龍9 5950X處理器的ALIENWARE AURORA R14游戲主機(jī),更是不少硬核玩家夢想的電競裝備。同樣的,AMD長期以來致力為消費(fèi)者提供性能出色的核心硬件產(chǎn)品和解決方案,不僅滿足了爆炸式增長的數(shù)據(jù)中心計(jì)算需求,還為更多消費(fèi)者打造了更輕、更薄、更驚人的游戲平臺。其核心產(chǎn)品還有著優(yōu)秀的游戲性能和超長的電池續(xù)航能力。“選銳龍,像我一樣戰(zhàn)斗”不止體現(xiàn)在技術(shù)之上,AMD還曾多次贊助電子競技賽事,讓更多消費(fèi)者透過賽事看到其背后生機(jī)勃勃的青春活力,推動電競行業(yè)的發(fā)展。



          CPT中國2022冠軍賽參賽選手火熱招募中,ALIENWARE外星人與AMD共同邀請硬核玩家參與這場中國頂級格斗電競比賽。

          ?CAPCOM CO., LTD. 2016, 2020 ALL RIGHTS RESERVED.



          *關(guān)注微信服務(wù)號:外星人ALIENWARE(微信號:ALIENWARE-1996)或AMD中國(微信號:amdchina),點(diǎn)擊底部菜單欄,即可進(jìn)入賽事報(bào)名頁面:

          https://www.matchon.net/match/info/17155.html。

          加入這場電競格斗賽事,一起無憾搏戰(zhàn),成就爭霸傳奇。


          主站蜘蛛池模板: 国产精品熟女一区二区| 一区二区三区日本视频| 国产乱码一区二区三区爽爽爽| 免费在线观看一区| 91福利视频一区| 亲子乱av一区二区三区| 中文字幕日韩欧美一区二区三区| 污污内射在线观看一区二区少妇 | 国产精品一区在线观看你懂的| 国产成人av一区二区三区不卡| 精品免费国产一区二区三区| 亚洲av成人一区二区三区在线观看 | 亚洲国产精品一区二区成人片国内| 精品亚洲一区二区| 91一区二区视频| 麻豆AV一区二区三区久久| 中文字幕日韩人妻不卡一区| 女人和拘做受全程看视频日本综合a一区二区视频 | 成人精品一区二区户外勾搭野战| 亚洲一区爱区精品无码| 国产电影一区二区| 亚洲AV永久无码精品一区二区国产| 日本一区二区三区免费高清在线 | 精品国产乱子伦一区二区三区 | 亚洲Av无码一区二区二三区| 亚洲日韩一区精品射精| 一区二区三区久久精品| 亚洲国产老鸭窝一区二区三区| 成人精品一区二区三区校园激情| 美女视频一区二区三区| 日韩精品一区二区三区中文| 日韩在线视频不卡一区二区三区 | 在线观看精品视频一区二区三区| 痴汉中文字幕视频一区| 亚洲AV日韩综合一区尤物| 综合人妻久久一区二区精品| 成人国产精品一区二区网站公司| 国产精品免费一区二区三区四区| 在线精品自拍亚洲第一区| 精品国产一区AV天美传媒| 国产福利电影一区二区三区久久久久成人精品综合 |