【RPG Maker MV插件编程】【实例教程4】玩转标题画面

  • 作者:Mandarava(鳗驼螺)
  • 微博:@鳗驼螺pro

标题画面代表了游戏的脸面,必须好看。标题画面对应的类是Scene_Title,所以对于标题画面的修改可以通过重写Scene_Title中的相关方法来实现。

本文涉及的内容包括:

  1. 美化游戏标题
  2. 让背景动起来
  3. 自定义标题菜单
  4. 美化菜单

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

美化游戏标题

Scene_Title类中可以找到Scene_Title.prototype.createForeground方法,它用于创建标题画面的前景。在原始实现中,这个“前景”就是标题文字(简单的给文字加了描边效果)。我们可以在【数据库 - 系统 - 标题画面】选项中设置是否绘制标题名称。
  这里,我们重新实现createForeground方法,使用一个游戏logo图片来代替标题文字,相对于纯文字来说,图片可以包含图文效果,更高大上。实现代码:

Scene_Title.prototype.createForeground = function() {
    if ($dataSystem.optDrawTitle) {
        var gameLogo = ImageManager.loadBitmap("img/mndtitle/", "GameLogo");
        this._gameTitleSprite = new Sprite(gameLogo);
        this._gameTitleSprite.anchor = new Point(0.5, 0);
        this._gameTitleSprite.x = Graphics.width / 2;
        this._gameTitleSprite.y = 50;
        this.addChild(this._gameTitleSprite);
    }
};

这段代码将在画面显示一个Logo图片。我们的logo图片放到img/mndtitle目录下,名称为GameLogo.png,这张图片的内容是“武林外史”几个特效文字。if ($dataSystem.optDrawTitle)用于检测是否要绘制标题(这个就是前面说的在数据库中可以配置的选项)。因为mndtitle是个自定义目录,所以从该目录加载图片时要使用ImageManager.loadBitmap方法,参数指定文件夹位置和加载的图片名称。将这个图片放置在画面中间靠上的位置,如果你要放到正中间,可以使用this.centerSprite()方法。再一次提醒,如果你要用到图片的宽高尺寸来给图片设置坐标,那么设置方法应该放到Scene_Title.prototype.start中去做。最后效果如下。背景是MV自带素材,不是这里的重点;顶部的“武林外史”就是要展示的游戏logo兼游戏标题(随便网络找的一个素材,勿商用)。

Screenshot1.png

让背景动起来

背景部分,如果只是想改成其它静态图,那么在“数据库”中更改就足够了,没必要自己重写。当然,如果你想,或者如果想实现动态背景,那么就需要修改Scene_Title.prototype.createBackground方法了。
  createBackground方法用于创建标题画面的背景。在这个方法的重写版本中,我们先创建一个用于显示背景的精灵,加载动态背景所需要的帧序列图片,然后在Scene_Title.prototype.update方法中按顺序用帧序列图片循环更新精灵的显示图片,从而实现背景的动态效果。先来看一下实现效果:

Title.gif

  这个动态效果由三张图片组成(直接拿MV中的官方的标题图片 Devil.png 改的,旋转了一下怪物的二只爪子,做成了动画序列)。资源图片放到 img/mndtitle 目录下,名称按动画顺序依次为:TitleBack1.pngTitleBack2.pngTitleBack3.png
  下面是Scene_Title.prototype.createBackground的实现代码:

Scene_Title.prototype.createBackground = function() {
    this._animFrameImgs=[
        ImageManager.loadBitmap("img/mndtitle/", "TitleBack1"),
        ImageManager.loadBitmap("img/mndtitle/", "TitleBack2"),
        ImageManager.loadBitmap("img/mndtitle/", "TitleBack3")
    ];
    this._animFrames=[0,1,2,1];
    this._currFrame=0;
    this._animDelay=0.2;
    this._backSprite = new Sprite(this._animFrameImgs[0]);
    this.centerSprite(this._backSprite)
    this.addChild(this._backSprite);
};

首先依次加载三张背景图片存放到this._animFrameImgs中,this._animFrames是一个索引数组,表示一个动画序列,每个元素代表了在this._animFrameImgs数组中的图片索引,this._currFrame是动画序列中的当前帧,this._animDelay表示动画的二帧之间要求的时间间隔,this._backSprite是显示背景图片的精灵。
  因为createBackground方法的原始实现中创建了this._backSprite1this.__backSprite2,且在start方法中使用了这二个精灵,但我们现在重写的createBackground没有创建它们也没有去call原始方法,所以还要先重写Scene_Title.prototype.start方法,删除与_backSprite1_backSprite2有关的代码,如下:

Scene_Title.prototype.start = function() {
    Scene_Base.prototype.start.call(this);
    SceneManager.clearStack();
    //this.centerSprite(this._backSprite1);//删除
    //this.centerSprite(this._backSprite2);//删除
    this.playTitleMusic();
    this.startFadeIn(this.fadeSpeed(), false);
};

接下来是Scene_Title.prototype.update方法,在其中每隔this._animDelay的时间就按动画序列中的索引更新到下一张背景图片,代码如下。

var _Scene_Title_update = Scene_Title.prototype.update;
Scene_Title.prototype.update = function() {
    _Scene_Title_update.call(this);

    this._elapsedSinceLastUpdate = this._elapsedSinceLastUpdate || 0;
    if (this._elapsedSinceLastUpdate >= this._animDelay) {
        this._currFrame++;
        this._currFrame = this._currFrame % this._animFrames.length;
        var animFrameIndex = this._animFrames[this._currFrame];
        this._backSprite.bitmap = this._animFrameImgs[animFrameIndex];
        this._elapsedSinceLastUpdate = 0;
    }
    this._elapsedSinceLastUpdate += 1 / Graphics._fpsMeter.fps;
};

可以通过Graphics._fpsMeter.fps获取游戏当前的FPS,这里用1 / Graphics._fpsMeter.fps的方式粗略的表示二个update之间的时间间隔。this._elapsedSinceLastUpdate表示自上次更新背景图片以来流逝的游戏时间,在每次update都会累计一次,直到当流逝时间超过this._animDelay后,就开始绘制新一帧的图片,从而不断循环,实现动态背景的不断往复循环。
  当然,这里只是演示一个简单的动画效果,更多关于动画的技巧可以参考另一篇教程:【实例教程5】制作小游戏:坦克大战(上)

自定义标题菜单

默认标题画面只显示 开始游戏、继续游戏、设置 三个菜单命令,如果要增加其它的菜单,如:官方网站、致谢 等菜单,那么需要重写二个方法:Window_TitleCommand.prototype.makeCommandListScene_Title.prototype.createCommandWindow。前者用于在菜单画面创建菜单命令,后者用于将事件绑定到菜单。如果你读过之前的一篇教程:玩转菜单初级篇,应该就会猜到 Window_TitleCommand 对应的是标题界面中间偏下那个由白边框围成的菜单区域,菜单的显示由它创建,而事件处理由它所在的场景,也就是Scene_Title来绑定和处理。菜单绑定事件用setHandler,相关的实现也可以参考“玩转菜单初级篇”一文。最后实现代码如下:

var _Window_TitleCommand_makeCommandList = Window_TitleCommand.prototype.makeCommandList;
Window_TitleCommand.prototype.makeCommandList = function () {
    _Window_TitleCommand_makeCommandList.call(this);

    this.addCommand("官方网站", 'homepage');//增加一个新菜单,标识符为 homepage
};

var _Scene_Title_createCommandWindow = Scene_Title.prototype.createCommandWindow;
Scene_Title.prototype.createCommandWindow = function() {
    _Scene_Title_createCommandWindow.call(this);

    this._commandWindow.setHandler('homepage', this.commandHomepage.bind(this)); //将标识符为homepage的菜单绑定到commandHomepage方法
};

Scene_Title.prototype.commandHomepage = function() {
    this._commandWindow.activate();
    //打开url
    var cmd;
    if (process.platform === 'darwin') cmd = 'open';
    if (process.platform === 'win32') cmd = 'explorer.exe';
    if (process.platform === 'linux') cmd = 'xdg-open';
    var spawn = require('child_process').spawn;
    spawn(cmd, ["//www.greatytc.com/nb/13204998"]);
};

这样,在游戏时点击标题画面的“官方网站”菜单后会打开本人在简书写的关于RMMV的文章专题首页。这里,this._commandWindow.activate();是重新激活当前游戏窗口,因为使用其它进程打开网页时会造成游戏窗口失焦。
  如果要删除菜单,更简单,只需要重写Window_TitleCommand.prototype.makeCommandList方法,删除不想看到的菜单即可,当然不要去call原始方法。

美化菜单

默认的菜单就是白边框围成的几个菜单文本,与目前的画面不太搭,所以现在的目标是用美化的图片来代替文本做成菜单。先看看完成效果:


Final

四个菜单全部由图片做成,实际上它们是图片按钮Sprite_Button对象,本质上也是Sprite精灵,但能绑定方法能回应点击。像原始菜单一样,这些图片菜单可以用鼠标点击,也可以用方向键上下移动切换菜单,在当前选中的菜单前面会显示一个指示标记。
  要实现这个效果,首先要重写Scene_Title.prototype.create方法,这个方法用于初始化图片菜单的相关资源。实现代码:

var _Scene_Title_create = Scene_Title.prototype.create;
Scene_Title.prototype.create = function () {
    _Scene_Title_create.call(this);
    this._commandWindow.visible = false;//不显示原始的文本菜单
    this._commandWindow.x=Graphics.width;//移到画面外去,否则虽然不显示仍能点击
    var btnimgs=["CmdStartGame", "CmdContinueGame", "CmdOptions", "CmdHomepage"];
    var clicks=[
        function(){this.commandNewGame(); SoundManager.playOk();},
        function(){this.commandContinue(); SoundManager.playOk();},
        function(){this.commandOptions(); SoundManager.playOk();},
        function(){this.commandHomepage(); SoundManager.playOk();}
    ];
    this._cmdButtons=[];//所有图片菜单
    for(var i in btnimgs){
        var sprite=new Sprite_Button();
        sprite.width=184;
        sprite.height=53;
        sprite.bitmap=ImageManager.loadBitmap("img/mndtitle/", btnimgs[i]);
        //sprite.anchor=new Point(0.5,0.5);//不要设置,设置这个会出现菜单点不中的问题,不清楚原因。
        sprite.x=Graphics.width/2-92;
        sprite.y=360+60*i;
        sprite.setClickHandler(clicks[i].bind(this));
        this._cmdButtons.push(sprite);
        this.addChild(sprite);
    }
    this._cmdSelect=new Sprite(ImageManager.loadBitmap("img/mndtitle/", "CmdSelect"));//选中菜单的指示器
    this._cmdSelect.anchor=new Point(1,0);//因为按钮的anchor是默认的(0,0),这个指示器要放在按钮左侧,所以让它的anchor为(1,0)更容易定位
    this.addChild(this._cmdSelect);
};

this._commandWindow.visible用于将原始的文本菜单隐藏掉,并将它放到画面外部,因为这个菜单即使不显示也能点击到,也能用上下方向键切换选择其菜单,不过,我们又不能删除它。当然,你可以重写Scene_Title.prototype.createCommandWindow方法,并只删除原始代码中的this.addWindow(this._commandWindow);,从而真的不让它出现,但那样你就得自己实现一些方法,比如:在游戏启动时原始的文本菜单会根据是否有存档自动选择 继续游戏 还是 开始游戏,可以用上下方向键切换不同的菜单,可以用确定键打开选中的菜单,如果不让它出现,那么这些功能就得自己去为图片菜单实现。所以,为了简单起见,我们让图片菜单与原始的文本菜单保持联动,每个图片菜单与原始的文本菜单一一对应。
  btnimgs是各个图片菜单的图片名称(也就是 img/mndtitle文件夹下与菜单相关的图片的名称),要按照原始文本菜单的顺序排列,以便使图片菜单与原始的文本菜单顺序是一致的。
  clicks是保存了各个菜单按钮要绑定的方法的一个数组,这个数组中,也是按顺序定义好各个图片菜单对应要绑定的方法。所有图片菜单都是一个Sprite_Button对象,这里注意,不要忘记设置它们的宽高,这会影响点击的热区,否则可能点在菜单上却没有反应。使用sprite.setClickHandler(clicks[i].bind(this));将图片菜单与对应的方法进行绑定。
  this._cmdButtons是存放图片按钮的数组,在update方法会用到。this._cmdSelect是个指示器,会显示在当前选中的菜单左侧。
  接下来就是要让选择指示器的显示在选中的菜单左侧,这个需要在Scene_Title.prototype.update中处理,在update方法中添加以下代码到最后(前文中已经重写了该方法,现在再添加代码进去):

    var btnSelect = this._cmdButtons[this._commandWindow.index()];
    this._cmdSelect.x = btnSelect.x;
    this._cmdSelect.y = btnSelect.y;

this._commandWindow.index()是用来获取原始的文本菜单中当前所选中的菜单索引,根据这个索引我们用this._cmdButtons[index]来从图片菜单数组中得到对应的图片菜单对象,然后将指示器放到它左侧。

补充:优化菜单点选方式

菜单的功能基本完成,但在运行过程中,我们发现它的工作方式与RMMV原始的方式不太一样。原始方式是:点击一个菜单时,如果该菜单不是当前选中的菜单,则只是选中它(在菜单上显示一个白色透明背景的方框,以呈高亮显示),让它成为当前选中的菜单;如果是当前选中的菜单,则直接进入该菜单功能。我们的菜单点击任何一个按钮,不论指示器在哪里,都会直接进入菜单功能。

  1. 如果要实现RMMV自带的原始点选方式,可以将clicks换成如下定义:
var clicks=[
  function(){if(this._commandWindow.index()!=0){this._commandWindow.select(0);}else{this._commandWindow.processOk();} },
  function(){if(this._commandWindow.index()!=1){this._commandWindow.select(1);}else{this._commandWindow.processOk();} },
  function(){if(this._commandWindow.index()!=2){this._commandWindow.select(2);}else{this._commandWindow.processOk();} },
  function(){if(this._commandWindow.index()!=3){this._commandWindow.select(3);}else{this._commandWindow.processOk();} }        
];

这种方式其实还不是很好,对于一个非当前选中的菜单,需要双击才能进入菜单功能,个人认为更好的方式是下面这种:

  1. 点击任何一个菜单(不论它是否为当前选中的菜单),指示器都会指向它,并直接进入菜单的功能,要实现这种方式,更简单:
var clicks=[
  function(){ this._commandWindow.select(0); this._commandWindow.processOk(); },
  function(){ this._commandWindow.select(1); this._commandWindow.processOk(); },
  function(){ this._commandWindow.select(2); this._commandWindow.processOk(); },
  function(){ this._commandWindow.select(3); this._commandWindow.processOk(); }
]

目前的源码中推荐使用最后一种方式。

那么到这里,整个效果也已经完成了。本文涉及的资源、代码请到 这里 下载。

by Mandarava(鳗驼螺) 2017.06.21

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

推荐阅读更多精彩内容