- 作者:Mandarava(鳗驼螺)
- 微博:@鳗驼螺pro
标题画面代表了游戏的脸面,必须好看。标题画面对应的类是Scene_Title
,所以对于标题画面的修改可以通过重写Scene_Title
中的相关方法来实现。
本文涉及的内容包括:
- 美化游戏标题
- 让背景动起来
- 自定义标题菜单
- 美化菜单
创建一个名为 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兼游戏标题(随便网络找的一个素材,勿商用)。
让背景动起来
背景部分,如果只是想改成其它静态图,那么在“数据库”中更改就足够了,没必要自己重写。当然,如果你想,或者如果想实现动态背景,那么就需要修改Scene_Title.prototype.createBackground
方法了。
createBackground
方法用于创建标题画面的背景。在这个方法的重写版本中,我们先创建一个用于显示背景的精灵,加载动态背景所需要的帧序列图片,然后在Scene_Title.prototype.update
方法中按顺序用帧序列图片循环更新精灵的显示图片,从而实现背景的动态效果。先来看一下实现效果:
这个动态效果由三张图片组成(直接拿MV中的官方的标题图片 Devil.png 改的,旋转了一下怪物的二只爪子,做成了动画序列)。资源图片放到
img/mndtitle
目录下,名称按动画顺序依次为:
TitleBack1.png
,
TitleBack2.png
,
TitleBack3.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._backSprite1
和this.__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.makeCommandList
和Scene_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, ["http://www.jianshu.com/nb/13204998"]);
};
这样,在游戏时点击标题画面的“官方网站”菜单后会打开本人在写的关于RMMV的文章专题首页。这里,this._commandWindow.activate();
是重新激活当前游戏窗口,因为使用其它进程打开网页时会造成游戏窗口失焦。
如果要删除菜单,更简单,只需要重写Window_TitleCommand.prototype.makeCommandList
方法,删除不想看到的菜单即可,当然不要去call原始方法。
美化菜单
默认的菜单就是白边框围成的几个菜单文本,与目前的画面不太搭,所以现在的目标是用美化的图片来代替文本做成菜单。先看看完成效果:
四个菜单全部由图片做成,实际上它们是图片按钮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原始的方式不太一样。原始方式是:点击一个菜单时,如果该菜单不是当前选中的菜单,则只是选中它(在菜单上显示一个白色透明背景的方框,以呈高亮显示),让它成为当前选中的菜单;如果是当前选中的菜单,则直接进入该菜单功能。我们的菜单点击任何一个按钮,不论指示器在哪里,都会直接进入菜单功能。
- 如果要实现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();} }
];
这种方式其实还不是很好,对于一个非当前选中的菜单,需要双击才能进入菜单功能,个人认为更好的方式是下面这种:
- 点击任何一个菜单(不论它是否为当前选中的菜单),指示器都会指向它,并直接进入菜单的功能,要实现这种方式,更简单:
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