2016 年 1 月

第 31 卷,第 1 期

本文章是由機器翻譯。

遊戲開發-Babylon.js: 此類進階功能可加強您的第一個 Web 遊戲

Raanan Weber

在 12 月期中,我一開始本教學課程先檢閱 Babylon.js 的 WebGL 型 3D 遊戲引擎的基本建置組塊 (msdn.com/magazine/mt595753)。我開始設計非常簡單的保齡球遊戲使用 Babylon.js 提供的工具。到目前為止,遊戲都包含所需的物件 — 保齡球球、 lane、 裝訂及 10 個球瓶。

現在我將告訴您如何讓遊戲更生動 — 如何擲回球、 叫用的 pin,加上一些音訊效果、 提供不同的數位相機檢視與多個。

此部分的教學課程自然仰賴,並且從第一個部分會擴充此程式碼。若要區分兩個部分,我會建立新的 JavaScript 檔案包含將用於此組件中的程式碼。即使在擴充特定物件 (如場景或主攝影機) 的功能,我不會實作它的第一個部分中建立物件的函式中。我必須延伸唯一函式是初始化,其中包含的所有變數所需的兩個部分的教學課程。

這麼做是為了方便您使用。當實作您的遊戲,您應該不用多說,使用程式設計的開發架構和您想要的語法。建議您讓 TypeScript 和其物件導向的程式設計風格,試試。這是非常適合組織的專案。

在我撰寫這篇文章花的時間,已發行新版的 Babylon.js。我將會繼續使用 2.1 版。若要檢查的 Babylon.js 2.2 的新功能,請移至 bit.ly/1RC9k6e

原生的衝突偵測

用於遊戲中的主攝影機是免費的相機可讓播放程式到處移動整個的 3D 場景,使用滑鼠和鍵盤。如果沒有特定的修改,不過,相機是能夠透過場景浮點數、 走壁,甚至透過最低限度值和路段。若要建立實際的遊戲,玩家應該能夠只在地面和路段移。

若要啟用此功能,我將使用巴比倫的內部碰撞偵測系統。衝突系統可防止將合併到其他兩個物件。它也具有防止播放程式在空中浮點數,如果向前走向上尋找時重力功能。

首先,讓我們啟用衝突偵測和重力:

function enableCameraCollision(camera, scene) {
  // Enable gravity on the scene. Should be similar to earth's gravity. 
  scene.gravity = new BABYLON.Vector3(0, -0.98, 0);
  // Enable collisions globally. 
  scene.collisionsEnabled = true;
  // Enable collision detection and gravity on the free camera. 
  camera.checkCollisions = true;
  camera.applyGravity = true;
  // Set the player size, the camera's ellipsoid. 
  camera.ellipsoid = new BABYLON.Vector3(0.4, 0.8, 0.4);
}

此函式可讓遊戲的場景和相機中的衝突系統,並設定相機的橢圓體,可以視為玩家的大小。這是 (在此情況下) 調整 0.8x1.6x0.8 單位,大致平均人力大小的方塊。相機需要此方塊,因為它不是網狀結構。Babylon.js 衝突系統會檢查網狀結構,這就是為什麼應該模擬相機的網狀結構之間發生衝突。因此 0.8 大小就會轉譯為 0.4,橢圓體會定義物件的中心。我也會啟用在場景重力,將會套用至攝影機的移動。

一旦相機具有已啟用的衝突,我需要啟用地面和路段衝突檢查。這是我想要針對衝突的每一個網狀結構上設定的簡單布林旗標:

function enableMeshesCollision(meshes) {
  meshes.forEach(function(mesh) {
    mesh.checkCollisions = true;
  });
}

加入兩個函式呼叫初始化函式是最後一個步驟:

// init function from the first part of the tutorial.
  ...
  enableCameraCollision(camera, scene);
  enableMeshesCollision[[floor, lane, gutters[0], gutters[1]]);
}

衝突偵測的唯一工作是為了防止網狀結構合併到另一個。其重力功能的實作將全新的相機。若要建立實際的實體特定網狀結構互動 — 在此案例中球和 pin 碼 — 更複雜的系統是必要: 物理引擎。

擲回球 — 物理 Babylon.js 中的整合

擲回球瓶朝為遊戲的主要動作。需求是相當簡單: 播放程式應該能夠設定方向和擲回的強度。如果特定 pin 會叫用,它們應該落。如果 pin 叫用其他的 pin,那些應該落,以及。如果播放程式中,會擲回球的側邊,它應該屬於裝訂邊。Pin 應該可以歸類為根據的球衝著他們的速度。

這是完全物理引擎的網域。物理引擎計算即時網狀結構的內文動態,並計算下一步根據套用強制移動。簡單來說,是決定哪些人或由使用者移動網狀結構時另一個網狀結構與網狀結構剛好物理引擎。它會考慮網狀結構的目前速度、 重量、 圖形等等。

若要計算即時的固定主體變動 (網狀結構的下一步移動空間),連繫物理引擎必須簡化網狀結構。若要這麼做,每一個網狀結構具有冒名頂替者 — 它 (通常球體或方塊) 繫結的簡單網狀結構。這可減少計算的精確度,但仍可將物件移動到實體驅使的快速計算。若要深入了解如何物理引擎運作,請造訪 bit.ly/1S9AIsU

物理引擎不 Babylon.js 的一部分。認可為單一引擎,架構的開發人員決定實作介面,以不同的物理條件引擎可讓開發人員決定哪一個他們想要使用的一個。Babylon.js 目前有兩個物理引擎的介面: Cannon.js (cannonjs.org) 和 Oimo.js (github.com/lo-th/Oimo.js)。兩者都很棒! 我個人覺得 Oimo 整合有點更好,因此,在會用到我發給的保齡球比賽。Cannon.js 的介面已經過徹底改寫的 Babylon.js 2.3 現在是 alpha,而現在支援最新的 Cannon.js 版本,並附上豐富的 bug 修正和新 impostors,包括複雜的高度地圖。我建議您不妨試試如果您使用 Babylon.js 2.3 或更新版本。

啟用物理引擎是使用簡單的一行程式碼:

scene.enablePhysics(new BABYLON.Vector3(0, -9.8, 0), new BABYLON.OimoJSPlugin());

這樣會設定在場景重力並定義要使用的物理條件引擎。取代 Cannon.js Oimo.js,只需要變更的第二個變數:

new BABYLON.CannonJSPlugin()

接下來,我需要的所有物件上定義 impostors。這是使用網格 setPhysicsState 函式。例如,以下是路段定義:

lane.setPhysicsState(BABYLON.PhysicsEngine.BoxImpostor, {
  mass: 0,
  friction: 0.5,
  restitution: 0
});

第一個變數是冒名頂替者的類型。路段甚至是完美的方塊,因為我使用方塊冒名頂替者。第二個變數是主體的物理條件定義,其重量 (以公斤)、 其所面臨的 restitution 因素。路段大量為 0,我想要保留在其位置。設定大量設為 0 冒名頂替者上的會保留在其目前位置中鎖定的物件。

我將使用球體冒名頂替者球體。保齡球球重量大約 6.8 公斤,而且通常非常順利,因此需要無摩擦:

ball.setPhysicsState(BABYLON.PhysicsEngine.SphereImpostor, {
  mass: 6.8,
  friction: 0,
  restitution: 0
});

如果您想知道為什麼我使用公斤和不磅,它是相同的原因,我使用公尺和不英呎整個專案: 物理引擎會使用公制系統。例如,預設重力定義是 (0、-9.8,0),就是大約地球重力。所用的單位是公尺為單位,每第二個平方 (m/s2)。

現在我必須能夠擲回球。我將使用的物理條件引擎的另一項功能 — 將脈衝套用至某一特定物件。在這裡,直接的想法是特定的方向與啟用的物理條件套用至網狀結構中的強制。比方說,擲回球,我將使用下列:

ball.applyImpulse(new BABYLON.Vector3(0, 0, 20), ball.getAbsolutePosition());

第一個變數是向量的脈衝,這裡 20 單位時,向前場景會重設 Z 軸上。第二個變數會指定在物件上應該套用強制的位置。在此情況下,它是球的中心。想想射擊遊戲集區中的一個技巧,佇列可以叫用在許多不同的點,不僅中央球。這是您可以模擬這類行為的方式。

現在我可以擲回球向前。[圖 1 顯示看起來像是當球達到 pin。

保齡球球達到 Pin
圖 1] 保齡球球達到 Pin

不過,我仍缺乏方向和強度。

有許多方法可設定的方向。若要使用目前的鏡頭方向或指標點選位置,有兩種的最佳選項。我的第二個選項。

尋找空間接觸在使用者完成點使用 PickingInfo 物件、 傳送每個指標清單和指標向上事件。PickingInfo 物件包含事件所觸發,包括已觸及網狀結構的點、 被接觸到 mesh 上的點、 到這個點的距離資訊。如果已觸及介面沒有網狀結構,PickingInfo 叫用的變數會是 false。Babylon.js 場景有幫助我挑選資訊的兩個有用的回呼函式: onPointerUp 和 onPointerDown。這些兩個回呼觸發時都會觸發指標事件,而且其簽章,如下所示:

function(evt: PointerEvent, pickInfo: PickingInfo) => void

Evt 變數是原始 JavaScript 事件所觸發。第二個變數是由架構上觸發的每個事件產生挑選資訊。

我可以使用這些回呼擲回該方向的球:

scene.onPointerUp = function(evt, pickInfo) {
  if (pickInfo.hit) {
    // Calculate the direction using the picked point and the ball's position. 
    var direction = pickInfo.pickedPoint.subtract(ball.position);
    // To be able to apply scaling correctly, normalization is required.
    direction = direction.normalize();
    // Give it a bit more power (scale the normalized direction).
    var impulse = direction.scale(20);
    // Apply the impulse (and throw the ball). 
    ball.applyImpulse(impulse, new BABYLON.Vector3(0, 0, 0));
  }
}

現在我可以把球特定方向。俱備,只欠就是擲回的能力。若要新增,我將計算的框架指標清單和指標向上事件之間的差異。[圖 2 顯示用來擲回的特定強度球的函數。

[圖 2 擲回球的強度與方向

var strengthCounter = 5;
var counterUp = function() {
  strengthCounter += 0.5;
}
// This function will be called on pointer-down events.
scene.onPointerDown = function(evt, pickInfo) {
  // Start increasing the strength counter. 
  scene.registerBeforeRender(counterUp);
}
// This function will be called on pointer-up events.
scene.onPointerUp = function(evt, pickInfo) {
  // Stop increasing the strength counter. 
  scene.unregisterBeforeRender(counterUp);
  // Calculate throw direction. 
  var direction = pickInfo.pickedPoint.subtract(ball.position).normalize();
  // Impulse is multiplied with the strength counter with max value of 25.
  var impulse = direction.scale(Math.min(strengthCounter, 25));
  // Apply the impulse.
  ball.applyImpulse(impulse, ball.getAbsolutePosition());
  // Register a function that will run before each render call 
  scene.registerBeforeRender(function ballCheck() {
    if (ball.intersectsMesh(floor, false)) {
      // The ball intersects with the floor, stop checking its position.  
      scene.unregisterBeforeRender(ballCheck);
      // Let the ball roll around for 1.5 seconds before resetting it. 
      setTimeout(function() {
        var newPosition = scene.activeCameras[0].position.clone();
        newPosition.y /= 2;
        resetBall(ball, newPosition);
      }, 1500);
    }
  });
  strengthCounter = 5;
}

指標事件有關的注意事項,刻意未曾使用詞彙 「 按一下 」。 Babylon.js 使用指標事件系統,其延伸了滑鼠點選觸控的瀏覽器能夠按一下、 更動,並指向其他輸入的裝置的各項功能。如此一來,smartphone 上的觸控或滑鼠點選或桌面上同時觸發程序相同的事件。若要模擬此瀏覽器不支援此功能,Babylon.js 使用 hand.js,polyfill 指標事件也包含遊戲的 index.html 中。閱讀更多有關在 GitHub 頁面 hand.js bit.ly/1S4taHF。指標事件草稿,請參閱 bit.ly/1PAdo9J。請注意該 hand.js 將在未來的版本取代 jQuery PEP (bit.ly/1NDMyYa)。

這就是物理! 發給的保齡球比賽一大堆更勝。

新增音訊效果

新增音訊效果與遊戲提供龐大的提升 UX音訊可以建立右大氣及發給的保齡球比賽中加入更多的真實性。幸運的是,Babylon.js 包含音訊引擎,2.0 版中導入。音訊引擎以 Web 音訊 API 為基礎 (bit.ly/1YgBWWQ),支援這種 Internet Explorer 以外的所有主要瀏覽器。可用的檔案格式取決於瀏覽器本身。

我加入三個不同的音訊效果。第一個是環境的聲音 — 模擬周圍環境的聲音。發給的保齡球比賽,在保齡球大廳的聲音會正常運作良好,但是因為我建立草保齡球道之外,某些本質音效將會更好。

若要加入環境的聲音,我會載入音效、 自動播放它,並不斷循環播放:

var atmosphere = new BABYLON.Sound("Ambient", "ambient.mp3", scene, null, {
  loop: true,
  autoplay: true
});

此聲音持續播放的那一刻起,就會載入。

我要加入第二個音訊效果是保齡球道上滾動球的聲音。此聲音播放只要球道,但的分鐘球離開路段,它會停止。

首先,我要建立的滾動聲音:

var rollingSound = new BABYLON.Sound("rolling", "rolling.mp3", scene, null, {
  loop: true,
  autoplay: false
});

聲音就會載入,但不會播放我執行球就會擲回時,即會其播放函式之前。我擴充函式的 [圖 2 並加入下列:

...ball.applyImpulse(impulse, new BABYLON.Vector3(0, 0, 0));
// Start the sound.
rollingSound.play();
...

當球離開路段停止聲音:

...
If(ball.intersectsMesh(floor, false)) {
  // Stop the sound.
  rollingSound.stop();
  ...
}
...

Babylon.js 可以讓我附加至特定網狀結構的音效。如此一來會自動計算聲音的磁碟區和移動瀏覽使用網狀結構的位置,並建立更實際的經驗。若要這樣做只是新增下列這一行我建立聲音之後:

rollingSound.attachToMesh(ball);

現在將一律播放音樂,從球的位置。

我想要加入的最後一個音效是球達到 pin。若要這樣做,我建立的音效,然後將它附加到第一個 pin:

var hitSound = new BABYLON.Sound("hit", "hit.mp3", scene);
hitSound.attachToMesh(pins[0]);

聽不會執行迴圈,它將不會自動播放。

我會在每次球撞上的 pin 一個播放音效。若要完成這項工作加上的函式,球便會擲回之後,將會不斷檢查是否與任何 pin 的球的交集。如果它沒有交集,我會取消登錄函式,並播放音效。我這樣的 scene.onPointerUp 函式中加入下列幾行 [圖 2:

scene.registerBeforeRender(function ballIntersectsPins() {
  // Some will return true if the ball hit any of the pins.
  var intersects = pins.some(function (pin) {
    return ball.intersectsMesh(pin, false);
  });
  if (intersects) {
    // Unregister this function – stop inspecting the intersections.
    scene.unregisterBeforeRender(ballIntersectsPins);
    // Play the hit sound.
    hit.play();
  }
});

遊戲現在的我想要新增的所有音訊效果。接下來,我將繼續遊戲新增改善的面貌。

請注意,我不能包含我使用隨附的專案,因為材料的著作權法與音訊效果。我找不到我也可以發行任何免費音訊範例。因此,程式碼會取消註解。如果您加入我所使用的三個音訊範例,它將會運作。

加入分數顯示

玩家達到 pin 之後,它是最適合用來實際看到它們多少還是待命和多少已被卸除。我要加入的面貌。

計分板本身將會是簡單的黑色平面上的白色文字。若要建立它,我將使用動態紋理功能,這基本上是可用來當做紋理遊戲的 3D 物件的 2D 畫布。

建立平面和動態紋理很簡單:

var scoreTexture = new BABYLON.DynamicTexture("scoreTexture", 512, scene, true);
var scoreboard = BABYLON.Mesh.CreatePlane("scoreboard", 5, scene);
// Position the scoreboard after the lane.
scoreboard.position.z = 40;
// Create a material for the scoreboard.
scoreboard.material = new BABYLON.StandardMaterial("scoradboardMat", scene);
// Set the diffuse texture to be the dynamic texture.
scoreboard.material.diffuseTexture = scoreTexture;

動態紋理,可讓我直接繪製到基礎的畫布上,使用其 getContext 函式會傳回 CanvasRenderingContext2D (mzl.la/1M2mz01)。動態紋理物件也會提供幾個說明函式,如果我不想直接處理畫布內容可能很有用。一個這類函式是 drawText,這讓我繪製使用此畫布上的特定字型的字串。已卸除的 pin 數目變更時,我將會更新畫布:

var score = 0;
scene.registerBeforeRender(function() {
  var newScore = 10 - checkPins(pins, lane);
  if (newScore != score) {
    score = newScore;
    // Clear the canvas. 
    scoreTexture.clear();
    // Draw the text using a white font on black background.
    scoreTexture.drawText(score + " pins down", 40, 100,
      "bold 72px Arial", "white", "black");
  }
});

檢查連接關閉時是簡單 — 我只檢查它們在 y 軸上的位置是否等於 (預先定義的變數 'pinYPosition') 的所有連接到原始 Y 位置:

function checkPins(pins) {
  var pinsStanding = 0;
  pins.forEach(function(pin, idx) {
    // Is the pin still standing on top of the lane?
    if (BABYLON.Tools.WithinEpsilon(pinYPosition, pin.position.y, 0.01)) {
      pinsStanding++;
    }
  });
  return pinsStanding;
}

中可以看到動態紋理 [圖 3

遊戲的排行榜
圖 3 遊戲排行榜

我漏現在是函式來重設路段和面板。將鍵盤上按下 R 鍵時,都是動作觸發程序 (請參閱 [圖 4)。

[圖 4 路段和面板重設

function clear() {
  // Reset the score.
  score = 0;
  // Initialize the pins.
  initPins(scene, pins);
  // Clear the dynamic texture and draw a welcome string.
  scoreTexture.clear();
  scoreTexture.drawText("welcome!", 120, 100, "bold 72px Arial", "white", "black");
}
scene.actionManager.registerAction(new BABYLON.ExecuteCodeAction({
  trigger: BABYLON.ActionManager.OnKeyUpTrigger,
  parameter: "r"
}, clear));

按下 R 鍵將會重設/初始化 [場景。

加入下列相機

我想要加入至遊戲好結果是會遵循保齡球球時就會擲回的相機。我想要將 「 換用 」 以及球瓶朝和停止球時回到其原始位置中的相機。我可以完成這項作業使用 Babylon.js 的多重檢視功能。

在本教學課程的第一個部分中設定可用的攝影機物件如場景的作用中使用相機:

scene.activeCamera = camera

使用中的數位相機變數會告知的情況下定義一個以上的相機是要呈現的場景。當我想要使用的單一相機整個遊戲,這是沒問題。但是,如果我想要有 「 圖片在圖片 」 效果,一個使用中的數位相機仍嫌不足。相反地,我需要使用儲存在變數名稱 scene.activeCameras 場景中的作用中的數位相機陣列。這個陣列中的數位相機會呈現一個接著一個。如果 scene.activeCameras 不是空白,將會忽略 scene.activeCamera。

第一個步驟是新增此陣列的原始的免費相機。Init 函式中輕易完成這。取代 scene.activeCamera = 與數位相機:

scene.activeCameras.push(camera);

在第二個步驟中,我將建立下列相機球擲回時:

var followCamera = new BABYLON.FollowCamera("followCamera", ball.position, scene);
followCamera.radius = 1.5; // How far from the object should the camera be.
followCamera.heightOffset = 0.8; // How high above the object should it be.
followCamera.rotationOffset = 180; // The camera's angle. here - from behind.
followCamera.cameraAcceleration = 0.5 // Acceleration of the camera.
followCamera.maxCameraSpeed = 20; // The camera's max speed.

這會建立數位相機,並將它設定為獨立 1.5 部隊,並且 0.8 單位是關於物件上的進行。此瀏覽過的物件應該是球,但還有一個問題 — 球可能微調和相機會與其旋轉。我想要達到是在物件後面的 「 飛行路徑 」。若要這麼做,我會建立可以得到球的位置,但不是將它旋轉的下列物件:

// Create a very small simple mesh.
var followObject = BABYLON.Mesh.CreateBox("followObject", 0.001, scene);
// Set its position to be the same as the ball's position.
followObject.position = ball.position;

然後,我將相機的目標是 followObject:

followCamera.target = followObject;

現在相機會遵循移動以及球的待處理物件。

上次設定相機需要為其檢視區。每個相機可以定義將使用的實際螢幕面積。這是與檢視區變數,使用下列變數定義:

var viewport = new BABYLON.Viewport(xPosition, yPosition, width, height);

所有的值是介於 0 和 1,就像百分比 (含 1 是 100%) 相對於螢幕的高度和寬度。前兩個值定義起始點,在畫面上,相機的矩形的寬度和高度定義矩形的寬度和高度,相較於螢幕的實際大小,檢視區的預設值為 (0.0,0.0,1.0,1.0),其中涵蓋了整個螢幕。我要自己遵循相機設定的高度和寬度設為 30%的畫面:

followCamera.viewport = 新巴比倫。檢視區 (0.0、 0.0、 0.3,0.3);

[圖 5 顯示遊戲的檢視外觀之後球就會擲回。請注意在左下角的檢視。

遵循相機的圖片中圖片效果
[圖 5 遵循相機的圖片中圖片效果

輸入的控制項 — 的螢幕反應輸入的事件,例如指標或鍵盤事件,會保存在本教學課程的第一個部分中定義的免費相機。這已設定在 init 函式。

如何開發遊戲進一步

在開始時,我說過這個遊戲會成為原型。有幾個要把它變成真正的遊戲實作。

第一個就是加入 GUI,會要求輸入使用者的名稱,例如,顯示 [設定選項 (或許是我們可以播放 Mars 發給的保齡球比賽! 只設定重力不同)。而且可能需要其他任何使用者。

Babylon.js 並未提供原生的方式來建立 GUI,但社群成員的已建立一些擴充功能可用來建立很棒的 Gui,包括 CastorGUI (bit.ly/1M2xEhD),bGUi (bit.ly/1LCR6jk) 和對話方塊延伸模組在 bit.ly/1MUKpXH。第一個来加入 3D 畫布頂端的圖層是使用 HTML 和 CSS。其他加入對話方塊 3D 場景本身使用一般的網狀結構和紋理動態。我建議您試著閱讀這些之前撰寫您自己的解決方案。所有您輕鬆使用它們會更簡化的 GUI 建立程序。

另一項改進是使用更好的網狀結構。在遊戲中的網狀結構已建立所有使用 Babylon.js 內部函式和顯然與僅限程式碼所能達到的限制。有許多網站提供免費和付費的 3D 物件。最佳,在我看來,是 TurboSquid (bit.ly/1jSrTZy)。尋找更佳的效能的低階多種網狀結構。

之後新增更好的網狀結構,何不加入人力物件實際上會擲出球? 若要執行需要極簡動畫的功能整合 Babylon.js 和支援功能的網狀結構中。您會發現示範了看起來像是在 babylonjs.com/BONES

在最後一個步驟,請嘗試建立遊戲虛擬實際用途。唯一的變更需要在此情況下是所使用的相機。取代 WebVRFreeCamera FreeCamera,請參閱目標 Google 黏貼是多麼的容易。

有許多您可以建立其他改進功能 — 新增 pin 碼,加入更多進行多人的功能,請限制相機的移動和位置,從一端上的相機可能擲球等等。我會讓您了解的一些其他您自己的功能。

總結

我希望您已經閱讀本教學課程,因為我撰寫多樂趣,以及它提供您推入正確的方向,並取得您嘗試 Babylon.js。這真的是很棒的架構,並使用很棒的愛的開發人員建置適用於開發人員。請瀏覽 babylonjs.com 為架構的更多示範。與同樣地,寄信加入支援論壇,並詢問您擁有的任何問題。有沒有無聊的問題,荒謬解答!


Raanan Weber是 IT 顧問、 完整堆疊開發人員、 丈夫和父親。在閒暇他會加到 Babylon.js 及其他開放原始碼專案。您可以閱讀他的部落格 blog.raananweber.com

感謝以下技術專家對本文的審閱: David Catuhe