JavaScript >> Javascript 文檔 >  >> JavaScript

Phaser 3 和 Tiled:構建平台遊戲

簡介

Phaser 3 使我們能夠使用 JavaScript 在瀏覽器中快速創建遊戲。我們最喜歡的一些 2D 遊戲是平台遊戲——想想馬里奧、索尼克、Su​​per Meat Boy 或 Cuphead 等遊戲。

Tiled 是一個 2D 地圖編輯器,用於創建遊戲世界。我們將探索如何使用 Tiled 創建平台遊戲關卡,將其與 Phaser 集成,並為 sprite 設置動畫以創建豐富的 2D 平台體驗。

在本文中,我們將創建一個基本的平台遊戲,我們的玩家可以在我們的世界中繼續跳躍。如果玩家擊中了一個尖峰,那麼我們會重置玩家的位置。可以在此處找到該遊戲的可玩演示。

本教程是為那些熟悉 Phaser 3 的人編寫的。如果您不熟悉,請閱讀我們之前關於 Phaser 的一篇文章來熟悉該框架。

開始使用

為了更好地學習本教程,請下載項目 stackabuse-platformer.zip 並將其解壓縮到您的工作區中。該文件夾應包含以下資產:

  • index.html :加載 Phaser 3.17 和我們的 game.js 文件
  • game.js :包含我們遊戲的邏輯
  • 資產/圖片 :
    • background.png
    • kenney_player.png
    • kenney_player_atlas.json
    • spike.png
  • 資產/平鋪地圖 :空文件夾,將用於保存 Tiled 文件
  • 資產/圖塊集 :
    • platformPack_tilesheet.png

注意 :如果您願意,也可以在我們的 GitHub 存儲庫上查看項目代碼來跟進。

不要忘記在項目文件夾中運行服務器,使用 IDE 甚至使用 Python:python3 -m http.server .這是 Phaser 能夠通過 HTTP 加載這些資產所必需的。同樣,有關更多信息,請參閱我們之前關於該主題的文章(上面鏈接)。

所有遊戲資產均由 Kenney 創建和共享。 atlas 文件是使用 Atlas Phaser Packer 創建的。

平鋪地圖編輯器

Tiled 是用於創建遊戲關卡的免費開源軟件。它適用於所有主要的桌面操作系統,因此請訪問該網站並下載它以繼續。

創建平鋪地圖

打開平鋪並單擊“新地圖”。在提示符下,將 Tile 圖層格式更改為“Base64(未壓縮)”,寬度為 14 個瓦片,高度為 7 個瓦片,每個瓦片大小為 64 像素。

將文件另存為“assets/tilemaps”中的“level1.tmx”。

創建一個瓦片集

在右窗格中,單擊“New Tileset...”。在彈出窗口中,將圖塊集命名為“kenny_simple_platformer”。確保選擇了“嵌入地圖”選項 .如果沒有該選項,Phaser 可能會遇到正確加載地圖的問題。在“Source”屬性中,從“assets/tilesets”目錄中選擇“platformPack_tilesheet.png”。

tilesheet 的圖像寬度為 896 像素,高度為 448 像素。它總共包含 98 張相同大小的圖像,它們都適合 7 行和 14 列。通過基本數學我們可以推斷出每個圖塊的寬度和高度均為 64 像素。確保tileset的寬高為64px:

設計我們的關卡

Tiled 中的地圖由層組成。每一層都存儲了遊戲世界的一些設計。頂部圖層的圖塊顯示在下方圖層之上。我們通過使用它們來獲得深度。這個基本遊戲只有兩層:

  • 平台:包含玩家與之交互的世界
  • 尖刺:包含可能傷害玩家的危險尖刺。

平台層

在我們將瓦片添加到地圖之前,讓我們首先重命名圖層。層的名稱將在我們的 Phaser 代碼中引用,所以讓我們將“Tiled Layer 1”更改為“Platforms”:

要創建關卡,只需從您的圖塊集中選擇一個圖塊,然後單擊您希望將其放置在地圖上的位置。讓我們創建/添加我們所有的平台:

對象層中的尖峰

在屏幕右側的“圖層”窗格中,單擊“新建圖層”按鈕並選擇“對像圖層”。將圖層命名為“Spikes”。

在頂部工具欄中,選擇“插入對象”選項:

現在我們可以從圖塊集中添加尖峰圖塊:

我們已經創建了我們的遊戲關卡!現在我們需要將其與 Phaser 集成。

加載平鋪地圖

Phaser 無法讀取 .tmx Tiled 創建的文件。首先,讓我們將地圖導出為 JSON。點擊“File -> Export As”,格式選擇JSON,在tilemaps中命名為“level1.json” 文件夾。與所有 Phaser 項目一樣,我們的資產需要加載到我們的 preload() 功能:

function preload() {
  this.load.image('background', 'assets/images/background.png');
  this.load.image('spike', 'assets/images/spike.png');
  // At last image must be loaded with its JSON
  this.load.atlas('player', 'assets/images/kenney_player.png','assets/images/kenney_player_atlas.json');
  this.load.image('tiles', 'assets/tilesets/platformPack_tilesheet.png');
  // Load the export Tiled JSON
  this.load.tilemapTiledJSON('map', 'assets/tilemaps/level1.json');
}

注意 :您可能想知道為什麼我們必須單獨加載尖峰圖像,如果它包含在 tilemap 中。不幸的是,要正確顯示對象,就需要這種重複。

在我們的 create() 函數,讓我們首先添加背景並根據我們的分辨率對其進行縮放:

const backgroundImage = this.add.image(0, 0,'background').setOrigin(0, 0);
backgroundImage.setScale(2, 0.8);

然後讓我們添加我們的地圖:

const map = this.make.tilemap({ key: 'map' });

該鍵與 preload() 中給出的名稱匹配 加載 Tiled JSON 時的函數。我們還必須將tileset圖像添加到我們的Phaser map 對象:

const tileset = map.addTilesetImage('kenney_simple_platformer', 'tiles');

addTilesetImage 的第一個參數 是我們在 Tiled 中使用的 tileset 的名稱。第二個參數是我們在preload()中加載的圖片的key 功能。

我們現在可以添加我們的平台層:

const platforms = map.createStaticLayer('Platforms', tileset, 0, 200);

並且應該看到這個:

默認情況下,Phaser 不管理我們平鋪層的碰撞。如果我們現在添加我們的播放器,它將完全穿過平台圖塊。讓我們告訴 Phaser 該層可以與其他對象發生碰撞:

platforms.setCollisionByExclusion(-1, true);

Tiled 為我們地圖中的每個圖塊提供了一個索引,以引用應該在那裡顯示的內容。我們平台的索引只能大於0。setCollisionByExclusion 告訴 Phaser 為索引不為 -1 的每個圖塊啟用碰撞,因此,所有圖塊。

紋理圖集

我們的播放器動畫存儲在紋理圖集中 - 包含較小圖像的圖像。與精靈表類似,它們通過加載一個文件來減少網絡活動。大多數紋理圖集包含的不僅僅是精靈信息。

我們來看看我們的圖片文件:“kenney_player.png”:

免費電子書:Git Essentials

查看我們的 Git 學習實踐指南,其中包含最佳實踐、行業認可的標準以及隨附的備忘單。停止谷歌搜索 Git 命令並真正學習 它!

我們的圖集包含 8 幀:第 0 到第 3 幀在上面,第 4 到第 7 幀在下面。就其本身而言,這對 Phaser 並沒有那麼有用,這就是為什麼它帶有一個 JSON 文件:“kenney_player_atlas.json”。

該文件有一個 frames 數組,其中包含有關構成圖集的每張圖片的信息。

要使用地圖集,您需要了解 filename 您正在使用的框架的屬性。

添加播放器

設置好我們的世界後,我們可以添加玩家並讓它與我們的平台交互。在我們的 create 函數讓我們添加以下內容:

this.player = this.physics.add.sprite(50, 300, 'player');
this.player.setBounce(0.1);
this.player.setCollideWorldBounds(true);
this.physics.add.collider(this.player, platforms);

默認情況下,Phaser 使用圖集的第一幀,如果我們想從不同的幀開始,我們可以添加一個 next sprite 的參數 filename 的方法 地圖集圖像的屬性,例如robo_player_3 .

當我們的玩家跳躍和著陸時,反彈屬性只是增加了一點活力。我們設置玩家與我們的遊戲世界和平台發生碰撞。我們現在應該看到我們的玩家站在我們的平台上:

紫色框存在於我們的播放器周圍,因為 debug 我們的物理引擎啟用了模式。調試模式顯示了決定我們的精靈如何碰撞的邊界。

添加動畫

回想一下,我們的紋理圖集有 8 幀用於玩家移動。 Phaser 允許我們根據圖集圖像的幀創建動畫。讓我們使用地圖集第一行的最後兩幀通過我們的 create() 創建一個行走動畫 功能:

this.anims.create({
  key: 'walk',
  frames: this.anims.generateFrameNames('player', {
    prefix: 'robo_player_',
    start: 2,
    end: 3,
  }),
  frameRate: 10,
  repeat: -1
});

key property 是我們稍後用來播放動畫的字符串。 frames 屬性是我們圖集的 JSON 文件中包含動畫的幀數組。動畫從數組的第一幀開始,到最後一幀結束。我們使用輔助函數 generateFrameNames() 為我們創建框架名稱列表,對於大型圖集文件非常有用。

frameRate 默認為每秒 24 幀,這對我們的播放器來說可能有點太快,所以我們將其設置為 10。當我們設置 repeat 到 -1 我們告訴 Phaser 無限運行這個動畫。

讓我們為空閒精靈添加動畫,即圖集的第一幀:

this.anims.create({
  key: 'idle',
  frames: [{ key: 'player', frame: 'robo_player_0' }],
  frameRate: 10,
});

我們的空閒動畫只是一幀。讓我們為我們的玩家跳躍時添加一個動畫,這也只是一幀:

this.anims.create({
  key: 'jump',
  frames: [{ key: 'player', frame: 'robo_player_1' }],
  frameRate: 10,
});

添加動畫後,我們需要啟用光標鍵以便移動播放器:

this.cursors = this.input.keyboard.createCursorKeys();

動畫播放器

如果我們的玩家向左或向右移動,那麼我們想要 .如果我們按空格鍵或向上鍵,我們想要跳轉 .否則,我們將留在我們的 idle 位置。讓我們在 update() 中實現它 功能:

// Control the player with left or right keys
if (this.cursors.left.isDown) {
  this.player.setVelocityX(-200);
  if (this.player.body.onFloor()) {
    this.player.play('walk', true);
  }
} else if (this.cursors.right.isDown) {
  this.player.setVelocityX(200);
  if (this.player.body.onFloor()) {
    this.player.play('walk', true);
  }
} else {
  // If no keys are pressed, the player keeps still
  this.player.setVelocityX(0);
  // Only show the idle animation if the player is footed
  // If this is not included, the player would look idle while jumping
  if (this.player.body.onFloor()) {
    this.player.play('idle', true);
  }
}

// Player can jump while walking any direction by pressing the space bar
// or the 'UP' arrow
if ((this.cursors.space.isDown || this.cursors.up.isDown) && this.player.body.onFloor()) {
  this.player.setVelocityY(-350);
  this.player.play('jump', true);
}

為精靈設置動畫就像將動畫設置為 true 一樣簡單 .如果您細心,您會注意到我們的地圖集只有向右運動。如果我們向左移動,無論是行走還是跳躍,我們都希望在 x 軸上翻轉精靈。如果我們向右移動,我們想把它翻轉回來。

我們可以通過以下代碼實現這個目標:

if (this.player.body.velocity.x > 0) {
  this.player.setFlipX(false);
} else if (this.player.body.velocity.x < 0) {
  // otherwise, make them face the other side
  this.player.setFlipX(true);
}

現在我們的玩家以動畫風格在遊戲中四處走動!

添加尖峰

Phaser 為我們提供了許多從對象層獲取精靈的方法。尖峰存儲在我們平鋪地圖對象的數組中。如果擊中他們,每個尖峰都會迫使我們的玩家重新開始。將所有尖峰放在一個精靈組中並在玩家和組之間設置碰撞對我們來說是有意義的。當與精靈組設置碰撞時,它會應用於所有精靈。

create() 函數添加如下:

// Create a sprite group for all spikes, set common properties to ensure that
// sprites in the group don't move via gravity or by player collisions
 this.spikes = this.physics.add.group({
    allowGravity: false,
    immovable: true
  });
  
// Let's get the spike objects, these are NOT sprites
// We'll create spikes in our sprite group for each object in our map
map.getObjectLayer('Spikes').objects.forEach((spike) => {
    // Add new spikes to our sprite group
    const spikeSprite = this.spikes.create(spike.x, spike.y + 200 - spike.height, 'spike').setOrigin(0);
});

我們應該得到這個:

尖峰精靈的碰撞邊界遠高於尖峰本身。如果保持不變,則會造成糟糕的遊戲體驗。玩家可以在不擊中精靈的情況下重置他們的位置!讓我們將尖刺的主體尺寸調整為更小,尤其是高度。替換 forEach 用這個:

map.getObjectLayer('Spikes').objects.forEach((spike) => {
    const spikeSprite = this.spikes.create(spike.x, spike.y + 200 - spike.height, 'spike').setOrigin(0);
    spikeSprite.body.setSize(spike.width, spike.height - 20).setOffset(0, 20);
});

為了使邊界框正確地包含尖峰,我們添加了一個與高度減少相匹配的偏移量。現在我們有了更合適的尖峰精靈:

與玩家衝突

如果我們的玩家與尖峰發生碰撞,他們的位置會被重置。在平台遊戲中,玩家有一個“失敗”的動畫是很常見的。當我們的播放器重置時,讓我們添加一個閃爍的動畫。一、在create() 讓我們添加碰撞:

this.physics.add.collider(this.player, this.spikes, playerHit, null, this);

播放器重置的邏輯將在 playerHit() 中 功能。每次玩家與尖峰精靈組中的精靈發生碰撞時,都會調用此函數。在文件末尾添加以下內容:

function playerHit(player, spike) {
  player.setVelocity(0, 0);
  player.setX(50);
  player.setY(300);
  player.play('idle', true);
  player.setAlpha(0);
  let tw = this.tweens.add({
    targets: player,
    alpha: 1,
    duration: 100,
    ease: 'Linear',
    repeat: 5,
  });
}

這裡發生了很多事情。讓我們逐行查看每條指令:

  • 將玩家的速度設置為 0。在重新啟動時停止玩家的移動更加可預測(也更安全)
  • 將 X 和 Y 坐標設置為玩家的第一個位置
  • 使用空閒動畫,就像播放器開始時一樣
  • alpha 屬性控制精靈的不透明度。它是一個介於 0 和 1 之間的值,其中 0 表示完全透明,1 表示完全不透明
  • 創建補間 - 遊戲對象屬性的“動畫”。補間應用於與尖峰碰撞的玩家對象。它將 alpha 屬性設置為 1(即讓我們的播放器完全可見)。此補間持續 100 毫秒,並且不透明度線性增加,如 ease 所示 財產。它還會重複 5 次,因此它看起來像是在閃爍。

現在我們的遊戲是這樣的:

注意 :一定要去掉 debug: true 在與朋友分享之前從遊戲配置中獲取屬性,千萬不要在生產中離開調試模式!

結論

使用 Tiled,我們可以設計小型和廣闊的 2D 遊戲世界。在我們的遊戲世界中創建深度層是最佳實踐。然後,我們將我們在 Tiled 中構建的世界添加到我們的 Phaser 遊戲中。

我們將平台層添加為靜態層,使其在玩家碰撞時無法移動。然後,我們為尖峰創建了一個精靈組,並創建了一個函數來處理每個尖峰與玩家之間的碰撞。

除了創建一個充滿活力的遊戲世界之外,我們還學習瞭如何使用圖集為我們的角色設置動畫——一個包含多個較小圖像的大圖像,並附有一個 JSON 文件,詳細說明每一幀中的圖像。我們還使用補間來改變精靈的屬性一段時間。

借助這些技術,您可以使用 Phaser 製作下一個最好的平台遊戲!

您可以在此處查看遊戲的註釋源代碼。


Tutorial JavaScript 教程
  1. 構建 Javascript 異步函數錯誤處理程序

  2. JavaScript 和 DOM 兼容性表的資源

  3. 無法運行節點 app.js 文件

  4. JavaScript 動態數組 |創建簡單動態數組示例

  5. 如何構建一個自動擴展的 Textarea jQuery 插件,第 3 部分

  6. Angular 表單控件驗證模式

  7. get、find、query(React 測試庫)的區別。

  1. 帶有長時間戳的MYSQL查詢

  2. JSON 方法,toJSON

  3. 如何禁用輸入類型=文本?

  4. 如何檢測Harshen的jQuery-countdownTimer何時到達00:00

  5. 很棒的人的很棒的事情-2020 年 6 月

  6. 在 Heroku 上設置 umami

  7. JavaScript 中的解構

  1. 在 NextJS 中編寫 Markdown 的最簡單方法!

  2. 為什麼我們在 React 中綁定事件處理程序???

  3. 使用 JavaScript 構建一個簡單的時鐘

  4. 用於匹配/替換 JavaScript 註釋的正則表達式(多行和內聯)