【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的子类,本文不涉及它,以后再写文章单独研究一下。

本文涉及的内容包括:

  1. 给各个菜单界面添加背景
  2. 让背景滚动起来
  3. 在主菜单界面增加自定义菜单:改名
  4. 在主菜单界面移除菜单命令
  5. 在主菜单界面增加一个自定义窗口

创建一个名为 MND_SceneMenuEx1.js 的JavaScript文件,保存到 js/plugins 目录下,在RMMV的插件管理中安装该插件。

给菜单界面添加背景

主菜单界面对应Scene_Menu类,其父类是Scene_MenuBase,分析Scene_MenuBase类源码,可以在其中找到一个名为Scene_MenuBase.prototype.createBackground 的方法,显然,这个方法是用来创建菜单界面背景的。
  除非想要将所有菜单界面(如物品菜单、装备菜单等界面)的背景都设置为相同,否则不要直接重写Scene_MenuBase类的该方法,因为Scene_ItemScene_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)将创建的精灵加入当前场景。运行测试,效果如下图:

Screenshot1.png

前面说了,菜单界面有十几个,如果每个菜单界面都要改为单独的背景,或者某几个菜单界面想要共用一个背景,那是否一定就得为这十几个菜单类重写十几个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_ItemScene_SkillScene_Equip的菜单界面也分别使用各自不同的背景图片;而Scene_SaveScene_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方法,查看该方法的原始实现,就会明白,所有默认菜单都是在这个方法中绑定事件的,所以新增的菜单也在这里绑定事件。在这里,我们要实现点击 改名 菜单后,进入用户选择状态(像物品、技能、装备、状态等主要菜单命令一样),玩家选择要改名的角色,自动进入改名界面修改所选角色的名称。先看看实现效果:

Rename

具体实现代码如下:

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_okrename_cancel方法绑定。这里的绑定标识符 okcancel 是固定的,不能改为其它的,要不然,不能代表“确定选择”和“取消选择”二种操作结果。
  最后,这里重点是重写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()获取到的仍然是上一次选中的角色,而不是这一次选中的角色。重写之后会先去设置$gamePartymenuActor,然后再执行我们的自定义方法rename_ok,因为,像这里一样,自定义方法中可能需要知道当前选中的是哪个角色,所以这样修改显然比原始实现更合理。
  当然,有人会问类似的像 技能(对应场景类Scene_Skill), 装备(对应场景类Scene_Equip), 状态(对应场景类Scene_Status) 这三个命令又为什么没出现这个问题呢?这三个菜单命令的实现方式其实与我们的 改名 菜单的实现方式并不一样,它们并不需要在自定义命令中向对应的场景类传递参数,而是直接在相关的场景类中使用$gameParty.menuActor()来获得的,在从菜单界面切换到这些相应的场景时,不论是processOk还是我们的自定义的rename_ok方法都已经执行过了,况且它们并不需要接收参数,所以它们的执行顺序此时并不重要。根据这个原理,我们就有另外一种改法,就是像 Scene_SkillScene_EquipScene_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.makeCommandListWindow_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 来回车换行。效果如下:

Window_Tips

by Mandarava(鳗驼螺) 2017.06.06

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容