Project1

标题: RPG Maker MZ 游戏引擎源码研究 - (1) 游戏入口 [打印本页]

作者: etherstalker    时间: 2023-5-9 00:20
标题: RPG Maker MZ 游戏引擎源码研究 - (1) 游戏入口
本帖最后由 etherstalker 于 2023-5-9 00:21 编辑

游戏跑在nw.js上。nw.js可以让DOM能够直接调用Node.js的模块。这样就可以用HTML,CSS和JS来写应用程序。RPG Maker 这么做是一是为了简化各平台上的分发,二是可以用web技术来开发整个引擎。

NW.js (previously known as node-webkit) lets you call all Node.js modules directly from DOM and enables a new way of writing applications with all Web technologies.


JAVASCRIPT 代码复制
  1. const main = new Main();
  2. main.run();


Main.run() 中一共有四步

JAVASCRIPT 代码复制
  1. run() {
  2.         this.showLoadingSpinner();
  3.         this.testXhr();
  4.         this.hookNwjsClose();
  5.         this.loadMainScripts();
  6.     }


第一步是显示一个读取界面的spinner动画,这是用css实现的

JAVASCRIPT 代码复制
  1. showLoadingSpinner() {
  2.         const loadingSpinner = document.createElement("div");  
  3.         const loadingSpinnerImage = document.createElement("div");
  4.         loadingSpinner.id = "loadingSpinner";
  5.         loadingSpinnerImage.id = "loadingSpinnerImage";
  6.         loadingSpinner.appendChild(loadingSpinnerImage);
  7.         document.body.appendChild(loadingSpinner);
  8.     }


对应的css可以在css/game.css 中找到

CSS 代码复制
  1. #loadingSpinner {
  2.     margin: auto;
  3.     position: absolute;
  4.     top: 0px;
  5.     left: 0px;
  6.     right: 0px;
  7.     bottom: 0px;
  8.     width: 120px;
  9.     height: 120px;
  10.     z-index: 10;
  11. }
  12. #loadingSpinnerImage {
  13.     margin: 0px;
  14.     padding: 0px;
  15.     border-radius: 50%;
  16.     width: 96px;
  17.     height: 96px;
  18.     border: 12px solid rgba(255, 255, 255, 0.25);
  19.     border-top: 12px solid rgba(255, 255, 255, 1);
  20.     animation: fadein 2s ease, spin 1.5s linear infinite;
  21. }


读取动画的效果:



第二步会测试XHR。XHR就是XMLHttpRequest,是微软在IE5发布的时候第一次引入的功能,允许js脚本向服务器发起HTTP的请求,是AJAX技术的核心。这里测试的方法就是发送一个XMLHttpRequest的GET请求,获取当前运行的脚本。要注意这里的测试是异步的,因为测试的结果暂时还没有用到。


JAVASCRIPT 代码复制
  1. testXhr() {
  2.         const xhr = new XMLHttpRequest();
  3.         xhr.open("GET", document.currentScript.src);    //initializes a newly-created request, or re-initializes an existing one - open(method, url)
  4.         xhr.onload = () => (this.xhrSucceeded = true);
  5.         xhr.send();  //sends the request to the server.
  6.     }


第三步是个补丁,注释说关闭窗口的时候nw.js进程可能有时候不会被终止,所以这里强制把窗口关闭事件和退出app绑定在一起。

JAVASCRIPT 代码复制
  1. hookNwjsClose() {
  2.         // [Note] When closing the window, the NW.js process sometimes does
  3.         //   not terminate properly. This code is a workaround for that.
  4.         if (typeof nw === "object") {
  5.             nw.Window.get().on("close", () => nw.App.quit());
  6.         }
  7.     }


第四步才是整个过程的核心,把每一个script都放上DOM来load。这些script都用defer但不是async的方法来读取,也就是在后台异步下载,然后在DOM ready的时候按顺序执行。每个script在load完成之后会更新loadCount,最后一个script读取完之后,PluginManager会开始设置插件。在window load完之后,这时候才来检查之前的XHR是不是成功。然后检查是不是游戏在/private/var里面运行,不然的话可能存不了档,这一段不是太重要就不贴了。然后会通过调取initEffekseerRuntime() 来初始化Effekseer这个用来实现粒子效果的工具。

JAVASCRIPT 代码复制
  1. const scriptUrls = [
  2.     "js/libs/pixi.js",
  3.     "js/libs/pako.min.js",
  4.     "js/libs/localforage.min.js",
  5.     "js/libs/effekseer.min.js",
  6.     "js/libs/vorbisdecoder.js",
  7.     "js/rmmz_core.js",
  8.     "js/rmmz_managers.js",
  9.     "js/rmmz_objects.js",
  10.     "js/rmmz_scenes.js",
  11.     "js/rmmz_sprites.js",
  12.     "js/rmmz_windows.js",
  13.     "js/plugins.js"
  14. ];   
  15.  
  16.   loadMainScripts() {
  17.         for (const url of scriptUrls) {
  18.             const script = document.createElement("script");
  19.             script.type = "text/javascript";
  20.             script.src = url;
  21.             script.async = false;
  22.             script.defer = true;
  23.             script.onload = this.onScriptLoad.bind(this);
  24.             script.onerror = this.onScriptError.bind(this);
  25.             script._url = url;
  26.             document.body.appendChild(script);
  27.         }
  28.         this.numScripts = scriptUrls.length;
  29.         window.addEventListener("load", this.onWindowLoad.bind(this));
  30.         window.addEventListener("error", this.onWindowError.bind(this));
  31.     }
  32.  
  33.   onScriptLoad() {
  34.         if (++this.loadCount === this.numScripts) {
  35.             PluginManager.setup($plugins);
  36.         }
  37.     }
  38.  
  39.   onScriptError(e) {
  40.       this.printError("Failed to load", e.target._url);
  41.   }
  42.  
  43. onWindowLoad() {
  44.         if (!this.xhrSucceeded) {
  45.             const message = "Your browser does not allow to read local files.";
  46.             this.printError("Error", message);
  47.         } else if (this.isPathRandomized()) {
  48.             const message = "Please move the Game.app to a different folder.";
  49.             this.printError("Error", message);
  50.         } else if (this.error) {
  51.             this.printError(this.error.name, this.error.message);
  52.         } else {
  53.           this.initEffekseerRuntime();
  54.       }
  55.   }
  56.  
  57.   onWindowError(event) {
  58.       if (!this.error) {
  59.           this.error = event.error;
  60.       }
  61.   }


这里在初始化Effekseer之后就把读取的spinner给擦除了,然后我们会就可以读取Scene_Boot这个场景来进入到第一个画面。

JAVASCRIPT 代码复制
  1. const effekseerWasmUrl = "js/libs/effekseer.wasm";
  2.  
  3.     initEffekseerRuntime() {
  4.         const onLoad = this.onEffekseerLoad.bind(this);
  5.         const onError = this.onEffekseerError.bind(this);
  6.         effekseer.initRuntime(effekseerWasmUrl, onLoad, onError);
  7.     }
  8.  
  9.     onEffekseerLoad() {
  10.         this.eraseLoadingSpinner();
  11.         SceneManager.run(Scene_Boot);
  12.     }
  13.  
  14.     onEffekseerError() {
  15.         this.printError("Failed to load", effekseerWasmUrl);
  16.     }


顺便来研究一下这里显示错误信息的方式。错误有名称和消息。如果遇到了错误的话,首先会把读取动画给停止,然后插入一个errorPrinter的div来分别显示错误的名称和消息。

JAVASCRIPT 代码复制
  1. printError(name, message) {
  2.         this.eraseLoadingSpinner();
  3.         if (!document.getElementById("errorPrinter")) {
  4.             const errorPrinter = document.createElement("div");
  5.             errorPrinter.id = "errorPrinter";
  6.             errorPrinter.innerHTML = this.makeErrorHtml(name, message);
  7.             document.body.appendChild(errorPrinter);
  8.         }
  9.     }
  10.     makeErrorHtml(name, message) {
  11.         const nameDiv = document.createElement("div");
  12.         const messageDiv = document.createElement("div");
  13.         nameDiv.id = "errorName";
  14.         messageDiv.id = "errorMessage";
  15.         nameDiv.innerHTML = name;
  16.         messageDiv.innerHTML = message;
  17.         return nameDiv.outerHTML + messageDiv.outerHTML;
  18.     }


显示出的错误如下:



下一章节将会贴出关于场景管理器的研究。
作者: 恶魔捏手    时间: 2023-5-10 16:56
厉害,跟着一起学习




欢迎光临 Project1 (https://rpg.blue/) Powered by Discuz! X3.1