babylonjs游戏教程 - 设置状态机

866 阅读8分钟

在整个应用程序中使用单个场景是完全可能的,但在我的游戏中,我想将这些状态划分为单独的场景。所以,我创建了一个状态机来处理整个游戏的不同场景的渲染。

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。在这一点上,这似乎是不必要的,因为我们还没有引入动画,也没有加载任何重资产,但一旦我们进入开发过程的这一阶段,这就非常重要了。

这是一个重要的发现,最终促使我改变了游戏导入和加载资产的结构。如果我们不等待我们的资产完成导入,异步函数将告诉我们的代码继续在后台加载。这最终会破坏我们在场景之间的转换,因为我们需要在内容完全加载之前继续前进。我在测试游戏的网页托管版本时发现了这种情况:

  1. Safari有几个与声音和场景转换有关的问题
  2. 资产需要很长时间来加载,因此显示未定义的网格错误

出于测试目的,我们将添加一个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文件现在应该是这样的。这是一个简单的工作状态机!您可以根据需要对其进行修改。

如果您在通过这些状态时遇到了麻烦,请打开浏览器的检查器,查看控制台中显示了什么错误(您可能需要注释掉画布的样式,以便能够打开检查器)。