使用 BabylonJS 和 JavaScript 构建 Web VR 游戏
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
在本教程中,我将一步一步地向您展示如何使用 BabylonJS 构建 Web VR 游戏。
先决条件
BabylonJS 和 CannonJS 是什么?
BabylonJS是一个完整的 JavaScript 框架,用于使用 HTML5、WebGL、WebVR 和 Web Audio 构建 3D 游戏和体验。
CannonJS是一个用 JavaScript 编写的物理引擎。你可能会问,什么是物理引擎?简单来说,它是“一种软件,用于对某些物理系统进行近似模拟,例如刚体动力学(包括碰撞检测)、软体动力学和流体动力学,可应用于计算机图形学、视频游戏和电影等领域。”
首先,我们需要使用 Babylon.js、Webpack 和 TypeScript 获取一个基础的启动项目。
运行入门项目的步骤和Git 仓库链接
- 克隆仓库
git clone https://github.com/cassieview/babylonjs-webpack-typescript-starter-project.gitcd babylonjs-webpack-typescript-starter-project - 安装包
npm install - 建设项目
npm run build - 运行脚本测试项目
npm start - 在 VS Code 中打开
code .
我们来谈谈启动项目。
简单的 index.html 模板。
<!DOCTYPE html>
<html>
<head>
<style>
html,
body {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
text-align: center;
}
#renderCanvas {
width: 100%;
height: 100%;
touch-action: none;
}
</style>
</head>
<body>
<canvas id="renderCanvas"></canvas>
<script src="dist/index.js"></script>
</body>
</html>
index.ts TypeScript 文件
index.ts 文件是创建主场景的 TypeScript 文件。它是在 dist 文件夹中被转译成 JavaScript 的 TypeScript 文件。
游戏脚本源文件位于 dist 文件夹中。Webpack 是一个开源的 JavaScript 模块打包工具,它会生成代表这些模块的静态资源。这些静态资源就是从 dist 文件夹中加载的。Webpack 会将脚本编译成一个单独的源文件,并用它来运行游戏脚本。
首先,我们从 BabylonJS 导入创建游戏场景所需的包。创建 canvas 变量,并使用原生 JavaScript 从 HTML 的 body 部分获取 renderCanvas 画布标签。然后,我们创建引擎并传入 BabylonJS 引擎。
import { Engine, Scene, HemisphericLight, Vector3, MeshBuilder, Mesh } from "babylonjs";
var canvas: any = document.getElementById("renderCanvas");
var engine: Engine = new Engine(canvas, true);
接下来是创建场景函数。在这里,我们定义场景,并传入引擎。然后我们创建一个摄像机。摄像机代表游戏玩家的视角。我们使用的是通用摄像机。
接下来,我们在场景中添加一个简单的球体网格并设置其基本属性。VR 辅助程序会在屏幕右下角添加一个 VR 按钮,以便用户进入 VR 游戏。但这在浏览器中查看游戏和进行测试时会造成一些问题。为了方便测试,我建议注释掉这行代码。然后,当您想使用 VR 头显进行测试时,再取消注释即可进入 VR 游戏。
提示:您可以轻松测试所做的更改,只需运行命令,npm run build然后在浏览器中打开 index.html 文件即可C:/Code/babylonjs-webpack-typescript-starter-project/index.html。这是一个静态网站,因此您实际上无需运行命令npm start。只需运行构建并刷新浏览器,即可打开 index.html 文件。
function createScene(): Scene {
// Create scene
var scene: Scene = new Scene(engine);
// Create camera
var camera = new BABYLON.UniversalCamera("UniversalCamera", new BABYLON.Vector3(0, 0, -10), scene);
// Create sphere
var sphere1: Mesh = MeshBuilder.CreateSphere("sphere", { diameter: 1 }, scene);
sphere1.position.y = 5;
sphere1.material = new BABYLON.StandardMaterial("sphere material", scene)
// Enable VR
var vrHelper = scene.createDefaultVRExperience();
vrHelper.enableInteractions();
return scene;
}
var scene: Scene = createScene();
engine.runRenderLoop(() => {
scene.render();
});
开始构建游戏
现在你应该对入门项目的内容以及 Babylon.js 的功能有了基本的了解。接下来,我们要添加重力效果,所以需要用到上面提到的 Cannon.js 库。
import { Engine, Scene, ArcRotateCamera, HemisphericLight, Vector3, MeshBuilder, Mesh, CannonJSPlugin } from "babylonjs";
复制这段代码块并粘贴到场景变量下方。这里我们添加了地面网格,并赋予它一个属性physicsImpostor,使球体能够落下并落在地面上。
var gravityVector = new BABYLON.Vector3(0, -1, 0);
scene.enablePhysics(gravityVector, new CannonJSPlugin);
var light = new HemisphericLight("light",Vector3.Zero(),scene);
// Parameters : name, position, scene
var camera = new BABYLON.UniversalCamera("UniversalCamera", new BABYLON.Vector3(0, 0, -10), scene);
camera.checkCollisions = true;
camera.applyGravity = true;
// Targets the camera to a particular position. In this case the scene origin
camera.setTarget(BABYLON.Vector3.Zero());
// Attach the camera to the canvas
camera.attachControl(canvas, true);
// Create Ground
var ground = BABYLON.Mesh.CreatePlane("ground", 25.0, scene);
ground.position = new BABYLON.Vector3(0, -10, 0);
ground.rotation = new BABYLON.Vector3(Math.PI / 2, 0, 0);
ground.material = new BABYLON.StandardMaterial("groundMat", scene);
ground.material.backFaceCulling = false;
ground.receiveShadows = true;
ground.physicsImpostor = new BABYLON.PhysicsImpostor(ground, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 0, friction: 1, restitution: 0 }, scene);
为球体添加物理效果、阴影和光照:
import { Engine, Scene, ArcRotateCamera, HemisphericLight, Vector3, MeshBuilder, Mesh, CannonJSPlugin, ShadowGenerator, DirectionalLight } from "babylonjs";
// Create sphere
var sphereLight = new DirectionalLight("dir02", new Vector3(0.2, -1, 0), scene);
sphereLight.position = new Vector3(0, 80, 0);
var sphere1: Mesh = MeshBuilder.CreateSphere("sphere", { diameter: 1 }, scene);
sphere1.position.y = 5;
sphere1.material = new BABYLON.StandardMaterial("sphere material", scene)
sphere1.physicsImpostor = new BABYLON.PhysicsImpostor(sphere1, BABYLON.PhysicsImpostor.SphereImpostor, { mass: 1 }, scene);
var shadowGenerator = new ShadowGenerator(2048, sphereLight);
shadowGenerator.addShadowCaster(sphere1);
现在我们看到一个球体落到我们制作的地面面板上。真令人兴奋。
项目架构
我们还有很多内容要添加到这个游戏中,虽然我们可以把所有内容都放在一个巨大的函数里,但出于各种原因,这不是最佳实践。让我们新建一个sphere.ts文件,把球体逻辑移到里面。
import { Scene, Vector3, MeshBuilder, Mesh, ShadowGenerator, DirectionalLight } from "babylonjs";
export function addSphere(scene: Scene) {
// Create sphere
var sphereLight = new DirectionalLight("dir02", new Vector3(0.2, -1, 0), scene);
sphereLight.position = new Vector3(0, 80, 0);
var sphere: Mesh = MeshBuilder.CreateSphere("sphere", { diameter: 1 }, scene);
sphere.position.y = 5;
sphere.material = new BABYLON.StandardMaterial("sphere material", scene)
sphere.physicsImpostor = new BABYLON.PhysicsImpostor(sphere, BABYLON.PhysicsImpostor.SphereImpostor, { mass: 1 }, scene);
var shadowGenerator = new ShadowGenerator(2048, sphereLight);
shadowGenerator.addShadowCaster(sphere);
}
然后返回index.ts并导入我们创建的文件,并调用之前逻辑所在的addSphere函数。addSphere
line 2: import { addSphere } from "./sphere";
line 35: addSphere(scene);
现在最好npm run build刷新一下浏览器,看看逻辑操作是否成功完成。
添加开始按钮
好吧,就像任何一款好游戏一样,你需要一个开始按钮,你知道,用来开始游戏。
导入 gui 库,以便我们可以使用 3D 按钮和面板。
import * as GUI from "babylonjs-gui";
startGameButton在现有函数下方添加该函数createScene。将addSphere函数调用移至button.onPointerUpObservable事件处理程序中。此事件用于在点击时触发其他事件。
var startGameButton = function (panel) {
var button = new GUI.Button3D();
panel.addControl(button);
button.onPointerUpObservable.add(function () {
addSphere(scene);
});
var text1 = new GUI.TextBlock();
text1.text = "Start Game";
text1.color = "white";
text1.fontSize = 24;
button.content = text1;
}
更新createScene函数,将按钮添加到场景中。这将添加到addSphere之前所在的第 35 行。
// Create the 3D UI manager
var manager = new GUI.GUI3DManager(scene);
// Create a horizontal stack panel
var panel = new GUI.StackPanel3D();
panel.margin = 0.02;
manager.addControl(panel);
startGameButton(panel);
现在正是测试你所做的更改的好时机npm run build。点击按钮后,球体应该会从空中落到地面上。
点击使球体消失
为此,我们需要打开sphere.ts文件并添加代码,ActionManager这样sphere当我们点击球体时,它就会消失。从第 17 行开始添加以下逻辑。此外,您还需要更新文件顶部的导入语句,以包含ActionManager和ExecuteCodeAction。
import { Scene, Vector3, MeshBuilder, Mesh, ShadowGenerator, DirectionalLight, ActionManager, ExecuteCodeAction } from "babylonjs";
sphere.actionManager = new ActionManager(scene);
//add click event to sphere
sphere.actionManager.registerAction(new
ExecuteCodeAction(ActionManager.OnPickUpTrigger, function () {
scene.removeMesh(sphere);
}));
点击“开始”按钮,即可添加循环以添加多个球体。
sphere.ts在函数上方添加以下代码addSphere。这样,点击按钮时就会添加 10 个球体,而不是一个。由于我们将不再直接从文件中调用该函数,addSphere因此请更新该函数。var addSphere = function (scene: Scene) {index.ts
export function addSpheres(scene: Scene) {
for (let index = 0; index < 10; index++) {
addSphere(scene);
}
}
更新index.ts文件,导入该addSpheres函数并调用该函数,而不是调用addSphere.
line 3: import { addSpheres } from "./sphere";
line 54: addSpheres(scene);
然后更新文件中的球体位置,sphere.ts避免在同一位置创建 10 个球体。删除sphere.position.y = 5;并添加
line 17: sphere.position = new Vector3(Math.random() * 20 - 10, 10, Math.random() * 10 - 5);
给球体添加粒子动画以模拟爆炸效果
球体消失的效果很酷,但为了让它更具戏剧性,我们可以添加一个particleSystem以卡通爆炸方式喷射粒子的效果。
添加一个名为 `<filename>` 的新文件particles.ts,并将以下代码粘贴到该文件中:
import { AbstractMesh, Texture, ParticleSystem, Scene, Vector3, Color4, Animation } from "babylonjs";
import { AdvancedDynamicTexture } from "babylonjs-gui";
let advancedTexture: AdvancedDynamicTexture;
export function addParticlesToMesh(mesh: AbstractMesh, scene: Scene): ParticleSystem {
// Fountain object
//var fountain = Mesh.CreateBox("foutain", 1.0, scene);
var particleSystem = new ParticleSystem("particles", 2000, scene);
//Texture of each particle
particleSystem.particleTexture = new Texture("textures/flare.png", scene);
// Where the particles come from
particleSystem.emitter = mesh; // the starting object, the emitter
particleSystem.minEmitBox = new Vector3(-1, 0, 0); // Starting all from
particleSystem.maxEmitBox = new Vector3(1, 0, 0); // To...
// Colors of all particles
particleSystem.color1 = new Color4(0.7, 0.8, 1.0, 1.0);
particleSystem.color2 = new Color4(0.2, 0.5, 1.0, 1.0);
particleSystem.colorDead = new Color4(0, 0, 0.2, 0.0);
// Size of each particle (random between...
particleSystem.minSize = 0.1;
particleSystem.maxSize = 0.5;
// Life time of each particle (random between...
particleSystem.minLifeTime = 0.3;
particleSystem.maxLifeTime = 1.5;
// Emission rate
particleSystem.emitRate = 1500;
// Blend mode : BLENDMODE_ONEONE, or BLENDMODE_STANDARD
particleSystem.blendMode = ParticleSystem.BLENDMODE_ONEONE;
// Set the gravity of all particles
particleSystem.gravity = new Vector3(0, -9.81, 0);
// Direction of each particle after it has been emitted
particleSystem.direction1 = new Vector3(-7, 8, 3);
particleSystem.direction2 = new Vector3(7, 8, -3);
// Angular speed, in radians
particleSystem.minAngularSpeed = 0;
particleSystem.maxAngularSpeed = Math.PI;
// Speed
particleSystem.minEmitPower = 1;
particleSystem.maxEmitPower = 3;
particleSystem.updateSpeed = 0.005;
// Start the particle system
particleSystem.start();
// Fountain's animation
var keys = [];
var animation = new Animation("animation", "rotation.x", 30, Animation.ANIMATIONTYPE_FLOAT,
Animation.ANIMATIONLOOPMODE_CYCLE);
// At the animation key 0, the value of scaling is "1"
keys.push({
frame: 0,
value: 0
});
// At the animation key 50, the value of scaling is "0.2"
keys.push({
frame: 50,
value: Math.PI
});
// At the animation key 100, the value of scaling is "1"
keys.push({
frame: 100,
value: 0
});
// Launch animation
animation.setKeys(keys);
mesh.animations.push(animation);
scene.beginAnimation(mesh, 0, 100, true);
return particleSystem;
}
export function removeParticlesFromMesh(particleSystem: ParticleSystem): any {
particleSystem.stop();
}
将particles.ts脚本导入到spheres.ts脚本中。
import { addParticlesToMesh, removeParticlesFromMesh } from "./particles";
更新球体的点击事件,并添加休眠函数。这样,当球体被点击时,粒子会添加到球体上,等待 250 毫秒后停止添加粒子。如果不停止添加粒子,即使球体已从场景中移除,粒子仍然会遍布整个场景。
sphere.actionManager.registerAction(new
ExecuteCodeAction(ActionManager.OnPickUpTrigger, function () {
var particleSystem = addParticlesToMesh(sphere, scene);
scene.removeMesh(sphere);
sleep(250).then(() => {
removeParticlesFromMesh(particleSystem);
})
}));
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
添加 score.ts 文件,因为每个游戏都需要一种计分方式。
创建score.ts脚本并将以下代码粘贴到脚本中。
import { AdvancedDynamicTexture, Rectangle, Control, TextBlock } from 'babylonjs-gui';
let advancedTexture: AdvancedDynamicTexture;
let scoreText: TextBlock = new TextBlock();
let score = 0;
function init(): void {
if (!advancedTexture) {
advancedTexture = AdvancedDynamicTexture.CreateFullscreenUI("ui1");
}
}
export function addLabelToScene(): void {
if (!advancedTexture) {
init();
}
let label = new Rectangle("score");
label.background = "black";
label.height = "30px";
label.alpha = 0.5;
label.width = "100px";
label.cornerRadius = 20;
label.thickness = 1;
label.linkOffsetY = 30;
label.top = "10%";
label.zIndex = 5;
label.verticalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
advancedTexture.addControl(label);
scoreText.text = "score: 0"
scoreText.color = "white";
label.addControl(scoreText);
}
export function incrementScore(): void{
score++;
scoreText.text = "score: " + score.toString();
}
export function updateScore(newScore: number): void{
score = newScore;
scoreText.text = "score: " + score.toString();
}
然后将脚本导入到index.ts脚本中。
import { addLabelToScene, updateScore } from "./score";
我们希望在文件中添加按钮之后index.ts添加函数调用,并且希望在点击按钮时重置分数。addLabelToScene(panel)startGameButton(panel);startGameButton
var startGameButton = function (panel) {
var button = new GUI.Button3D();
panel.addControl(button);
button.onPointerUpObservable.add(function () {
//reset score
updateScore(0);
addSpheres(scene);
});
var text1 = new GUI.TextBlock();
text1.text = "Start Game";
text1.color = "white";
text1.fontSize = 24;
button.content = text1;
}
sphere.ts我们需要从脚本import { incrementScore } from "./score";中获取信息score.ts,然后在点击球体时添加后缀以增加分数incrementScore();。removeParticlesFromMesh(particleSystem);
将球从地面网格中取出PhysicsImpostor,使球落入地下而不是留在地面上。
我们不希望玩家能够击打地面上的球,所以我们需要删除PhysicsImpostor地面网格。
ground.physicsImpostor = new BABYLON.PhysicsImpostor(ground, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 0, friction: 0, restitution: 0 }, scene);
最后,我们将在球体上添加一些材料。
将巴比伦素材导入脚本sphere.ts。
import {StandardMaterial, Texture, Color3} from "babylonjs-materials";
然后使用以下代码将材质添加到球体网格中。
// Material
var materialAmiga = new StandardMaterial("amiga", scene);
materialAmiga.diffuseTexture = new Texture("textures/amiga.jpg", scene);
materialAmiga.emissiveColor = new Color3(0.5, 0.5, 0.5);
sphere.material = materialAmiga;
好,我们来npm run build看看行不行!
还可以添加更多酷炫功能
- 质地
- 背景
- 自定义网格
- 音效
- 天空才是极限!
将网站作为静态网站部署到 Azure 存储,这样你的所有朋友都可以一起玩了。
特别感谢 Babylonjs 出色的文档和示例,以及辛勤工作的开发者们,他们创建了这个优秀的库,使我们能够构建游戏!
祝您游戏制作愉快!
文章来源:https://dev.to/azure/build-a-web-vr-game-with-javascript-using-the-babylonjs-framework-aek
