【RPG Maker MV插件编程】【实例教程3】玩转菜单初级篇
- 作者:Mandarava(鳗驼螺)
- 微博:@鳗驼螺pro
RMMV的菜单很丰富,包括主菜单(Scene_Menu),物品菜单(Scene_Item)、技能菜单(Scene_Skill)、装备菜单(Scene_Equip)、状态菜单(Scene_Status)、设置菜单(Scene_Options)、保存菜单(Scene_Save)、加载菜单(Scene_Load)、游戏结束菜单(Scene_GameEnd),还有调试菜单(Scene_Debug)、商店菜单(Scene_Shop)、(输入)姓名菜单(Scene_Name)等。标题菜单(Scene_Title)和前面这些有点不同,不属于Scene_MenuBase
的子类,本文不涉及它,以后再写文章单独研究一下。
本文涉及的内容包括:
- 给各个菜单界面添加背景
- 让背景滚动起来
- 在主菜单界面增加自定义菜单:改名
- 在主菜单界面移除菜单命令
- 在主菜单界面增加一个自定义窗口
创建一个名为 MND_SceneMenuEx1.js 的JavaScript文件,保存到 js/plugins 目录下,在RMMV的插件管理中安装该插件。
给菜单界面添加背景
主菜单界面对应Scene_Menu
类,其父类是Scene_MenuBase
,分析Scene_MenuBase
类源码,可以在其中找到一个名为Scene_MenuBase.prototype.createBackground
的方法,显然,这个方法是用来创建菜单界面背景的。
除非想要将所有菜单界面(如物品菜单、装备菜单等界面)的背景都设置为相同,否则不要直接重写Scene_MenuBase
类的该方法,因为Scene_Item
,Scene_Equip
等类都是Scene_MenuBase
的子类,且所有这些子类默认都没有重写该方法(所以实质上它们使用同一样式的背景),一旦重写基类的该方法,将影响所有子类(当然,这个问题其实很好解决,稍后说明)。
所以,如果要对不同的菜单界面使用不同的背景,就需要分别重写各个菜单界面类的该方法。下面的代码重写主菜单Scene_Menu
的该方法,以使主菜单界面有独立的背景:
Scene_Menu.prototype.createBackground = function() {
this._backgroundSprite=new Sprite();
this._backgroundSprite.bitmap=ImageManager.loadParallax("Mountains1");
this.addChild(this._backgroundSprite);
};
this._backgroundSprite
在基类Scene_MenuBase
中定义和使用的精灵变量,用于显示背景图片,为了兼容,这里直接使用该变量。ImageManager
类处理各种图片资源的加载,ImageManager.loadParallax("Mountains1")
方法会从 img/parallaxes 目录加载指定名称的图片,这里指定加载 Mountains1.png 图片作为背景图片。this.addChild(this._backgroundSprite)
将创建的精灵加入当前场景。运行测试,效果如下图:
前面说了,菜单界面有十几个,如果每个菜单界面都要改为单独的背景,或者某几个菜单界面想要共用一个背景,那是否一定就得为这十几个菜单类重写十几个createBackground
方法呢?
其实有简单点的方法,就是前文否定的只重写Scene_MenuBase
类的该方法的方式。因为这十几个菜单类都是Scene_MenuBase
类的子类,且它们默认都没有重写createBackground
方法,所以实际是可以直接改造该方法的,而避免各个菜单界面背景变得相同的解决方法是:根据当前对象的类型来区分它们是哪个菜单界面,从而区别使用背景图。在继续之前先将MND_SceneMenuEx1.js文件中的内容清空以免影响后面的代码。
要判断当前运行的是哪个菜单界面,可以使用 instanceof
或者 constructor
属性来判断当前实例的类型,根据判断结果选择要使用的背景,详细的Scene_MenuBase.prototype.createBackground
重写代码如下:
Scene_MenuBase.prototype.createBackground = function() {
this._backgroundSprite=new Sprite();
var imageName;
if(this instanceof Scene_Menu){
imageName="Mountains1";
}else if(this instanceof Scene_Item){
imageName="BlueSky"
}else if(this instanceof Scene_Skill){
imageName="Mountains2"
}else if(this instanceof Scene_Equip){
imageName="Ocean2"
}else if(this instanceof Scene_Save || this instanceof Scene_Load) {
imageName = "Sunset"
}else{
imageName="Mountains4"
}
this._backgroundSprite.bitmap=ImageManager.loadParallax(imageName);
this.addChild(this._backgroundSprite);
};
if(this instanceof Scene_Menu)
代码部分表示如果当前是主菜单(Scene_Menu
)界面,则使用 Mountains1 图片作为背景;同理,对于Scene_Item
、Scene_Skill
、Scene_Equip
的菜单界面也分别使用各自不同的背景图片;而Scene_Save
和Scene_Load
使用同一个背景 Sunset.png;剩下的所有其它菜单界面都使用同一个背景 Mountains4.png。当然,你可以按照类似的方式随意修改各个菜单界面的背景图片。
让背景滚动起来
前文是为各个菜单界面添加了背景图片,但都是静态的。如果要让这些背景进行循环滚动,可以使用 TilingSprite
平铺精灵类代替 Sprite
精灵类。TilingSprite
这个类可以通过简单的代码就让一个背景循环滚动,只需要在 update
方法中不断更新平铺精灵滚动的原点 origin
即可。所以前面的代码可以改为如下:
Scene_MenuBase.prototype.createBackground = function() {
this._backgroundSprite=new TilingSprite();
var imageName;
if(this instanceof Scene_Menu){
imageName="Mountains1";
}else if(this instanceof Scene_Item){
imageName="BlueSky"
}else if(this instanceof Scene_Skill){
imageName="Mountains2"
}else if(this instanceof Scene_Equip){
imageName="Ocean2"
}else if(this instanceof Scene_Save || this instanceof Scene_Load) {
imageName = "Sunset"
}else{
imageName="Mountains4"
}
this._backgroundSprite.bitmap=ImageManager.loadParallax(imageName);
this._backgroundSprite.move(0,0, Graphics.width, Graphics.height);
this.addChild(this._backgroundSprite);
};
var _Scene_MenuBase_update = Scene_MenuBase.prototype.update;
Scene_MenuBase.prototype.update = function () {
_Scene_MenuBase_update.call(this);
this._backgroundSprite.origin.x+=1;
}
this._backgroundSprite.move(0,0, Graphics.width, Graphics.height);
用于同时设置背景精灵的坐标和宽高。下面重写了Scene_MenuBase.prototype.update
方法,在其中更不断更新this._backgroundSprite.origin
以使平铺精灵出现循环滚动的效果。
这种方式也可以用于地图界面中的远景滚动,虽然地图已经有一个“远景”功能可以实现远景滚动,不过,这种方式可以设置滚动区域、大小、位置,甚至多个滚动效果出现在同一张地图上,所以可以有更多自由、更多订制。比如主角在一条东西大道上一路狂奔时,远处的山脉(远景)会缓慢的滚动,近处或路旁的树木(近景)会较快的滚动。
最后,在 这里 我提供了一个最终制作完成的用于订制所有菜单背景的插件:MND_MenuBackground,可用于设置包括主菜单、物品菜单、技能菜单、装备菜单、状态菜单、选项菜单、保存/加载菜单、商店菜单、结束游戏菜、改名菜单等菜单背景,支持背景循环滚动。
在主菜单界面增加自定义菜单:改名
默认的主菜单界面上会显示 物品、技能、装备、状态、整队、设置、保存、游戏结束 等数个菜单命令。这几个菜单命令的显示归
Window_MenuCommand
类管理,要添加新的菜单只需要重写 Window_MenuCommand.prototype.addOriginalCommands
方法,并使用addCommand(name, symbol, enabled, ext)
方法新增一个菜单命令即可,代码如下:
Window_MenuCommand.prototype.addOriginalCommands = function () {
this.addCommand("改名", "rename", true);
};
这个操作会在 整队 菜单命令下添加一个 改名 的新菜单命令。addCommand
这个方法有四个参数,name
是菜单显示的名称,symbol
需要指定一个唯一的标识符来代表菜单,这个标识符不能与其它菜单的标识符相同,所以不能使用以下标识符:item, skill, equip, status, formation, options, save, gameEnd, cancel,因为这些标识符已经被那几个默认的菜单命令使用了。enabled
表示是否要启用该菜单,如果设置为false
,则菜单会呈现灰色的禁用状态;至于ext
参数可以不管它,因为我也不知道干嘛的(看起来像保存扩展数据用的)。
目前为止,这个 改名 菜单并没有绑定事件,如果现在测试,点击该菜单命令是不会有任何效果的。要将菜单命令绑定到事件,这时就要用到Scene_Menu
类(主菜单界面对应于Scene_Menu
类),所有事件处理会在该类中处理,在这里就是重写Scene_Menu.prototype.createCommandWindow
方法,查看该方法的原始实现,就会明白,所有默认菜单都是在这个方法中绑定事件的,所以新增的菜单也在这里绑定事件。在这里,我们要实现点击 改名 菜单后,进入用户选择状态(像物品、技能、装备、状态等主要菜单命令一样),玩家选择要改名的角色,自动进入改名界面修改所选角色的名称。先看看实现效果:
具体实现代码如下:
Window_MenuCommand.prototype.addOriginalCommands = function () {
this.addCommand("改名", "rename", true);
};
var _Scene_Menu_createCommandWindow = Scene_Menu.prototype.createCommandWindow;
Scene_Menu.prototype.createCommandWindow = function () {
_Scene_Menu_createCommandWindow.call(this);
this._commandWindow.setHandler('rename', this.commandRename.bind(this));
};
Scene_Menu.prototype.commandRename = function () {
this._statusWindow.setFormationMode(false);
this._statusWindow.selectLast();
this._statusWindow.activate();
this._statusWindow.setHandler('ok', this.rename_ok.bind(this));
this._statusWindow.setHandler('cancel', this.rename_cancel.bind(this));
};
Scene_Menu.prototype.rename_ok = function() {
SceneManager.push(Scene_Name);
SceneManager.prepareNextScene($gameParty.menuActor()._actorId, 10);
};
Scene_Menu.prototype.rename_cancel = function() {
this._statusWindow.deselect();
this._commandWindow.activate();
};
Window_MenuStatus.prototype.processOk = function() {
$gameParty.setMenuActor($gameParty.members()[this.index()]);
Window_Selectable.prototype.processOk.call(this);
};
首先是重写Scene_Menu.prototype.createCommandWindow
方法,这个方法用于创建左上部的菜单命令窗口。在这个方法中使用this._commandWindow.setHandler('rename', this.commandRename.bind(this));
的方式将 改名 菜单与Scene_Menu.prototype.commandRename
方法绑定。commandRename
方法的实现可以参考Scene_Menu.prototype.commandPersonal
方法的原始实现。在rename_ok
方法中会使用prepareNextScene
方法去向Scene_Name
传递要改名的角色ID。
(PS:这里,所谓的窗口是指在菜单界面看到的一个个由白边框围起来的那一块块区域。在主菜单界面,默认有三个窗口,左上部一个窗口用于显示菜单命令,对应于Window_MenuCommand
类;左下角一个小窗口用于显示金钱数量,对应于Window_Gold
类;右侧占用一大半区域的窗口,显示角色状态,对应于Window_MenuStatus
类,所以,如果想改动这些窗口,重写它们对应的类的方法是一种方式。)
在commandRename
方法中,再使用setHandler
将“确定选择角色”和“取消选择角色”二个操作与rename_ok
和rename_cancel
方法绑定。这里的绑定标识符 ok 和 cancel 是固定的,不能改为其它的,要不然,不能代表“确定选择”和“取消选择”二种操作结果。
最后,这里重点是重写Window_MenuStatus.prototype.processOk
方法。先来看该方法的原始实现:
Window_MenuStatus.prototype.processOk = function() {
Window_Selectable.prototype.processOk.call(this);
$gameParty.setMenuActor($gameParty.members()[this.index()]);
};
可以看到,原始实现和我们这里的重写方法只是在二行代码在执行顺序上进行了换位。为什么要这样重写这个方法呢?首先,这个方法是在我们确定选择改名的角色时触发,原始实现中会先去执行我们自定义的方法rename_ok
(因为Window_Selectable.prototype.processOk
方法中会调用this.callOkHandler()
,也就是这里的rename_ok
方法的代理进行执行),然后才去更新$gameParty
中的 menuActor。所以,如果不重写,我们在rename_ok
方法中使用$gameParty.menuActor()
获取到的仍然是上一次选中的角色,而不是这一次选中的角色。重写之后会先去设置$gameParty
的 menuActor,然后再执行我们的自定义方法rename_ok
,因为,像这里一样,自定义方法中可能需要知道当前选中的是哪个角色,所以这样修改显然比原始实现更合理。
当然,有人会问类似的像 技能(对应场景类Scene_Skill), 装备(对应场景类Scene_Equip), 状态(对应场景类Scene_Status) 这三个命令又为什么没出现这个问题呢?这三个菜单命令的实现方式其实与我们的 改名 菜单的实现方式并不一样,它们并不需要在自定义命令中向对应的场景类传递参数,而是直接在相关的场景类中使用$gameParty.menuActor()
来获得的,在从菜单界面切换到这些相应的场景时,不论是processOk
还是我们的自定义的rename_ok
方法都已经执行过了,况且它们并不需要接收参数,所以它们的执行顺序此时并不重要。根据这个原理,我们就有另外一种改法,就是像 Scene_Skill
、Scene_Equip
、Scene_Status
类的处理方式一样,重写Scene_Name
,具体就是重写Scene_Name.prototype.create
,代码如下:
Scene_Name.prototype.create = function() {
Scene_MenuBase.prototype.create.call(this);
//this._actor = $gameActors.actor(this._actorId);//修改为下面的:
this._actor = $gameParty.menuActor();
this.createEditWindow();
this.createInputWindow();
};
这个修改很简单,改成直接从$gameParty.menuActor()
获得 menuActor,而忽略传递进来的参数 actorId,运行测试,也是没问题的。不过,因为 Scene_Name
还要考虑在其它地方的使用(如事件编辑器中使用 名字输入处理... 命令),这么一改会影响到这些地方的使用,Scene_Name
最重要的还是要通过传递参数来改变指定的角色名称,所以这样改就存在兼容性问题,是不妥的。不过,假如你要将 改名 菜单(或其它自定义菜单)绑定到一个自定义的场景类而非 Scene_Name
场景,那么就可以考虑使用这种方式。
最后,在 这里 我提供了一个最终完成的改名插件:MND_Rename,除了可以在主菜单界面增加 改名 菜单外,还可以用于绑定一个物品,让物品拥有改名功能,这样,可用来制作 改名卡 之类的改名道具。
在主菜单界面移除菜单命令
如果要去除主菜单界面中的菜单命令,只需要重写Window_MenuCommand.prototype.makeCommandList
和Window_MenuCommand.prototype.addMainCommands
,查看makeCommandList
的原始实现,可以看到,这个方法用于向菜单命令窗口添加各个菜单命令,不想添加的,重写时删除不要的菜单即可。
由于在makeCommandList
方法中,四个主要菜单命令(物品、技能、装备、状态)只能要么一起删除要么一起添加,所以如果只想去掉其中的一个或几个,就需要重写 addMainCommands
方法,在该方法中删除不要出现的菜单命令即可。
Window_MenuCommand.prototype.makeCommandList = function() {
this.addMainCommands(); //增加主菜单:物品、技能、装备、状态
this.addFormationCommand();//增加 整队 菜单
this.addOriginalCommands();//增加自定义的菜单
this.addOptionsCommand();//增加 设置 菜单
this.addSaveCommand();//增加 保存 菜单
this.addGameEndCommand();//增加 游戏结束 菜单
};
Window_MenuCommand.prototype.addMainCommands = function() {
var enabled = this.areMainCommandsEnabled();
if (this.needsCommand('item')) {
this.addCommand(TextManager.item, 'item', enabled);//增加 物品 菜单
}
if (this.needsCommand('skill')) {
this.addCommand(TextManager.skill, 'skill', enabled);//增加 技能 菜单
}
if (this.needsCommand('equip')) {
this.addCommand(TextManager.equip, 'equip', enabled);//增加 装备 菜单
}
if (this.needsCommand('status')) {
this.addCommand(TextManager.status, 'status', enabled);//增加 状态 菜单
}
};
在主菜单界面增加一个自定义窗口
前面说了,在主菜单界面默认有三个窗口,左侧有一大一小2个,右侧1个大的。左侧有一块区域是空的,可以在这个区域增加一个新的自定义窗口,至于要在窗口里显示什么,这个看自己的需要,比如显示一个任务提示。
首先主菜单界面对应于Scene_Menu
类,在该类的原始实现中可以找到一个Scene_Menu.prototype.create
方法,可以看到,在主菜单界面的三个窗口都是在这里创建的。所以,我们只需要重写该方法,也在这里创建自定义窗口即可。
其次,三个默认窗口都有对应的类实现,如左上部的菜单命令窗口对应于Window_MenuCommand
类,左下角金钱窗口,对应于Window_Gold
类;右侧角色状态窗口,对应于Window_MenuStatus
类,所以我们要新增的自定义窗口也要创建一个类来实现,实现方法可以参考前面这三个类,根据它们的实现稍微改改就可以了。假如,我们的实现类是Window_Tips
,具体代码如下:
var _Scene_Menu_create = Scene_Menu.prototype.create;
Scene_Menu.prototype.create = function() {
_Scene_Menu_create.call(this);
//创建自定义窗口,并将它加入主菜单界面
this._tipsWindow = new Window_Tips(0, 0);
this._tipsWindow.y = this._commandWindow.y + this._commandWindow.height + 5; //设置自定义窗口的Y坐标,由左上部的菜单命令窗口的Y轴坐标及其高度来决定
this.addWindow(this._tipsWindow);
};
function Window_Tips() {
this.initialize.apply(this, arguments);
}
Window_Tips.prototype = Object.create(Window_Base.prototype);
Window_Tips.prototype.constructor = Window_Tips;
Window_Tips.prototype.initialize = function(x, y) {
var width = this.windowWidth();
var height = this.windowHeight();
Window_Base.prototype.initialize.call(this, x, y, width, height);
this.refresh();
};
Window_Tips.prototype.windowWidth = function() {
return 240; //自定义窗口的宽度
};
Window_Tips.prototype.windowHeight = function() {
return this.fittingHeight(4); //自定义窗口的高度:通过设定窗口要容纳的行数来自动计算高度
};
Window_Tips.prototype.refresh = function() {
var x = this.textPadding();
var width = this.contents.width - this.textPadding() * 2;
this.contents.clear();
//在这里绘制需要显示的内容
this.drawIcon(191, 0, 0);
this.drawTextEx("新的任务", 40, 0);
this.drawTextEx("找到老王头,拿\n到红色宝箱。", 0, 40);
};
Window_Tips.prototype.open = function() {
this.refresh();
Window_Base.prototype.open.call(this);
};
在Scene_Menu.prototype.create
方法中创建Window_Tips
窗口,并设置其坐标,使之呈现在左侧空白区内。对于Window_Tips
类,用Window_Tips.prototype.windowWidth
设置自定义窗口的宽度,用Window_Tips.prototype.windowHeight
方法设置其高度;而Window_Tips.prototype.refresh
方法用于绘制内容的地方,想要在这个自定义窗口显示什么内容,可以在这个方法中绘制。这里用drawIcon
方法绘制了一个图标,用drawTextEx
方法绘制文本,文本中可以用 \n
来回车换行。效果如下:
by Mandarava(鳗驼螺) 2017.06.06