设为首页收藏本站|繁體中文

Project1

 找回密码
 注册会员
搜索
查看: 183|回复: 0
打印 上一主题 下一主题

[原创发布] 【源码解析】CallMenu 唤出菜单(场景替换)代码运行流程图解析

[复制链接]

Lv1.梦旅人

梦石
0
星屑
181
在线时间
25 小时
注册时间
2024-1-25
帖子
5
跳转到指定楼层
1
发表于 2024-4-5 14:47:07 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有帐号?注册会员

x
本帖最后由 循环往复的时间 于 2024-4-9 23:08 编辑

为了研究RPGmaker MV的场景替换运作原理,选取了唤出菜单这一操作进行代码运行分析
本来只是看纯代码追踪分析,奈何本人脑子太笨,总是追踪到一半就忘记,导致分析途中异常混乱,所以专门画了流程图来梳理,现在将研究内容分享出来
访问github仓库获取源文件
https://github.com/TheLushHill/rpgmaker-mv-flowchart
本帖的阅读需要一定的JavaScript基础

本帖主要涉及以下内容:1.按键的监听 2.场景的替换 3.场景的开始

CallMenu主要分为三部分 onKeyDownSceneMangager.updateonKeyUp

看图指南:紫色为比较重要的代码

一、onKeyDown
任何一个键位被按下就会触发Input._onKeyDown函数
这个函数在游戏初始化时就被注册到事件监听上
  1. Input._onKeyDown = function(event) {
  2.     if (this._shouldPreventDefault(event.keyCode)) {
  3.         event.preventDefault();
  4.     if (event.keyCode === 144) {    // Numlock
  5.         this.clear();
  6.     }
  7.     var buttonName = this.keyMapper[event.keyCode];
  8.     if (ResourceHandler.exists() && buttonName === 'ok')
  9.         ResourceHandler.retry();
  10.     } else if (buttonName) {
  11.         this._currentState[buttonName] = true;
  12.     }
  13. };
复制代码
在唤出菜单只需关注三个键位"escape","X","numpad 0"
在Input.keyMapper中这三个键被映射为"escape"

如果按下其中任何一个键
则this(Input)._currentState[buttonName] = true //buttonName = "escape"

二、SceneManager.update
每帧更新都会调用这个函数。
在唤出菜单中只有this.updateMain函数比较重要。

  1. /* this = SceneManager */

  2. SceneManager.updateMain = function() {
  3.     if (Utils.isMobileSafari()) {
  4.         this.changeScene();
  5.         this.updateScene();
  6.     } else {
  7.         var newTime = this._getTimeInMsWithoutMobileSafari();
  8.         var fTime = (newTime - this._currentTime) / 1000;
  9.         if (fTime > 0.25) fTime = 0.25;
  10.         this._currentTime = newTime;
  11.         this._accumulator += fTime;
  12.         while (this._accumulator >= this._deltaTime) {
  13.             this.updateInputData();
  14.             this.changeScene();
  15.             this.updateScene();
  16.             this._accumulator -= this._deltaTime;
  17.         }
  18.     }
  19.     this.renderScene();
  20.     this.requestUpdate();
  21. };
复制代码
进入到循环while (this._accumulator >= this._deltaTime)
第一次循环进行场景的改变,后面的循环进行新场景的开始

1.第一次循环
1)首先调用this.updateInputData更新按键数据
此时,this._latestButton 可能为null,也可能为"escape"映射之外的键位
在这一步,将 this._latestButton 更新为了"escape"
同时 this._pressedTime = 0
同时 this._previousState[name] = this._currentState[name] 将"escape“更新为上一个按键状态
按键数据更新完毕

2)this.changeScene()
  1. SceneManager.changeScene = function() {
  2.     if (this.isSceneChanging() && !this.isCurrentSceneBusy()) {
  3.         if (this._scene) {
  4.             this._scene.terminate();
  5.             this._scene.detachReservation();
  6.             this._previousClass = this._scene.constructor;
  7.         }
  8.         this._scene = this._nextScene;
  9.         if (this._scene) {
  10.             this._scene.attachReservation();
  11.             this._scene.create();
  12.             this._nextScene = null;
  13.             this._sceneStarted = false;
  14.             this.onSceneCreate();
  15.         }
  16.         if (this._exiting) {
  17.             this.terminate();
  18.         }
  19.     }
  20. };
复制代码
if (this.isSceneChanging() && !this.isCurrentSceneBusy()) //false
下面看看调用的这两个函数的具体返回值
  1. SceneManager.isSceneChanging = function() {
  2.         return this._exiting || !!this._nextScene;
  3. }
复制代码
this._exiting 退出游戏时才会为true,一般情况下都为false
this._nextScene 此时为null
这个函数返回false

  1. SceneManager.isCurrentSceneBusy = function() {
  2.     return this._scene && this._scene.isBusy();
  3. };

  4. Scene_Base.prototype.isBusy = function() {
  5.     return this._fadeDuration > 0;
  6. };
复制代码
this._fadeDuration 用于控制淡入淡出的持续时间,一般情况下为0
this.isCurrentSceneBusy()函数返回false
但是由于是 !this.isCurrentSceneBusy() 故&&右边的表达式为true

第一次循环不进入this.changeScene()

3)this.updateScene()
  1. SceneManager.updateScene = function() {
  2.     if (this._scene) {
  3.         if (!this._sceneStarted && this._scene.isReady()) {
  4.             this._scene.start();
  5.             this._sceneStarted = true;
  6.             this.onSceneStart();
  7.         }
  8.         if (this.isCurrentSceneStarted()) {
  9.             this._scene.update();
  10.         }
  11.     }
  12. };
复制代码


在第一次循环中,this._scene = Scene_Map
this_sceneStarted 记录当前场景是否已经开始,显然,Scene_Map已经开始,this._sceneStarted = true。
!this._sceneStarted && this._scene.isReady() 为false

  1. SceneManager.isCurrentSceneStarted = function() {
  2.     return this._scene && this._sceneStarted;
  3. };
复制代码
this.isCurrentSceneStarted() 为true


进入到Scene_Map.update

  1. Scene_Map.prototype.update = function() {
  2.     this.updateDestination();
  3.     this.updateMainMultiply();
  4.     if (this.isSceneChangeOk()) {
  5.         this.updateScene();
  6.     } else if (SceneManager.isNextScene(Scene_Battle)) {
  7.         this.updateEncounterEffect();
  8.     }
  9.     this.updateWaitCount();
  10.     Scene_Base.prototype.update.call(this);
  11. };
复制代码
  1. Scene_Map.prototype.isSceneChangeOk = function() {
  2.     return this.isActive() && !$gameMessage.isBusy();
  3. };

  4. Scene_Base.prototype.isActive = function() {
  5.     return this._active;
  6. };
复制代码
显而易见,此时Scene_Map正在运行,且没有消息窗口。

进入到this.updateScene()
  1. Scene_Map.prototype.updateScene = function() {
  2.     this.checkGameover();
  3.     if (!SceneManager.isSceneChanging()) {
  4.         this.updateTransferPlayer();
  5.     }
  6.     if (!SceneManager.isSceneChanging()) {
  7.         this.updateEncounter();
  8.     }
  9.     if (!SceneManager.isSceneChanging()) {
  10.         this.updateCallMenu();
  11.     }
  12.     if (!SceneManager.isSceneChanging()) {
  13.         this.updateCallDebug();
  14.     }
  15. };
复制代码
此时SceneManager.isSceneChanging()为false
可以看到this.updateCallMenu()才是我们需要研究的函数,在这个函数内判断是否唤出菜单。


进入到this.updateCallMenu()

  1. Scene_Map.prototype.updateCallMenu = function() {
  2.     if (this.isMenuEnabled()) {
  3.         if (this.isMenuCalled()) {
  4.             this.menuCalling = true;
  5.         }
  6.         if (this.menuCalling && !$gamePlayer.isMoving()) {
  7.             this.callMenu();
  8.         }
  9.     } else {
  10.         this.menuCalling = false;
  11.     }
  12. };
复制代码
首先判断了菜单是否允许开始,显然是允许的,this.isMenuEnabled()返回true

接下来要重点关注this.isMenuCalled函数,这个函数判断菜单是否要被唤出
  1. Scene_Map.prototype.isMenuCalled = function() {
  2.     return Input.isTriggered('menu') || TouchInput.isCancelled();
  3. };
  4. //判断按键输入 或 触摸输入
复制代码
  1. Input.isTriggered = function(keyName) {
  2.     if (this._isEscapeCompatible(keyName) && this.isTriggered('escape')) {
  3.         return true;
  4.     } else {
  5.         return this._latestButton === keyName && this._pressedTime === 0;
  6.     }
  7. };

  8. Input._isEscapeCompatible = function(keyName) {
  9.     return keyName === 'cancel' || keyName === 'menu';
  10. };//true
复制代码
根据前文所述this._latestButton = "escape"
this._isEscapeCompatible(keyName) 也返回true
this.isTriggered('escape')返回true
this.isMenuCalled()返回true

进入到this.callMenu()
在这一步执行切换场景的准备工作
将Scene_Map运行状态设置为停止
将下一个场景设置为Scene_Menu

  1. Scene_Map.prototype.callMenu = function() {
  2.     SoundManager.playOk();
  3.     SceneManager.push(Scene_Menu); //场景预替换
  4.     Window_MenuCommand.initCommandPosition();
  5.     $gameTemp.clearDestination();
  6.     this._mapNameWindow.hide();
  7.     this._waitCount = 2;
  8. };
复制代码
SceneManager.push(Scene_Menu)执行主要的场景预替换工作

  1. SceneManager.goto = function(sceneClass) {
  2.     if (sceneClass) {
  3.         this._nextScene = new sceneClass();
  4.     }
  5.     if (this._scene) {
  6.         this._scene.stop();
  7.     }
  8. };
复制代码
this._nextScene 初始化为Scene_Menu实例  

this._scene.stop()将Scene_Map运行状态设置为停止
主要关注一点Scene_Map._active = false

运行到这一步,updateScene()结束了。

2.第二次循环


1)this.updateInputData()
将Input._pressedTime加1


2)this.changeScene()
  1. SceneManager.changeScene = function() {
  2.     if (this.isSceneChanging() && !this.isCurrentSceneBusy()) {
  3.         if (this._scene) {
  4.             this._scene.terminate();
  5.             this._scene.detachReservation();
  6.             this._previousClass = this._scene.constructor;
  7.         }

  8.         this._scene = this._nextScene;

  9.         if (this._scene) {
  10.             this._scene.attachReservation();
  11.             this._scene.create();
  12.             this._nextScene = null;
  13.             this._sceneStarted = false;
  14.             this.onSceneCreate();
  15.         }
  16.         if (this._exiting) {
  17.             this.terminate();
  18.         }
  19.     }
  20. };
复制代码
据前文所述this.isCurrentSceneBusy()一般情况下为false
而this.isSceneChanging()由于_nextscene = Scene_Menu 返回值为true
此时会进入changeScene函数执行场景的替换

一开始this._scene = Scene_Map
this._scene.terminate() 执行Scene_Map的停止

this._scene = this._nextScene 更换当前场景
this._scene.create() 创建Scene_Menu
具体怎么创建在此就不展开了,想知道可以去研究Scene_Menu.prototype.create函数

  1. Scene_Menu.prototype.create = function() {
  2.     Scene_MenuBase.prototype.create.call(this) //背景
  3.     this.createCommandWindow(); //左上角选项
  4.     this.createGoldWindow(); //金币窗口
  5.     this.createStatusWindow() //右侧状态窗口
  6. };
复制代码
this._sceneStarted = false 标识当前场景未开始


3)this.updateScene
  1. SceneManager.updateScene = function() {
  2.     if (this._scene) {
  3.         if (!this._sceneStarted && this._scene.isReady()) {
  4.             this._scene.start();
  5.             this._sceneStarted = true;
  6.             this.onSceneStart();
  7.         }
  8.         if (this.isCurrentSceneStarted()) {
  9.             this._scene.update();
  10.         }
  11.     }
  12. };
复制代码
!this._sceneStarted = true
this._scene.isReady() 当Scene_Menu准备好,返回true
主要就是确认图片资源是否缓存好,这里假定已经缓存好

this._scene.start()
  1. Scene_Menu.prototype.start = function()
  2.     Scene_MenuBase.prototype.start.call(this); //Scene_Base.prototype.start
  3.     this._statusWindow.refresh(); //刷新右侧状态窗口
  4. };
复制代码

至此,唤出菜单的场景切换就结束了。
this._scene.update() 调用Scene_Base.prototype.update更新children
接下来的循环就是窗口状态的更新

三、onKeyUp
这一步比较简单
  1. Input._onKeyUp = function(event) {
  2.     var buttonName = this.keyMapper[event.keyCode];
  3.     if (buttonName) {
  4.         this._currentState[buttonName] = false;
  5.     }
  6.     if (event.keyCode === 0) {  // For QtWebEngine on OS X
  7.         this.clear();
  8.     }
  9. };
复制代码
就是将按键的状态设置为false















评分

参与人数 1+1 收起 理由
505681468 + 1 塞糖

查看全部评分

青山依旧在,几度夕阳红。
您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

拿上你的纸笔,建造一个属于你的梦想世界,加入吧。
 注册会员
找回密码

站长信箱:[email protected]|手机版|小黑屋|无图版|Project1游戏制作

GMT+8, 2024-5-3 02:11

Powered by Discuz! X3.1

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表