Skip to content

第一个前端工程

简介

RoyEngine的任何工程都会有一个最小的工程模板,无论是自定义插件、最终的web端工程还是server端工程。对于前端终端工程来说,参考的工程有两个:

  • 纯粹的不集成任何框架的工程:位于apps/roynativepc
  • 与Umi或AntDesignPro结合的工程:位于umis/simpleAppTemplate或umis/antdProTemplate,Umi和AntDesignPro都是阿里系的前端开发框架,二者及其相似,也有包含关系,这里参考任何一个工程都可以。

详述

这里以roynativepc为例,介绍工程的配置和初始化。

首先,工程本身集成了很多易于开发的开发套件:

  • eslint:用于检查代码规范
  • prettier:用于格式化代码,同时与eslint配合
  • husky:git的钩子,在git上传的时候,使用eslint和prettier检查代码
  • commitlint:git的message的规范化,提交的时候也要求提交信息是规范的
  • editorconfig:编辑器的文本输入规范,如使用tab还是space,如果是space的是4个空格还是2个空格等。

第二,工程基于webpack作为打包工具:

  • 基本过程:清理生成目录,编译ts且处理外部依赖,打包assets,处理html模板。是一个正常的webpack流程。
  • 执行pnpm build:dev:即可在dist中生成最终产物。
  • 注意:为了规避循环依赖的问题,以external的方式引入@sd开头的包。

最后让我们来看看代码,只有两个文件,index.ts和AppNativePC.ts。

  • index.ts内容很少,只是示意性的执行初始化引擎和启动引擎两个函数。
typescript
import { initEngine, startEngine } from './AppNativePC';

async function start() {
    await initEngine();
    await startEngine();
}

start();

// @ts-expect-error: ROY_PCK_VERSION is from package.json
export const VERSION = ROY_PCK_VERSION;
  • 然后是AppNativePC.ts,发现其中有两个函数和一个类。两个函数分别是在index.ts调用的initEngine(加载引擎的wasm文件、初始化引擎)和startEngine(开始进入主循环)
typescript
export const initEngine = (): Promise<boolean> => {
    const rlt = new Promise<boolean>((resolve) => {
        const urlSearch = new URLSearchParams(location.search);
        const backend: string = urlSearch.get('backend') || 'webgl';

        registerSImpl();

        RoyEngine.init(
            ['./assets/favicon.ico'],
            () => {
                const mainCanvas = document.getElementById('layout-canvas') as HTMLCanvasElement;
                const touchCanvas = document.getElementById('layout-touch') as HTMLCanvasElement;

                const elementData: ElementData = {
                    mainCanvas: mainCanvas,
                    touchCanvas: touchCanvas,
                    stretchCanvas: true,

                    hireachyPanel: document.getElementById('layout-hireachy'),
                    inspectorPanel: document.getElementById('layout-inspector'),
                    assetsPanel: document.getElementById('layout-assets'),
                };
                _GAppNativePC = new AppNativePC(elementData);
                window['app'] = _GAppNativePC;

                resolve(true);
            },
            backend,
        );
    });

    return rlt;
};

export const startEngine = (): Promise<boolean> => {
    const rlt = new Promise<boolean>((resolve) => {
        const loadPlugin = _GAppNativePC.appFsm.getAppState('LoadPlugin') as AppStateLoadPlugin;
        loadPlugin.callback = () => resolve(true);
        _GAppNativePC.appFsm.start();
    });

    return rlt;
};

我们可以看到在initEngine中,在引擎初始化的回调中,还new了我们的唯一的一个类AppNativePC,AppNativePC的代码如下

typescript
export class AppNativePC extends AppLite {
    public constructor(elementData: ElementData) {
        super(elementData);
    }

    override onInit() {
        super.onInit();

        // gltf
        this.registerGltfMatTemplate(true);
        this.registerBaseMatTemplate();

        // other
        this.registerMaterialTemplate(new MatGizmoGridFloor());
        this.registerMaterialTemplate(new MatShadowPlane());
        this.registerMaterialTemplate(new MatGroundShadow());

        this.registerRoyRes<RoyResImage>(RoyResImage);
    }

    override onStart() {
        const sceneCreator: FastSceneCreator = new FastSceneCreator(this);
        sceneCreator.buildScene().buildCamera().buildIndirectLightComp().buildSunLightComp().buildSkyboxComp().buildLight().buildAssistLine();
        this.m_scene = sceneCreator.scene;
        this.m_sunLightComp = sceneCreator.sunLightComp;
        this.m_indirectLightComp = sceneCreator.indirectLightComp;
        this.m_skyboxComp = sceneCreator.skyboxComp;
        this.m_assistLine = sceneCreator.assistLine;

        if (RoyRoot.hasRendererType(RendererType.Base)) {
            this.m_scene.lightMode = LightModeType.PreviewPerfect; // Never Use PreviewPerformance
        } else {
            if (this.deviceInfo.isMobile) {
                this.m_scene.lightMode = LightModeType.PreviewPerformance;
            } else {
                this.m_scene.lightMode = LightModeType.PreviewPerfect;
            }
        }

        sceneCreator.buildIbl('indoor_bathroom', 43410, false);
        if (RoyRoot.hasRendererType(RendererType.Base)) {
            sceneCreator.buildSkyboxFromImage('city');
            this.skyboxComp.baseRotation = -Math.PI * 0.5;
        } else {
            sceneCreator.buildSkyboxFromIBL('studio_tomoco', false);
        }

        this.m_camereSceneNode = sceneCreator.camereSceneNode;
        this.m_cameraComp = sceneCreator.cameraComp;
        this.m_fov = this.cacheCanvasWidth > this.cacheCanvasHeight ? this.m_fovHorizontalScreen : this.m_fovVerticalScreen;
        this.m_cameraComp.setProjectionPerspective(this.m_fov, this.cacheCanvasWidth, this.cacheCanvasHeight, this.m_cameraNear, this.m_cameraFar, RoyCameraFOVType.FOV_HORIZONTAL);

        const dpr: number = this.elementData.stretchCanvas ? window.devicePixelRatio : 1;
        this.m_cameraComp.viewRect = [0, 0, this.cacheCanvasWidth * dpr, this.cacheCanvasHeight * dpr];
        this.m_cameraComp.bloomOption = { enabled: false };

        this.createSelector([ObjectSelectorStroke]);
        this.createCameraCtrl(CamManipulatorOrbit);
        (this.m_cameraCtrl as unknown as CamManipulatorOrbit).jumpToTransform({
            phi: 0.4918672643969554,
            theta: -0.704592653589793,
            distance: 5.759142264341616,
            pivot: new Vec3(0.8441087960683509, -0.19999999999999998, -0.9885501164259002),
        });

        this.m_mouseEventStragety = new MouseEventStragety(this.m_elementData.touchCanvas, this.m_cameraComp);
        this.m_mouseEventStragety.registerStragety(new MESCameraCtrl(this.m_mouseEventStragety, this.m_cameraCtrl));
        this.m_mouseEventStragety.registerStragety(new MESObjectSelect(this.m_mouseEventStragety, this.m_objSelector));

        super.onStart();

        this.onCreateProcGemo('Torus', ProcGemo.createTorus);
    }

    private onCreateProcGemo(name: string, execFunc: () => RoyMesh) {
        const royMesh: RoyMesh = execFunc();
        const royMaterial: RoyMaterialInstance = RoyRoot.get().royMatTemplateMgr.createMatInstance(MatGltfLitOpaque.MatName);

        const sceneNode = this.m_scene.createSceneNode(name);
        this.m_scene.rootSceneNode.addChild(sceneNode);

        const meshComp: RoyMeshComponent = sceneNode.createComponent(RoyMeshComponent);
        meshComp.setMesh(royMesh, 1);
        meshComp.setMaterial(0, royMaterial);
        meshComp.setGeometryRange(0, RenderableManager$PrimitiveType.TRIANGLES, 0, royMesh.getIndexBufferData().length);

        const createAction: HistoryAction = {
            name: 'CreateProcGemo',
            undo: () => {
                sceneNode.enabled = false;
                this.m_scene.rootSceneNode.removeChild(sceneNode);
            },
            redo: () => {
                this.m_scene.rootSceneNode.addChild(sceneNode);
                sceneNode.enabled = true;
            },
            combine: false,
            level: HistoryLevelType.Normal,
        };
        GHistory.add(createAction);
    }
}

我们可以看到AppNativePC的代码似乎并不复杂,里面有三个函数

  • onInit函数用于初始化一些材质,注册了一个Image的资源加载器
  • OnStart开始创建场景、相机
  • onCreateProcGemo是在OnStart最后调用,也就是在场景创建好之后,又创建了一个面包圈的几何体用于显示。最终的显示如图

工程示例

其实AppNativePC是继承自AppLite的,在继承的父类中包含了许多被封装的细节,这些封装的细节在项目与项目之间具有相似性,所以被封装起来。下面简述一些继承的每一个父类都做了什么。

  • AppNativePC,继承自AppLite,是项目的最终的类,任何项目侧与App有关的特化逻辑都可以写在其身上。
  • AppLite,继承自AppClient,封装了通用性业务逻辑,如创建选择器、相机控制器,封装了引擎的update,封装了窗口的resize的处理
  • AppClient,继承自AppBase,封装了web平台相关的内容,如浏览器Dom的处理,webgl或webgpu的初始化,鼠标、触摸和键盘事件的处理,有限状态机的封装。
  • AppBase,基类,封装了作为App,不仅是前端也包括服务端的共有行为,如插件管理器、资源管理器、对象存粹等内容

在实际的项目开发中,可以根据情况,有选择的继承的某一个级别,比如项目需要更大的自定义,不想继承AppLite,那么可以直接继承AppClient,然后自己实现特化的部分。