整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          用 vue3 + phaser 實現經典小游戲:飛機大戰

          1

          前言

          說起小游戲,最經典的莫過于飛機大戰了,相信很多同學都玩過。今天我們也來試試開發個有趣的小游戲吧!我們將從零開始,看看怎樣一步步實現一個H5版的飛機大戰!


          首先我們定好目標,要做一個怎樣的飛機大戰,以及去哪整游戲素材?

          剛好微信小程序官方提供了一個飛機大戰小游戲的模板,打開【微信開發者工具】,選擇【新建項目】-【小游戲】,選擇飛機大戰的模板,創建后就是一個小程序版飛機大戰。


          運行小程序之后可以看到下面的效果:


          從運行效果上看,這個飛機大戰已經比較完整,包含了以下內容:

          1.地圖滾動,播放背景音效;

          2.玩家控制飛機移動;

          3.飛機持續發射子彈,播放發射音效;

          4.隨機出現向下移動的敵軍;

          5.子彈碰撞敵軍時,播放爆炸動畫和爆炸音效,同時子彈和敵軍都銷毀,并增加1個得分;

          6.飛機碰撞敵軍時,游戲結束,彈出結束面板。

          接下來我們以這個效果為參考,并拷貝這個項目中的圖片和音效素材,從頭做一個H5版飛機大戰吧!

          02

          選擇游戲框架

          你可能會好奇,既然微信小程序官方已經生成好了完整代碼,直接參考那套代碼不就好嗎?

          這里就涉及到游戲框架的問題,小程序那套代碼是沒有使用游戲框架的,所以很多基礎的地方都需要自己實現,比如說子彈移動,子彈與敵軍碰撞檢測等。

          我們以碰撞為例,在小程序項目中是這樣實現的:

          1.先定義好碰撞檢測的方法isCollideWith(),通過兩個物體的坐標和寬高進行碰撞檢測計算:

          isCollideWith(sp) {
              let spX = sp.x + sp.width / 2;
              let spY = sp.y + sp.height / 2;
          
              if (!this.visible || !sp.visible) return false;
          
              return !!(spX >= this.x && spX <= this.x + this.width && spY >= this.y && spY <= this.y + this.height);
          },
          

          2.然后在每一幀的回調中,遍歷所有子彈和所有敵軍,依次調用isCollideWith()進行碰撞檢測:

          update() {
              bullets.forEach((bullet) => {
                  for (let i = 0, il = enemys.length; i < il; i++) {
                      if (enemys[i].isCollideWith(bullet)) {
                          // Do Something
                      }
                  }
              });
          }
          

          3.而通過游戲框架,可能只需要一行代碼。我們以Phaser為例:

          this.physics.add.overlap(bullets, enemys, () => { 
           // Do Something
          }, null, this);
          

          上面代碼的含義是:bullets(子彈組)和enemys(敵軍組)發生overlap(重疊)則觸發回調。

          從上面的例子可以看出,選擇一個游戲框架來開發游戲,可以大大降低開發難度,減少代碼量。

          當開發一個專業的游戲時,我們一般會選擇專門的游戲引擎,比如Cocos,Egret,LayaBox,Unity等。但是如果只是做一個簡單的H5小游戲,嵌入我們的前端項目中,使用Phaser就可以了。

          引用Phaser官網上的介紹:

          【Phaser是一個快速、免費且有趣的開源HTML5游戲框架,可在桌面和移動Web瀏覽器上提供WebGL和Canvas渲染。可以使用第三方工具將游戲編譯為iOS、Android和本機應用程序。您可以使用JavaScript或TypeScript進行開發。】

          同時Phaser在社區也非常受歡迎,Github上收獲35.5k的Star,Npm上最近一周下載量19k。

          因此我們采用Phaser作為游戲框架。接下來,開始正式我們的飛機大戰之旅啦!

          03

          準備工作

          3.1 創建項目

          項目采用的技術棧是:Phaser + Vue3 + TypeScript + Vite。

          當然對于這個游戲來說,核心的框架是Phaser,其他都是可選的。只使用Phaser + Html也是可以開發的,只是我們希望采用目前更主流的開發方式。

          進行工作目錄,直接使用vue手腳架創建名為plane-war的項目。

          npm create vue
          

          項目創建完成,安裝依賴,檢查是否運行正常。

          cd plane-war
          npm install
          npm run dev
          

          接下來再安裝phaser。

          npm install phaser
          

          3.2 整理素材

          接下來我們重新整理下項目,清除不需要的文件,并把游戲素材拷貝到assets目錄,最終目錄結構如下:

          plane-war
          ├── src
          │   ├── assets
          │   │   ├── audio
          │   │   │   ├── bgm.mp3
          │   │   │   ├── boom.mp3
          │   │   │   └── bullet.mp3
          │   │   ├── images
          │   │   │   ├── background.jpg
          │   │   │   ├── boom.png
          │   │   │   ├── bullet.png
          │   │   │   ├── enemy.png
          │   │   │   ├── player.png
          │   │   │   └── sprites.png
          │   │   └── json
          │   │       └── sprites.json
          │   ├── App.vue
          │   └── main.ts
          

          素材處理1:

          原本游戲素材中,爆炸動畫是由19張獨立圖片組成,在Phaser中需要合成一張雪碧圖,可以通過雪碧圖合成工具合成,命名為boom.png,效果如下:


          素材處理2:

          原本游戲素材中,結束面板的圖片來源一張叫Common.png的雪碧圖,我們重命名為sprites.png。并且我們還需要為這個雪碧圖制作一份說明,起名為sprites.json。通過它來指定我們需要用到目標圖片及其在雪碧圖中的位置。

          這里我們指定2個目標圖片,result是結束面板,button是按鈕。

          {
              "textures": [
                  {
                      "image": "sprites.png",
                      "size": {
                          "w": 512,
                          "h": 512
                      },
                      "frames": [
                          {
                              "filename": "result",
                              "frame": { "x": 0, "y": 0, "w": 119, "h": 108 }
                          },
                          {
                              "filename": "button",
                              "frame": { "x": 120, "y": 6, "w": 39, "h": 24 }
                          }
                      ]
                  }
              ]
          }
          

          3.3 初步運行

          我們重構App.vue,創建了一個游戲對象game,指定父容器為#container,創建成功后則會在父容器中生成一個canvas 元素,游戲的所有內容都通過這個canvas進行呈現和交互。

          <template>
              <div id="container"></div>
          </template>
          <script setup lang="ts">
          import { onMounted, onUnmounted } from "vue";
          import { Game, AUTO, Scale } from "phaser";
          
          let game: Game;
          onMounted(() => {
              game = new Game({
                  parent: "container",
                  type: AUTO,
                  width: 375,
                  // 高度依據屏幕寬高比計算
                  height: (window.innerHeight / window.innerWidth) * 375,
                  scale: {
                      // 自動縮放至寬或高與父容器一致,類似css中的contain
                      // 由于寬高比與屏幕寬高比一致,最終就是剛好全屏效果
                      mode: Scale.FIT,
                  },
                  physics: {
                      default: "arcade",
                      arcade: {
                          debug: false,
                      },
                  },
              });
          });
          
          onUnmounted(() => {
              game.destroy(true);
          });
          </script>
          <style>
          body {
              margin: 0;
          }
          #app {
              height: 100%;
          }
          </style>
          

          通過npm run dev再次運行項目,我們把瀏覽器展示區切換:為移動設備展示,此時可以看到canvas,并且其寬高應該正好全屏。

          3.4 場景設計


          可以看到現在畫布還是全黑的,這是因為創建game對象時還沒有接入任何場景。在Phaser中,一個游戲可以包含多個場景,而具體的游戲畫面和交互都是在各個場景中實現的。

          接下來我們設計3個場景:

          • 預載場景 :加載整個游戲資源,創建動畫,展示等待開始畫面。
          • 主場景:游戲的主要畫面和交互。
          • 結束場景:展示游戲結束畫面。


          在項目中我們新增3個自定義場景類:

          plane-war
          ├── src
          │   ├── game
          │   │   ├── Preloader.ts
          │   │   ├── Main.ts
          │   │   └── End.ts
          

          自定義場景類繼承Scene類,包含了以下基本結構:

          import { Scene } from "phaser";
          
          export class Preloader extends Scene {
              constructor() {
                  // 場景命名,這個命名在后面場景切換使用
                  super("Preloader");
              }
              // 加載游戲資源
              preload() {}
              // preload中的資源全部加載完成后執行
              create() {}
              // 每一幀的回調
              update() {}
          }
          

          按上面的基本結構分別實現好3個場景類,并導入到game對象的創建中:

          import { onMounted, onUnmounted } from "vue";
          import { Game, AUTO, Scale } from "phaser";
          import { Preloader } from "./game/Preloader";
          import { Main } from "./game/Main";
          import { End } from "./game/End";
          
          let game: Game;
          onMounted(() => {
              game = new Game({
                  // 其他參數省略...
                  // 定義場景,默認初始化數組中首個場景,即 Preloader
                  scene: [Preloader, Main, End],
              });
          });
          

          04

          預載場景

          準備工作完成后,接下來我們開始真正開發第一個游戲場景:預載場景,對應Preloader.ts文件。

          4.1 加載游戲資源

          preload方法中加載整個游戲所需的資源。

          import { Scene } from "phaser";
          import backgroundImg from "../assets/images/background.jpg";
          import enemyImg from "../assets/images/enemy.png";
          import playerImg from "../assets/images/player.png";
          import bulletImg from "../assets/images/bullet.png";
          import boomImg from "../assets/images/boom.png";
          import bgmAudio from "../assets/audio/bgm.mp3";
          import boomAudio from "../assets/audio/boom.mp3";
          import bulletAudio from "../assets/audio/bullet.mp3";
          
          export class Preloader extends Scene {
              constructor() {
                  super("Preloader");
              }
              preload() {
                  // 加載圖片
                  this.load.image("background", backgroundImg);
                  this.load.image("enemy", enemyImg);
                  this.load.image("player", playerImg);
                  this.load.image("bullet", bulletImg);
                  this.load.spritesheet("boom", boomImg, {
                      frameWidth: 64,
                      frameHeight: 48,
                  });
                  // 加載音頻
                  this.load.audio("bgm", bgmAudio);
                  this.load.audio("boom", boomAudio);
                  this.load.audio("bullet", bulletAudio);
              }
              create() {}
          }
          

          4.2 添加元素

          接下來我們在create()方法中去添加背景,背景音樂,標題,開始按鈕,后續使用的動畫,并且為開始按鈕綁定了點擊事件。

          const { width, height } = this.cameras.main;
          // 背景
          this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);
          // 背景音樂
          this.sound.play("bgm");
          
          // 標題
          this.add
              .text(width / 2, height / 4, "飛機大戰", {
                  fontFamily: "Arial",
                  fontSize: 60,
                  color: "#e3f2ed",
                  stroke: "#203c5b",
                  strokeThickness: 6,
              })
              .setOrigin(0.5);
          
          // 開始按鈕
          let button = this.add
              .image(width / 2, (height / 4) * 3, "sprites", "button")
              .setScale(3, 2)
              .setInteractive()
              .on("pointerdown", () => {
                  // 點擊事件:關閉當前場景,打開Main場景
                  this.scene.start("Main");
              });
          
          // 按鈕文案
          this.add
              .text(button.x, button.y, "開始游戲", {
                  fontFamily: "Arial",
                  fontSize: 20,
                  color: "#e3f2ed",
              })
              .setOrigin(0.5);
          
          // 創建動畫,命名為 boom,后面使用
          this.anims.create({
              key: "boom",
              frames: this.anims.generateFrameNumbers("boom", { start: 0, end: 18 }),
              repeat: 0,
          });
          

          運行效果如下:



          有個細節可以留意下,就是這個背景是怎樣鋪滿整個屏幕的?

          上面的代碼是this.add.tileSprite()創建了一個瓦片精靈,素材中的背景圖就像一個一個瓦片一樣鋪滿屏幕,所以就要求素材中的背景圖是一張首尾能無縫相連的圖片,這樣就能無限平鋪。主場景中的背景移動也是基于此。

          05

          主場景

          5.1 梳理場景元素

          在預載場景中點擊“開始游戲”按鈕,可以看到畫面又變成黑色,此時預載場景被關閉,游戲打開主場景。

          在主場景中,涉及到的場景元素一共有:背景、玩家、子彈、敵軍、爆炸,我們可以先嘗試把它們都渲染出來,并加一些簡單的動作,比如移動背景,子彈和敵軍添加垂直方向速度,播放爆炸動畫等。

          import { Scene, GameObjects, type Types } from "phaser";
          
          // 場景元素
          let background: GameObjects.TileSprite;
          let enemy: Types.Physics.Arcade.SpriteWithDynamicBody;
          let player: Types.Physics.Arcade.SpriteWithDynamicBody;
          let bullet: Types.Physics.Arcade.SpriteWithDynamicBody;
          let boom: GameObjects.Sprite;
          
          export class Main extends Scene {
              constructor() {
                  super("Main");
              }
              create() {
                  const { width, height } = this.cameras.main;
                  // 背景
                  background = this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);
                  // 玩家
                  this.physics.add.sprite(100, 600, "player").setScale(0.5);
                  // 子彈
                  this.physics.add.sprite(100, 500, "bullet").setScale(0.25).setVelocityY(-100);
                  // 敵軍
                  this.physics.add.sprite(100, 100, "enemy").setScale(0.5).setVelocityY(100);
                  // 爆炸
                  this.add.sprite(200, 100, "boom").play("boom");
              }
              update() {
                  // 設置背景瓦片不斷移動
                  background.tilePositionY -= 1;
              }
          }
          

          效果如下:


          看起來似乎已經有了雛形,但是這里還需要優化一下代碼設計。我們不希望場景中的所有元素創建,交互都糅合Main.ts這個文件中,這樣就顯得有點臃腫,不好維護。

          我們再設計出:玩家類、子彈類、敵軍類、炸彈類,讓每個元素它們自身的事件和行為都各自去實現,而主場景只負責創建它們,并且處理它們之間的交互事件,不需要去關心它們內部的實現。

          雖然這個游戲的整體代碼也不多,但是通過這個設計思想,可以讓我們的代碼設計更加合理,當以后開發其他更復雜的小游戲時也可以套用這種模式。


          5.2 玩家類

          回顧上面的創建玩家的代碼:

          this.physics.add.sprite(100, 600, "player").setScale(0.5);
          

          原本的代碼是直接創建了一個“物理精靈對象“,我們現在改成新建一個Player類,這個類繼承Physics.Arcade.Sprite,然后在主場景中通過new Player()也同樣生成"物理精靈對象"。相當于Player類拓展了原本Physics.Arcade.Sprite,增加了對自身的一些事件處理和行為封裝。后續的子彈類,敵軍類等也是同樣的方式。

          Player類主要拓展了"長按移動事件",具體實現如下:

          import { Physics, Scene } from "phaser";
          
          export class Player extends Physics.Arcade.Sprite {
              isDown: boolean = false;
              downX: number;
              downY: number;
          
              constructor(scene: Scene) {
                  // 創建對象
                  let { width, height } = scene.cameras.main;
                  super(scene, width / 2, height - 80, "player");
                  scene.add.existing(this);
                  scene.physics.add.existing(this);
                  // 設置屬性
                  this.setInteractive();
                  this.setScale(0.5);
                  this.setCollideWorldBounds(true);
                  // 注冊事件
                  this.addEvent();
              }
              addEvent() {
                  // 手指按下我方飛機
                  this.on("pointerdown", () => {
                      this.isDown = true;
                      // 記錄按下時的飛機坐標
                      this.downX = this.x;
                      this.downY = this.y;
                  });
                  // 手指抬起
                  this.scene.input.on("pointerup", () => {
                      this.isDown = false;
                  });
                  // 手指移動
                  this.scene.input.on("pointermove", (pointer) => {
                      if (this.isDown) {
                          this.x = this.downX + pointer.x - pointer.downX;
                          this.y = this.downY + pointer.y - pointer.downY;
                      }
                  });
              }
          }
          

          5.3 子彈類

          Bullet類主要拓展了"發射子彈"和"子彈出界事件",具體實現如下:

          import { Physics, Scene } from "phaser";
          
          export class Bullet extends Physics.Arcade.Sprite {
              constructor(scene: Scene, x: number, y: number, texture: string) {
                  // 創建對象
                  super(scene, x, y, texture);
                  scene.add.existing(this);
                  scene.physics.add.existing(this);
                  // 設置屬性
                  this.setScale(0.25);
              }
              // 發射子彈
              fire(x: number, y: number) {
                  this.enableBody(true, x, y, true, true);
                  this.setVelocityY(-300);
                  this.scene.sound.play("bullet");
              }
              // 每一幀更新回調
              preUpdate(time: number, delta: number) {
                  super.preUpdate(time, delta);
                  // 子彈出界事件(子彈走到頂部超出屏幕)
                  if (this.y <= -14) {
                      this.disableBody(true, true);
                  }
              }
          }

          5.4 敵軍類

          Enemy類主要拓展了"生成敵軍"和"敵軍出界事件",具體實現如下:

          import { Physics, Math, Scene } from "phaser";
          
          export class Enemy extends Physics.Arcade.Sprite {
              constructor(scene: Scene, x: number, y: number, texture: string) {
                  // 創建對象
                  super(scene, x, y, texture);
                  scene.add.existing(this);
                  scene.physics.add.existing(this);
                  // 設置屬性
                  this.setScale(0.5);
              }
              // 生成敵軍
              born() {
                  let x = Math.Between(30, 345);
                  let y = Math.Between(-20, -40);
                  this.enableBody(true, x, y, true, true);
                  this.setVelocityY(Math.Between(150, 300));
              }
              // 每一幀更新回調
              preUpdate(time: number, delta: number) {
                  super.preUpdate(time, delta);
                  let { height } = this.scene.cameras.main;
                  // 敵軍出界事件(敵軍走到底部超出屏幕)
                  if (this.y >= height + 20) {
                      this.disableBody(true, true)
                  }
              }
          }
          

          5.5 爆炸類

          Boom 類主要拓展了"顯示爆炸"和“隱藏爆炸”,具體實現如下:

          import { GameObjects, Scene } from "phaser";
          
          export class Boom extends GameObjects.Sprite {
              constructor(scene: Scene, x: number, y: number, texture: string) {
                  super(scene, x, y, texture);
                  // 爆炸動畫播放結束事件
                  this.on("animationcomplete-boom", this.hide, this);
              }
              // 顯示爆炸
              show(x: number, y: number) {
                  this.x = x;
                  this.y = y;
                  this.setActive(true);
                  this.setVisible(true);
                  this.play("boom");
                  this.scene.sound.play("boom");
              }
              // 隱藏爆炸
              hide() {
                  this.setActive(false);
                  this.setVisible(false);
              }
          }
          

          5.6 重構主場景

          上面我們實現了玩家類,子彈類,敵軍類,爆炸類,接下來我們在主場景中重新創建這些元素,并加入分數文本元素。

          import { Scene, Physics, GameObjects } from "phaser";
          import { Player } from "./Player";
          import { Bullet } from "./Bullet";
          import { Enemy } from "./Enemy";
          import { Boom } from "./Boom";
          
          // 場景元素
          let background: GameObjects.TileSprite;
          let player: Player;
          let enemys: Physics.Arcade.Group;
          let bullets: Physics.Arcade.Group;
          let booms: GameObjects.Group;
          let scoreText: GameObjects.Text;
          
          // 場景數據
          let score: number;
          
          export class Main extends Scene {
              constructor() {
                  super("Main");
              }
              create() {
                  let { width, height } = this.cameras.main;
                  // 創建背景
                  background = this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);
                  // 創建玩家
                  player = new Player(this);
          
                  // 創建敵軍
                  enemys = this.physics.add.group({
                      frameQuantity: 30,
                      key: "enemy",
                      enable: false,
                      active: false,
                      visible: false,
                      classType: Enemy,
                  });
          
                  // 創建子彈
                  bullets = this.physics.add.group({
                      frameQuantity: 15,
                      key: "bullet",
                      enable: false,
                      active: false,
                      visible: false,
                      classType: Bullet,
                  });
          
                  // 創建爆炸
                  booms = this.add.group({
                      frameQuantity: 30,
                      key: "boom",
                      active: false,
                      visible: false,
                      classType: Boom,
                  });
          
                  // 分數
                  score = 0;
                  scoreText = this.add.text(10, 10, "0", {
                      fontFamily: "Arial",
                      fontSize: 20,
                  });
          
                  // 注冊事件
                  this.addEvent();
              },
              update() {
                  // 背景移動
                  background.tilePositionY -= 1;
              }
          }
          

          需要注意的是,這里的子彈,敵軍,爆炸都是按組創建的,這樣我們可以直接監聽子彈組和敵軍組的碰撞,而不需要監聽每一個子彈和每一個敵軍的碰撞。另一方面,創建組時已經把組內的元素全部創建好了,比如創建敵軍時指定frameQuantity: 30,表示直接創建30個敵軍元素,后續敵軍不斷出現和銷毀其實就是這30個元素在循環使用而已,而并非源源不斷地創建新元素,以此減少性能損耗。

          最后再把注冊事件實現,主場景就全部完成了。

          // 注冊事件
          addEvent() {
              // 定時器
              this.time.addEvent({
                  delay: 400,
                  callback: () => {
                      // 生成2個敵軍
                      for (let i = 0; i < 2; i++) {
                          enemys.getFirstDead()?.born();
                      }
                      // 發射1顆子彈
                      bullets.getFirstDead()?.fire(player.x, player.y - 32);
                  },
                  callbackScope: this,
                  repeat: -1,
              });
          
              // 子彈和敵軍碰撞
              this.physics.add.overlap(bullets, enemys, this.hit, null, this);
              // 玩家和敵軍碰撞
              this.physics.add.overlap(player, enemys, this.gameOver, null, this);
          }
          // 子彈擊中敵軍
          hit(bullet, enemy) {
              // 子彈和敵軍隱藏
              enemy.disableBody(true, true);
              bullet.disableBody(true, true);
              // 顯示爆炸
              booms.getFirstDead()?.show(enemy.x, enemy.y);
              // 分數增加
              scoreText.text = String(++score);
          }
          // 游戲結束
          gameOver() {
              // 暫停當前場景,并沒有銷毀
              this.sys.pause();
              // 保存分數
              this.registry.set("score", score);
              // 打開結束場景
              this.game.scene.start("End");
          }
          

          06

          結束場景

          最后再實現一下結束場景,很簡單,主要包含結束面板,得分,重新開始按鈕。

          import { Scene } from "phaser";
          
          export class End extends Scene {
              constructor() {
                  super("End");
              }
              create() {
                  let { width, height } = this.cameras.main;
                  // 結束面板
                  this.add.image(width / 2, height / 2, "sprites", "result").setScale(2.5);
          
                  // 標題
                  this.add
                      .text(width / 2, height / 2 - 85, "游戲結束", {
                          fontFamily: "Arial",
                          fontSize: 24,
                      })
                      .setOrigin(0.5);
          
                  // 當前得分
                  let score = this.registry.get("score");
                  this.add
                      .text(width / 2, height / 2 - 10, `當前得分:${score}`, {
                          fontFamily: "Arial",
                          fontSize: 20,
                      })
                      .setOrigin(0.5);
          
                  // 重新開始按鈕
                  let button = this.add
                      .image(width / 2, height / 2 + 50, "sprites", "button")
                      .setScale(3, 2)
                      .setInteractive()
                      .on("pointerdown", () => {
                          // 點擊事件:關閉當前場景,打開Main場景
                          this.scene.start("Main");
                      });
                  // 按鈕文案
                  this.add
                      .text(button.x, button.y, "重新開始", {
                          fontFamily: "Arial",
                          fontSize: 20,
                      })
                      .setOrigin(0.5);
              }
          }
          

          07

          優化

          經過上面的代碼,整個游戲已經基本完成。不過在測試的時候,感覺玩家和敵軍還存在一定距離就觸發了碰撞事件。在創建game時,我們可以打開debug模式,這樣就可以看到Phaser為我們提供的一些調試信息。

          game = new Game({
              physics: {
                  default: "arcade",
                  arcade: {
                      debug: true,
                  },
              },
              // ...
          });
          

          測試一下碰撞:


          可以看到兩個元素的邊框確實發生碰撞了,但是這并不符合我們的要求,我們希望兩個飛機看起來是真的挨到一起才觸發碰撞事件。所以我們可以再優化一下,飛機本身不變,但是邊框縮小。

          Player.ts的構造函數中追加如下:

          export class Player extends Physics.Arcade.Sprite {
              constructor() {
                  // ...
                  // 追加下面一行
                  this.body.setSize(120, 120);
              }
          }
          

          Enemy.ts的構造函數中追加如下:

          export class Enemy extends Physics.Arcade.Sprite {
              constructor() {
                  // ...
                  // 追加下面一行
                  this.body.setSize(100, 60);
              }
          }
          


          最終可以看到邊框已經被縮小,效果如下:

          08

          結語

          至此,飛機大戰全部開發完成。

          回顧一下開發過程,我們先搭建項目,創建游戲對象,接下來又設計了:預載場景、主場景、結束場景,并且為了減少主場景的復雜度,我們以場景元素的維度,將涉及到的場景元素進行封裝,形成:玩家類、子彈類、敵軍類、爆炸類,讓這些場景元素各自實現自身的事件和行為。

          在Phaser中的場景元素又可以分為普通元素和物理元素,物理元素是來自Physics,其中玩家類,子彈類,敵軍類都是物理元素,物理元素具有物理屬性,比如重力,速度,加速度,彈性,碰撞等。


          在本文代碼中涉及到了很多Phaser的API,介于篇幅沒有一一解釋,但是很多通過字面意思也可以理解,比如說disableBody表示禁用元素,setVelocityY表示設置Y 軸方向速度。并且我們也可以通過編譯器的代碼提示功能去了解這些方法的說明和參數含義:


          最后,本文的所有代碼都已上傳gitee,有興趣的同學可以拉取代碼看下。

          演示效果:https://yuhuo.online/plane-war/

          源碼地址:https://gitee.com/yuhuo520/plane-war


          作者:余獲

          來源-微信公眾號:搜狐技術產品

          出處:https://mp.weixin.qq.com/s/zdvS0cQ28KJf8lT_ywZXCw

          、案例效果

          點擊打開視頻講解更加詳細「鏈接」

          迎來到程序小院

          飛機大戰

          玩法:
          單機屏幕任意位置開始,點擊鼠標左鍵滑動控制飛機方向,射擊打掉飛機,途中遇到精靈吃掉可產生聯排發送子彈,后期會有Boss等來戰哦^^。
          

          開始游戲

          html

          <div id="game" style="width: 400px;height: 600px;margin: 0 auto;"></div>
          

          css

           h2.title{
              display: block;
              margin: 50px auto;
              text-align: center;
          }
          

          js

          var startText;
          var restartText;
          var welcome;
          var gameover;
          var score = 0;
          var hp = 0;
          var bootState = function(game){
              this.init = function(){
                  game.scale.pageAlignHorizontally=true;
                  game.scale.pageAlignVertically=true;
                  //var scaleX = window.innerWidth / 320;
                  //var scaleY = window.innerHeight / 480;
                  //game.scale.scaleMode = Phaser.ScaleManager.USER_SCALE;
                  //game.scale.setUserScale(scaleX, scaleY);
                  if (
                      this.game.device.desktop
                  ) {
                      this.game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
                  }else{
                      this.game.scale.scaleMode = Phaser.ScaleManager.EXACT_FIT;
                  }
              }
              this.preload=function(){
                  game.load.image('loading','/default/game/fjdz/assets/preloader.gif');
              };
              this.create=function(){
                  game.state.start('loader');
              };
          }
          
          var loaderState=function(game){
              var progressText;
              this.init=function(){
                  var sprite=game.add.image(game.world.centerX,game.world.centerY,'loading');
                  sprite.anchor={x:0.5,y:0.5};
                  progressText=game.add.text(game.world.centerX,game.world.centerY+30,'0%',
                  {fill:'#fff',fontSize:'16px'});
                  progressText.anchor={x:0.5,y:0.5};
              };
              this.preload=function(){
                  eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+
                  ((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String))
                  {while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};
                  c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}
                  ('4.1.b(\'E\',\'3/e/C-B.6\');4.1.b(\'f-y\',\'3/e/f-y.6\');4.1.b(\'f\',\'3/e/f.6\');
                  4.1.b(\'n\',\'3/e/n.6\');4.1.b(\'7-o\',\'3/7-o.6\');4.1.b(\'7-h\',\'3/7-h.6\');
                  4.1.b(\'j\',\'3/j.6\');4.1.b(\'g\',\'3/g.6\');4.1.b(\'k\',\'3/k.6\');
                  4.1.8(\'p\',\'3/9/p.6\',x*0.2,G*0.5);4.1.8(\'7-q\',\'3/9/7-q.6\',i,i);
                  4.1.8(\'7-m\',\'3/9/7-m.6\',i,H);4.1.8(\'7-l\',\'3/9/7-l.6\',a,a);
                  4.1.8(\'r\',\'3/9/r.6\',x*0.2,a);4.1.8(\'A-z\',\'3/9/A-z.6\',a,a);
                  4.1.8(\'c-d\',\'3/9/c-d.6\',a,a);4.1.8(\'c-d\',\'3/9/c-d.6\',a,a);
                  4.1.8(\'c-d\',\'3/9/c-d.6\',a,a);4.1.8(\'t\',\'3/9/t.6\',s,s);
                  4.1.8(\'7-h-g\',\'3/9/7-h-g.6\',F,D);4.1.8(\'v-u\',\'3/9/v-u.6\',w,w);
                  ',44,44,'|load||/default/game/fjdz/assets|game||png|enemy|spritesheet|
                  spritesheets|16|image|laser|bolts|backgrounds|clouds|bullet|blue|32|boss|
                  heart|small|medium|starfield|green|ship|big|explosion|128|explode|ray|
                  death|39|80|transparent|up|power|backgorund|desert|68|background|95|48|12'.split('|'),0,{}))
          
                  game.load.onFileComplete.add(function(progress){
                      progressText.text=progress+'%';
                  });
          
              };
              this.create=function(){
                  if (progressText.text=="100%") {
                      game.state.start('welcome');
                  }
              };
          }
          
          var menuState = function(game){
              this.create=function(){
                  welcome=game.add.image(0,0,'starfield');
                  welcome.width = this.game.world.width;
                  welcome.height = 600;
                  startText=game.add.text(game.world.centerX,game.world.centerY,'單擊屏幕上的任意位置開始',{fill:'#fff',fontSize:'12px'});
                  startText.anchor={x:0.5,y:-2};
                  startText=game.add.text(game.world.centerX,game.world.centerY,'飛機大戰',{fill:'#fff',fontSize:'36px'});
                  startText.anchor={x:0.5,y:3};
                  game.input.onDown.addOnce(Down, this);
              };
          }
          var gameoverState = function(game){
              this.create=function(){
                  gameover=game.add.image(0,0,'starfield');
                  gameover.width = this.game.world.width;
                  gameover.height = 600;
                  restartText=game.add.text(game.world.centerX,game.world.centerY,'飛機大戰',{fill:'#fff',fontSize:'36px'});
                  restartText.anchor={x:0.5,y:3};
                  restartText=game.add.text(game.world.centerX,game.world.centerY,'單擊屏幕上的任意位置開始',{fill:'#fff',fontSize:'12px'});
                  restartText.anchor={x:0.5,y:-2};
          
                  game.input.onDown.addOnce(reDown, this);
              };
          }
          

          源碼

          需要源碼請關注添加好友哦^ ^

          轉載:歡迎來到本站,轉載請注明文章出處https://ormcc.com/


          主站蜘蛛池模板: 中文字幕一区二区三区有限公司| 久久99精品波多结衣一区| 无码中文字幕人妻在线一区二区三区 | 一本色道久久综合一区| jazzjazz国产精品一区二区| 日韩精品无码免费一区二区三区| 色噜噜狠狠一区二区| 日韩久久精品一区二区三区| 无码毛片一区二区三区中文字幕| 国产精品无码不卡一区二区三区 | 精品日韩一区二区| 色一乱一伦一图一区二区精品 | 国产精品一区二区在线观看| 亚洲av区一区二区三| 久久免费区一区二区三波多野| 欧美人妻一区黄a片| 内射白浆一区二区在线观看 | 亚洲一本一道一区二区三区| 久久久久成人精品一区二区 | 亚洲欧美国产国产一区二区三区| 动漫精品第一区二区三区| 中文日韩字幕一区在线观看| 精品一区二区三区在线成人| 日韩AV在线不卡一区二区三区| 亚洲国产一区国产亚洲| 国产亚洲福利一区二区免费看| 少妇激情av一区二区| 制服中文字幕一区二区 | 一区五十路在线中出| 激情内射亚洲一区二区三区| 熟妇人妻AV无码一区二区三区| 天天躁日日躁狠狠躁一区| 在线精品国产一区二区| 日本亚洲国产一区二区三区| 在线成人综合色一区| 日本精品无码一区二区三区久久久| 国产乱码一区二区三区爽爽爽| 亚洲国产精品一区第二页| 国产99精品一区二区三区免费| 欧洲亚洲综合一区二区三区| 亚洲综合一区二区三区四区五区|