babylonjs游戏教程 – 设置状态机
在整个应用程序中使用单个场景是完全可能的,但在我的游戏中,我想将这些状态划分为单独的场景。所以,我创建了一个状态机来处理整个游戏的不同场景的渲染。
App.ts
这将是我们处理场景创建和渲染的主文件。从构造函数开始,我们将把我们的场景创建和渲染循环调用分解成单独的函数。
状态
我是通过列出我在游戏中需要的所有不同场景来实现这一点的:
- 启动
- 场景画面
- 游戏
- 失败
没有胜利和暂停状态的原因是,这些实际上仍然在使用游戏场景,所以它仍然需要能够渲染游戏场景。我将这两个“状态”作为GUI的叠加。现在我们知道了我们想要的状态,我们可以继续并为它们创建一个枚举。enum所做的就是为这些状态分配名称,并将它们编码为数字。我们还希望创建一个类变量_state来存储我们所处的当前状态。现在,我们的app.ts应该是这样的:
//...这里是入口
//枚举的状态
enum State { START = 0, GAME = 1, LOSE = 2, CUTSCENE = 3 }
class App {
// 完整的应用程序
private _scene: Scene;
private _canvas: HTMLCanvasElement;
private _engine: Engine;
//场景相关
private _state: number = 0;
constructor() {
this._canvas = this._createCanvas();
// 初始化巴比伦场景和引擎
this._engine = new Engine(this._canvas, true);
this._scene = new Scene(this._engine);
var camera: ArcRotateCamera = new ArcRotateCamera("Camera", Math.PI / 2, Math.PI / 2, 2, Vector3.Zero(), this._scene);
camera.attachControl(this._canvas, true);
var light1: HemisphericLight = new HemisphericLight("light1", new Vector3(1, 1, 0), this._scene);
var sphere: Mesh = MeshBuilder.CreateSphere("sphere", { diameter: 1 }, this._scene);
// 隐藏/显示 Inspector
window.addEventListener("keydown", (ev) => {
// Shift+Ctrl+Alt+I
if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) {
if (this._scene.debugLayer.isVisible()) {
this._scene.debugLayer.hide();
} else {
this._scene.debugLayer.show();
}
}
});
// 运行主渲染循环
this._engine.runRenderLoop(() => {
this._scene.render();
});
}
}
new App();
我还创建了一个单独的函数来创建我们的画布,名为_createCanvas。此外,我们将从这里开始使用类变量(由this关键字表示)。
转场功能
场景设定
转场功能将负责设置场景,并包含只发生一次的事情。
让我们从_goToStart开始,这是一个如何设置场景的简单例子。
this._engine.displayLoadingUI();
在开始场景加载时显示加载UI。
this._scene.detachControl();
let scene = new Scene(this._engine);
scene.clearColor = new Color4(0, 0, 0, 1);
let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene);
camera.setTarget(Vector3.Zero());
创建场景和相机。任何相机都应该没问题,因为它会在场景中心被修复,所以我只使用FreeCamera。
//...做gui相关的事情
//--完成场景加载--
await scene.whenReadyAsync();
this._engine.hideLoadingUI();
//lastly set the current state to the start state and set the scene to the start scene
this._scene.dispose();
this._scene = scene;
this._state = State.START;
当场景准备好后,我们隐藏加载UI,处理当前存储的场景,然后切换场景,改变状态来渲染新的场景。
VSCode用户:在任何时候,如果你看到一个错误的巴比伦特定组件(如Color4和FreeCamera…)悬停它,你应该看到一个快速修复选项,这将为你的导入添加它。如果您没有看到这一点,您可以手动将其添加到文件顶部的导入中
GUI 设置
现在,我们将制作一个简单的全屏幕ui,带有一个按钮来切换场景。GUI元素需要从“@babylonjs/ GUI”导入。
//... 场景设置
//为我们所有的GUI元素创建一个全屏ui
const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI");
guiMenu.idealHeight = 720; //fit our fullscreen ui to this height
//创建一个简单的按钮
const startBtn = Button.CreateSimpleButton("start", "PLAY");
startBtn.width = 0.2;
startBtn.height = "40px";
startBtn.color = "white";
startBtn.top = "-14px";
startBtn.thickness = 0;
startBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
guiMenu.addControl(startBtn);
//这处理与开始按钮附加到场景的交互
startBtn.onPointerDownObservable.add(() => {
this._goToCutScene();
scene.detachControl(); //observables disabled
});
这里我们做的是创建一个AdvancedDynamicTexture fullscreenUI。这是用来保存所有gui元素的。然后,我们创建了一个简单的按钮,并添加了一个可观察对象,以便在点击它时进行检测。这将触发我们的场景调用goToCutScene。我们想要确保我们分离了控制,因为当我们按住鼠标时,goToCutScene可能会被调用多次。
其他状态
失败状态将遵循类似的格式,但出于组织和表现目的,过场动画和游戏状态的结构略有不同。
转场失败
private async _goToLose(): Promise<void> {
this._engine.displayLoadingUI();
//--SCENE SETUP--
this._scene.detachControl();
let scene = new Scene(this._engine);
scene.clearColor = new Color4(0, 0, 0, 1);
let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene);
camera.setTarget(Vector3.Zero());
//--GUI--
const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI");
const mainBtn = Button.CreateSimpleButton("mainmenu", "MAIN MENU");
mainBtn.width = 0.2;
mainBtn.height = "40px";
mainBtn.color = "white";
guiMenu.addControl(mainBtn);
//this handles interactions with the start button attached to the scene
mainBtn.onPointerUpObservable.add(() => {
this._goToStart();
});
//--SCENE FINISHED LOADING--
await scene.whenReadyAsync();
this._engine.hideLoadingUI(); //when the scene is ready, hide loading
//lastly set the current state to the lose state and set the scene to the lose scene
this._scene.dispose();
this._scene = scene;
this._state = State.LOSE;
}
转场动画_goToCutScene
转场动画通常与gui一起设置;然而,我们在这种状态下所做的是让我们的游戏能够正确加载。如果你看一下_goToCutScene函数,场景设置是一样的,但场景完成加载略有不同。注意我们没有hideLoadingUI。现在,我们需要添加它,但在最终版本中,我实际上删除了它,因为我在动画加载完成后隐藏它,然后在我们完成对话后触发它显示,但游戏仍在加载中。
最重要的方面是我们在那之后做什么:
var finishedLoading = false;
await this._setUpGame().then((res) => {
finishedLoading = true;
});
本质上,这是告诉代码等待直到_setUpGame完成了它的任务,然后设置finishhedloading为true。在这一点上,这似乎是不必要的,因为我们还没有引入动画,也没有加载任何重资产,但一旦我们进入开发过程的这一阶段,这就非常重要了。
这是一个重要的发现,最终促使我改变了游戏导入和加载资产的结构。如果我们不等待我们的资产完成导入,异步函数将告诉我们的代码继续在后台加载。这最终会破坏我们在场景之间的转换,因为我们需要在内容完全加载之前继续前进。我在测试游戏的网页托管版本时发现了这种情况:
- Safari有几个与声音和场景转换有关的问题
- 资产需要很长时间来加载,因此显示未定义的网格错误
出于测试目的,我们将添加一个next按钮,直接使用到游戏状态:
//--对话进展--
const next = Button.CreateSimpleButton("next", "NEXT");
next.color = "white";
next.thickness = 0;
next.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
next.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT;
next.width = "64px";
next.height = "64px";
next.top = "-3%";
next.left = "-12%";
cutScene.addControl(next);
next.onPointerUpObservable.add(() => {
this._goToGame();
});
游戏设置 _setUpGam
现在我们唯一需要担心的是:
private async _setUpGame() {
let scene = new Scene(this._engine);
this._gamescene = scene;
//...资源加载
}
_setUpGame是我们预创建游戏场景的地方,也是我们开始加载所有资产的地方。
转场游戏_goToGame
如果你看一下_goToGame函数,我们实际上已经将相机设置和gui设置封装到它们自己的函数中。现在你可以像这样使用默认的UI和摄像头:
private async _goToGame(){
//--场景设定--
this._scene.detachControl();
let scene = this._gamescene;
scene.clearColor = new Color4(0.01568627450980392, 0.01568627450980392, 0.20392156862745098); // 一种更适合整体配色方案的颜色
let camera: ArcRotateCamera = new ArcRotateCamera("Camera", Math.PI / 2, Math.PI / 2, 2, Vector3.Zero(), scene);
camera.setTarget(Vector3.Zero());
//--GUI--
const playerUI = AdvancedDynamicTexture.CreateFullscreenUI("UI");
//当游戏加载时,不要检测任何来自这个ui的输入
scene.detachControl();
//创建一个简单的按钮
const loseBtn = Button.CreateSimpleButton("lose", "LOSE");
loseBtn.width = 0.2
loseBtn.height = "40px";
loseBtn.color = "white";
loseBtn.top = "-14px";
loseBtn.thickness = 0;
loseBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
playerUI.addControl(loseBtn);
//这里处理与开始按钮附加到场景的交互
loseBtn.onPointerDownObservable.add(() => {
this._goToLose();
scene.detachControl(); //禁用可见
});
//临时场景对象
var light1: HemisphericLight = new HemisphericLight("light1", new Vector3(1, 1, 0), scene);
var sphere: Mesh = MeshBuilder.CreateSphere("sphere", { diameter: 1 }, scene);
//离开开始场景,切换到游戏场景并改变状态
this._scene.dispose();
this._state = State.GAME;
this._scene = scene;
this._engine.hideLoadingUI();
//游戏已经准备好了,重新控制
this._scene.attachControl();
}
我们在这里所做的是正常地设置场景,并添加一个简单的按钮来测试是否进入丢失状态。
我们也使用这个特定的场景,将我们的光和球体物体移动到这个函数中。
开关状态
现在我们已经设置好了场景,我们如何在它们之间进行渲染和切换呢?在App.ts的构造函数中,我们需要调用main。
主函数main
main函数是我们设置状态机的地方。这将取代我们第一次创建场景时设置的This ._engine. runrenderloop
private async _main(): Promise<void> {
await this._goToStart();
// 注册一个渲染循环来重复渲染场景
this._engine.runRenderLoop(() => {
switch (this._state) {
case State.START:
this._scene.render();
break;
case State.CUTSCENE:
this._scene.render();
break;
case State.GAME:
this._scene.render();
break;
case State.LOSE:
this._scene.render();
break;
default: break;
}
});
//如果屏幕被调整大小/旋转,则调整大小
window.addEventListener('resize', () => {
this._engine.resize();
});
}
我们首先调用await _goToStart来确保我们的场景已经准备好被渲染了。
switch语句所做的是,它告诉渲染循环根据我们所处的状态进行不同的操作。似乎没有必要总是调用它。_scene在每个状态,但这实际上保存了对我们当前场景的引用。回想一下,我们处理的是。_scene was,对那个场景进行其他分离,创建一个新场景,然后重新分配这个。场景到新的场景。你当然可以使用变量来引用不同的场景,但我认为这样会更好,因为我们会在不使用的时候处理场景,这确保了我们的渲染
现在,当我们运行游戏并通过状态时,我们应该看到我们的领域!ts文件现在应该是这样的。这是一个简单的工作状态机!您可以根据需要对其进行修改。
如果您在通过这些状态时遇到了麻烦,请打开浏览器的检查器,查看控制台中显示了什么错误(您可能需要注释掉画布的样式,以便能够打开检查器)。