【RPG Maker MV插件编程】【实例教程7】制作一个传送插件

作者:Mandarava(鳗驼螺)
微博:鳗驼螺Pro

这个传送插件可以用来制作传送道具或传送技能。当玩家使用传送道具或传送技能后,会弹出一个窗口显示可以传送到的地点列表,玩家选择地点后,角色可以瞬间转移到该地。制作出的道具可以是可消耗的物品,也可以是永久有效的物品。本文将完整呈现一个插件的构建过程,材料丰富、内容实用。成除了传送插件本身外,通过本文,你还将看到如何保存自定义数据到存档中(包括如何避免因版本升级造成的数据崩溃问题)、如何使用meta数据、如何在游戏界面中呈现自定义数据、如何使用数据库中定义动画等。

本文的主要内容包括:

  • 传送插件的主要功能
  • 将自定义数据保存到存档中
  • meta数据的使用
  • 使用地图备注登记传送点
  • 在插件中解析并记录传送点
  • 使用地图备注登记多个传送点并在插件中记录
  • 制作传送点选取窗口显示传送点数据
  • 将物品或技能标记为传送物品、传送技能
  • 显示传送动画实现传送功能
  • 禁止使用传送道具或传送技能
  • 实现插件命令

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

传送插件的主要功能

  1. 可以在地图备注中登记当前地图中的一个或多个传送点,当角色进入该地图后,自动获取传送点信息,将它(们)作为已知的传送点记录到角色的可传送点列表中。
  2. 也可以通过插件命令登记传送点。相比在地图备注中登记,虽然没那样简便,但这种方式可以确保在需要的时候(如到达指定区域或完成指定事件后)才登记和公开传送点。
  3. 允许启用或禁用传送点,允许隐藏或显示传送点。这样可以控制传送点的开关状态,比如,在某些事件完成前或完成后,某个传送点允许或不允许显示或使用等。
  4. 允许通过插件命令删除传送点。一般来说,这个命令没有必要使用,通常可以使用隐藏或禁用传送点命令代替。
  5. 允许通过地图备注或插件命令设置当前地图是否能够使用传送道具或传送技能。有些地图或进行某些任务时,你可能想禁止角色使用传送道具和传送技能。
  6. 传送点是随着游戏进展而不断登记到传送点列表中的,所以这个数据必须随游戏一起保存和恢复,本插件可以将传送点数据保存到存档中并随存档的加载而读取。
  7. 传送开始和结束时可以自定义一个动画作为传送效果。
  8. 战斗场景自动禁止使用传送道具和传送技能。

先来看一下完成后的效果。主角一开始在“世界之源”地图,获得传送道具后,首先进入左侧的“西之森”,该地图一开始禁止使用传送功能(即不能在该地图使用传送道具或传送技能),与精灵对话后可以打开该地图的传送功能;然后主角回到“世界之源”往上来到“北之墓”,该地图的传送点一开始是禁止传送状态(即不能从其它地方传送到“北之墓”),和精灵对话后可以开启“北之墓”的传送状态;最后,主角来到下方的“南之雪”,该地图中的传送点一开始并不显示在传送点选择窗口中,和精灵对话后就可以将它显示出来了。最后是传送演示,从“南之雪”传送回到“世界之源:起点”,传送开始和结束都可以显示一段自己设定的动画。演示项目源码在 这里 下载。

传送插件

将自定义数据保存到存档中

一般来说,传送点是随着游戏剧情展开,当角色到达具体地点或完成某些事件后,才会开放给角色供其选择传送。所以传送点通常是逐步登记的。那么这个数据就必须随同游戏一起保存和加载。要保存自定义的插件数据,最好的方式是直接保存到游戏存档中。

那么如果将自定义数据保存到存档中呢?【实例教程6】存档的加密解密与保护一文中,我们分析出一个方法DataManager.makeSaveContents,这个方法会将需要存入存档的数据(包括$gameSystem,$gameScreen,$gameTimer,$gameSwitches,$gameVariables,$gameSelfSwitches,$gameActors,$gameParty,$gameMap,$gamePlayer 等10个全局对象的数据)合并成一个对象contents,然后使用JsonEx.stringify方法将这个对象进行json序列化转换成json字符串,再经过Base64编码后保存到存档文件中。这里,所谓序列化,就是将对象的状态信息(如对像的属性及属性值)转换为可以存储或传输的形式(如json字符串),然后存入存档文件中;下次玩家加载游戏存档时,就会进行反向操作,也就是所谓的反序列化,从序列化的表示形式(json字符串)中提取数据,并直接设置对象状态,还原数据。所以,要想将自定义数据保存到存档中,只要将数据以属性的方式添加给上面那10个全局对象即可。当然,你也可以添加给contents对象,只是这种情况下,你需要在DataManager.extractSaveContents方法中在反序列化时手动绑定回数据。

有10个全局对象可以去扩展,这个数据那到底要添加给谁呢?一般可以根据数据的用途选择。在本插件中,传送点与地图有关,所以咱就直接扩展给$gameMap对象。那么这个添加的方式也很简单,直接重写$gameMap对象对应的类Game_Map类的initialize方法,在这里初始化新的对象属性,因为我们需要一个变量来保存所有传送点信息,所以只需要像这样定义一个数组即可:this._mndTeleportPlaces=[]。在LEARN_Teleport.js中添加以下实现代码:

var _Game_Map_initialize = Game_Map.prototype.initialize;
Game_Map.prototype.initialize = function() {
  _Game_Map_initialize.call(this);
  this._mndTeleportPlaces=[];
};

就这么简单,我们的自定义数据_mndTeleportPlaces就可以作为存档数据保存到存档中了,而且,我们不需要关心读档的问题,因为存档数据反序列化时,这些属性和数据会自动绑定。在使用时,只需要使用代码$gameMap._mndTeleportPlaces就能访问到这个变量了。当然,这里有一个坑,具体会在后面用到时再作说明。

下表是那10个全局对象及其对应类型的表格,如果要将数据添加给其它对象,那么参考这里重写相应类的initialize方法即可。注意:表中并不包括 $gameTemp, $gameMessage, $gameTroop 这三个全局对象,因为它们的数据不会被保存到存档中。

全局对象 类型
$gameSystem Game_System
$gameScreen Game_Screen
$gameTimer Game_Timer
$gameSwitches Game_Switches
$gameVariables Game_Variables
$gameSelfSwitches Game_SelfSwitches
$gameActors Game_Actors
$gameParty Game_Party
$gameMap Game_Map
$gamePlayer Game_Player

meta数据的使用

meta(元数据)是地图、角色、技能、物品等对象的一个属性,meta数据是以一定的格式定义在地图、角色、技能、物品等对象的备注中的内容,这些内容会被MV解析成meta的属性和数据(你可以在DataManager.extractMetadata中看到这个解析过程)。meta数据的格式如下:<key:value>,比如:<teleportName:世界之源>可以用来表示当前地图作为传送点时的名称。在使用时,可以直接像调用meta对象的属性一样调用它们,如$dataMap.meta.teleportName

我们先来做个测试,新建一个地图(内容任意),将玩家初始化到该地图。右键单击该地图,点击“编辑”,在地图备注中写入以下内容:

<teleport>
<teleportId:8>
<teleportName:世界之源>
<teleportXY:6,8>
Note

保存并启动游戏,确保角色在该地图,按F8打开开发者工具,转到Console控制台(默认),输入$dataMap并回车,显示该对象的数据。如下图:

$dataMap

$dataMap代表当前地图的数据,这里可以看到其meta属性值下已经包含了teleport, teleportId, teleportName, teleportXY等多个属性,除了teleport属性是布尔类型外,其它的都是字符串类型,你可以根据需要将字符串转换成你要的类型,比如这里的teleportId就可以用Number($dataMap.meta.teleportId)来转换成数值类型,而对于teleportXY而言它代表一个坐标点,我们需要自定义解析方式来将它转换成一个Point对象。你可能注意到meta下方的note属性,这个属性的数据呈现了原版的备注内容,对于<teleport>的meta数据,你也可以用$dataMap.note.contains('<teleport>')来判断本地图的传送功能是否开启,而不需要访问其meta属性$dataMap.meta.teleport,因为它可能不存在(当你没有在备注中写入时,它就是undefined,虽然不存在也代表false)。

使用地图备注登记传送点

知道了meta数据怎么使用,现在就可以利用地图的meta数据来登记传送点。为了简单方便,同时支持一个地图登记多个传送点,我们重先设计一下传送点登记格式,将同一个传送的数据都放在一个字符串里,用空格分隔传送点的各项数据。先看单个传送点登记格式:

<teleport: x y [enabled] [visible] [name]>

举个栗子:

<teleport: 11 3 0 1 世界之源:起点>

这个代码表示

  • 传送点坐标(x y):传送点坐标在地图的(行列)(11,3)处,即在传送时,角色最终将传送到该位置;
  • 启用状态(enabled):当前值为0,即false,表示不启用,即该传送点默认是不可用状态,无法传送到该地点;该参数为可选参数,不提供时默认为启用。
  • 可见状态(visible):当前值为1,即true,表示该传送点将显示在供玩家选择传送地点的传送点选择窗口中,但因为未启用,玩家只能看到并不能选择传送过去;该参数为可选参数,不提供时默认为可见。
  • 传送点名称(name):在传送点选择窗口中显示的传送点名称,当前为“世界之源:起点”;该参数为可选参数,不提供时默认使用当前地图的名称。

在插件中解析并记录传送点

前面已经在地图备注中登记了传送点信息,现在就可以在插件中将这些传送点记录到我们的传送点列表中,也就是保存到前面定义好的$gameMap._mndTeleportPlaces数组中去。我们只当角色进入地图时才会去记录该地图中的传送点信息,这比较经济、合理。当然,如果你要一开始就把所有传送点登记好,可以使用插件命令来登记,后面实现。
  角色在进入地图时,MV会对该地图进行安装配置,使用当前的地图数据重新初始化$dataMap$gameMap,使这二个对象都指向当前的游戏地图及其数据。安装配置工作由Game_Map.prototype.setup方法来完成,所以我们也只需要重写该方法,实现读取地图的meta数据,再将meta数据解析成传送点信息保存到传送点列表变量_mndTeleportPlaces中即可。在LEARN_Teleport.js中添加以下实现代码:

var _Game_Map_setup = Game_Map.prototype.setup;
Game_Map.prototype.setup = function(mapId) {
    _Game_Map_setup.call(this, mapId);

    if ($dataMap.note.contains("<teleport:")) {//判断备注中是否包含传送点信息
        var mapid = this.mapId();//获得当前的地图ID
        var sTeleport = $dataMap.meta.teleport;//读取地图的meta数据中的teleport数据
        //一条传送点信息,如:<teleport: 11 3 0 1 世界之源:起点>,包含了传送点的多个数据,它们之间用空格分隔,将这些数据分割出来
        var sProps = sTeleport.split(" ").filter(function (t) { return t != ""; }); //使用空格来分割出各个字符串,并过滤掉空白字符串
        var x = Number(sProps[0]);//传送点x坐标
        var y = Number(sProps[1]);//传送点y坐标
        var index = this.mndIndexOfTeleportPlace(mapid, x, y);//检查该传送点是否已经存在
        if (index < 0) {//如果不存在,就将该传送点保存起来
            var enabled = sProps[2] == undefined ? true : Boolean(Number(sProps[2]));//传送点是否启用
            var visible = sProps[3] == undefined ? true : Boolean(Number(sProps[3]));//传送点是否可见
            var name = sProps[4] || $dataMapInfos[mapid].name;//传送点的名称,如果未定义,则使用地图的名称
            this.mndRegisterTeleportPlace(mapid, x, y, enabled, visible, name);//将传送点信息保存下来
        }
    }
};
Game_Map.prototype.mndIndexOfTeleportPlace=function(mapid, x, y) {
    if(this._mndTeleportPlaces==undefined) return -1;
    for (var index in this._mndTeleportPlaces) {
        var place = this._mndTeleportPlaces[index];
        if (place.mapid == mapid && place.x == x && place.y == y) {
            return index;
        }
    }
    return -1;
}
Game_Map.prototype.mndRegisterTeleportPlace=function(mapid, x, y, enabled, visible, name) {
    if(this._mndTeleportPlaces==undefined) this._mndTeleportPlaces=[];//如果_mndTeleportPlaces不可用,则重新初始化
    if(this.mndIndexOfTeleportPlace(mapid, x, y)<0) {//只当传送点未登记时才保存
        this._mndTeleportPlaces.push({
            mapid: mapid,
            x: x,
            y: y,
            name: name,
            enabled: enabled,
            visible: visible
        });
    }
}

这里,Game_Map.prototype.mndIndexOfTeleportPlace方法用于根据传送点信息,在已登记的传送点中查找该传送点是否已经记录过,如果有记录,则返回传送点的索引,如果没有记录则返回-1。Game_Map.prototype.mndRegisterTeleportPlace方法用于将传送点信息记录到变量_mndTeleportPlaces中。每个传送点用一个对象表示,该对象由属性mapid, x, y, name, enabled, visible等组成,除了mapid表示传送点所在的地图ID外,其它的属性都是前面介绍过的。一个传送点对象举例如:

{
    mapid: 1,
    x: 11,
    y: 3,
    name: "世界之源:起点",
    enabled: false,
    visible: true
}

到这里我们可以再次测试一下,启动游戏,确保角色进入之前填写了备注的游戏地图,按F8打开开发者工具,在Console中输入$gameMap._mndTeleportPlaces回车,可以看到该变量中已经保存了当前地图中定义的传送点信息。

_mndTeleportPlaces

前面说了,自定义数据保存到存档中,存在一个坑,这里就来说说这个坑。在mndIndexOfTeleportPlacemndRegisterTeleportPlace方法中,都会检测一下_mndTeleportPlaces是否为undefined,你可能会问,这个变量不是已经在Game_Map.prototype.initialize中初始化了么,为嘛还要检测呢?考虑这样一个使用场景:你的第一版游戏发布时,你没有使用传送插件,玩家保存的第一版的游戏存档中并没有_mndTeleportPlaces这个数据,之后你的第二版游戏发布了,在该版中你使用了传送插件,此时玩家用第二版游戏加载第一版的存档,因为第一版存档中缺少_mndTeleportPlaces数据,就会造成现在_mndTeleportPlaces变量是undefined的状态,如果直接使用它将会导致游戏崩溃。为避免这种情况发生,有必要在使用这类保存到存档中的自定义数据前,对它进行一次可用性检测。当然,如果插件在你制作第一版的游戏中就开始使用,那么这种情况可以不考虑(当然这不太严谨。这个问题从引擎本身来说可以作改进,但官方不改进之前,我们恐怕都需要作这样一个检测)。

使用地图备注登记多个传送点并在插件中记录

如果一个地图有多个传送点,那么登记格式需要改变一下,我们用一个半角分号隔开多个传送点信息。支持多个传送点的登记代码格式如下:

<teleport: x y [enabled] [visible] [name][;...]>

举个栗子:

<teleport: 11 3 0 1 世界之源:起点; 5 8 1 1 世界之源:河心>

这段代码登记了二条传送点信息:11 3 0 1 世界之源:起点5 8 1 1 世界之源:河心。相应的在插件中处理的时候就要改进解析方式,具体就是修改Game_Map.prototype.setup方法中的实现,增加使用split分隔出多个传送点信息的代码,然后,就像前面解析单个传送点信息一样,使用foreach解析出每个传送点信息。Game_Map.prototype.setup方法修改后变为以下代码:

var _Game_Map_setup = Game_Map.prototype.setup;
Game_Map.prototype.setup = function(mapId) {
    _Game_Map_setup.call(this, mapId);

    if ($dataMap.note.contains("<teleport:")) {//判断备注中是否包含传送点信息
        var mapid = this.mapId();//获得当前的地图ID
        var sTeleports = $dataMap.meta.teleport;//读取地图的meta数据中的teleport数据
        //多个传送点信息,如:“<eleport: 11 3 0 1 世界之源:起点; 15 8 1 1 世界之源:河心>”,包含了多个传送点的数据,它们之间用半角分号空格分隔
        //每个传送点信息,如:“11 3 0 1 世界之源:起点”,包含了传送点的多个数据,它们之间用空格分隔,将这些数据分割出来
        var sTeleportInfos = sTeleports.split(";").filter(function (t) { return t != "" });//分解出多个传送点;filter用于过滤掉空白字符串
        sTeleportInfos.forEach(function (sTeleportInfo) {//处理每个传送点信息
            var sProps = sTeleportInfo.split(" ").filter(function (t) { return t != ""; });
            var x = Number(sProps[0]);//传送点x坐标
            var y = Number(sProps[1]);//传送点y坐标
            var index=this.mndIndexOfTeleportPlace(mapid, x, y);//检查该传送点是否已经存在
            if (index < 0) {//如果不存在,就将该传送点保存起来
                var enabled = sProps[2] == undefined ? true : Boolean(Number(sProps[2]));//传送点是否启用
                var visible = sProps[3] == undefined ? true : Boolean(Number(sProps[3]));//传送点是否可见
                var name = sProps[4] || $dataMapInfos[mapid].name;//传送点的名称,如果未定义,则使用地图的名称
                this.mndRegisterTeleportPlace(mapid, x, y, enabled, visible, name);//将传送点信息保存下来
            }
        }, this);
    }
};

我们再用开发者工具测试一下效果,如下图,$gameMap._mndTeleportPlaces中已经成功记录了二条传送点信息。

多个传送点

制作传送点选取窗口显示传送点数据

传送点选取窗口用于显示已经登记的传送点信息,也就是已经保存到$gameMap._mndTeleportPlaces中的传送点数据。当我们使用传送道具或传送技能时,会弹出一个窗口显示所有可供传送到的传送点,点击其中一个传送点后开始进行传送。
  传送点选取窗口显然是一个命令窗口,所有传送点像一个个菜单命令一样可以被点击,所以这个窗口可以继承自Window_Selectable或者Window_Command,后者是前者的子类,至于继承自哪个,看情况而定(如果Window_Command提供的新功能让我们更容易的实现效果那就选它),这里我们继承自Window_Command。在LEARN_Teleport.js中添加以下实现代码:

function Window_Teleport() {
        this.initialize.apply(this, arguments);
    }

    Window_Teleport.prototype = Object.create(Window_Command.prototype);
    Window_Teleport.prototype.constructor = Window_Teleport;
    //窗口宽度
    Window_Teleport.prototype.windowWidth = function () {
        return 240;
    };
    //窗口高度
    Window_Teleport.prototype.windowHeight = function () {
        return Graphics.height;
    };
    //传送点排列时显示的列数
    Window_Teleport.prototype.maxCols = function () {
        return 2;
    };
    //制作菜单项并显示在窗口中
    Window_Teleport.prototype.makeCommandList = function () {
        for (var index in $gameMap._mndTeleportPlaces) {//处理每个传送点
            var teleportPlace = $gameMap._mndTeleportPlaces[index];//获取传送点对象
            if (teleportPlace.visible) {//如果传送点是可见的
                //将一个传送点以菜单命令方式加入窗口中,所有菜单的标识符都设置为`teleport`,即都绑定到同一个事件处理
                //这里将index作为ext数据,这索引表示该传送点在在传送点列表_mndTeleportPlaces中的索引
                this.addCommand(teleportPlace.name, 'teleport', teleportPlace.enabled, index);
            }
        }
    };

这个传送点选取窗口的类名为Window_Teleport,在这里重写了windowWidthwindowHeight方法以重新设定窗口宽度和高度,重写maxCols以设定传送点菜单列表显示的列数(窗口足够宽的话,可以增加列数,这样如果传送点较多,可以减少翻页次数)。重写makeCommandList方法将传送点信息以菜单命令的方式添加到窗口中。在该方法中,this.addCommand(teleportPlace.name, 'teleport', teleportPlace.enabled, index);用于将一个传送点信息以菜单命令形式添加到窗口中。这里所有菜单命令都有一个相同的标识符teleport,也就是说它们都被绑定到同一个事件中去处理选取操作。那么在处理时如何知道选择的是哪个传送点命令呢?这个就要用到addCommand方法中的ext参数,它用于保存菜单的扩展数据,这里将传送点在_mndTeleportPlaces中的索引index作为ext数据传递给addCommand方法,在处理时就可以根据该数据来区分不同的传送点。addCommand方法的第3个参数是用于设置菜单的启用或禁用状态,这里用teleportPlace.enabled来设置该传送点是否允许传送。在这里,菜单并没有实际绑定处理事件,它只是绑定了事件标识符,我们会在重写Scene_ItemBase时才给它们具体绑定处理事件,因为我们要在玩家使用传送道具、传送技能时才去触发传送点选取事件。
  现在让我们测试一下这个窗口能不能正常工作了。重写Scene_Map.prototype.start方法,在该方法中创建Window_Teleport窗口,并将它添加到场景中,这样,这个窗口就会在游戏一开始就出现在场景中。在LEARN_Teleport.js中添加以下临时代码(测试完后删除):

var _Scene_Map_create = Scene_Map.prototype.start;
    Scene_Map.prototype.start = function() {
        _Scene_Map_create.call(this);
        var win=new Window_Teleport();
        this.addWindow(win);
    };

运行游戏查看效果:

测试传送点选取窗口

传送点选取窗口中已经成功显示了我们在该地图登记的二个传送点信息,并且第一个传送点如我们所要的是一开始是禁止传送状态。当然,现在点击传送点不会有任何作用,因为还没有绑定处理事件。将上面的临时代码从LEARN_Teleport.js文件中删除,这个窗口现在不应该出现在这里。

将物品或技能标记为传送物品、传送技能

与地图数据的meta类似,这一次在物品或技能的备注中添加meta数据即可。如果一个物品或技能要具有传送功能,我们约定,只需要在它的备注中添加代码<teleport>即可。由于一旦作为传送物品或传送技能使用,我们将忽略物品或技能的其它功能,所以应将该物品或技能仅作为传送物品或技能使用(当然,如果你想要保留它们的其它功能,也是可以的。对于对全体使用的物品或技能,这个处理比较简单,而对于需要指定使用者的物品或技能处理起来则稍微麻烦点。由于传送物品或技能通常都是专用的,所以咱这里就不管它们的其它功能了)。

回城卷轴_消耗品

上面是一个作为可消耗的传送物品:回城卷轴的设置。主要是在它的备注中添加代码<teleport>表示这是个传送道具。如果要将其设置为永久使用的物品,则将“消耗品”参数设置为“否”即可。使用场合设置为“菜单画面”,避免在战斗中使用,当然,即使不这样设置,我们也将在插件中作战斗检测,并禁止在战斗中使用传送功能。

现在回到插件中对这个meta进行处理。要在哪里处理呢?当我们选择一个物品或技能进行使用时,才有必要检查这个物品或技能是不是传送物品或传送技能(如果是则弹出传送点选择窗口进行传送操作)。所以,只要重写Scene_ItemBase.prototype.determineItem方法,在这里检查物品、技能的备注内容中是否含有<teleport>标记,如果有,就表示它们是传送物品或传送技能,则显示传送点选择窗口,以供选择传送点开启传送操作,否则则仍然使用原方法进行处理。在LEARN_Teleport.js添加以下实现:

var _Scene_ItemBase_determineItem = Scene_ItemBase.prototype.determineItem;
Scene_ItemBase.prototype.determineItem = function () {
    var item = this.item();
    if (item.note.contains("<teleport>")) {
        this.showSubWindow(this.mnd_winTeleport);
    } else {
        _Scene_ItemBase_determineItem.call(this);
    }
};

这里,showSubWindow(this.mnd_winTeleport)方法用于显示传送点选择窗口。所以这里还需要先定义一下mnd_winTeleport。这个可以在Scene_ItemBase.prototype.start中定义,所以需要重写该方法。在LEARN_Teleport.js中增加以下代码:

var _Scene_ItemBase_start = Scene_ItemBase.prototype.start;
Scene_ItemBase.prototype.start = function () {
    _Scene_ItemBase_start.call(this);
    //创建传送点选择窗口
    this.mnd_winTeleport = new Window_Teleport();
    this.mnd_winTeleport.hide();
    this.mnd_winTeleport.x = Graphics.width;//移动到画面外面去,因为即使隐藏,还是可以被点击到(要显示时MV会自动设置它的位置,所以可以不管)
    this.mnd_winTeleport.setHandler('teleport', this.onTeleport.bind(this));
    this.mnd_winTeleport.setHandler('cancel', this.onTeleportCancelled.bind(this));
    this.addWindow(this.mnd_winTeleport);
};

在这里,我们还为该窗口增加了事件处理方法。在前面的Window_Teleport类的Window_Teleport.prototype.makeCommandList方法中,我们使用addCommand(teleportPlace.name, 'teleport', teleportPlace.enabled, index)将所有传送点都做成了菜单命令,将命令的点击事件绑定到事件标识符teleport,而在这里,mnd_winTeleportWindow_Teleport类的实例,我们使用mnd_winTeleport.setHandler('teleport', this.onTeleport.bind(this)) 将事件标识符teleport与事件onTeleport进行绑定,这样,当玩家在传送点选择窗口中点击传送点菜单后就会由onTeleport方法进行传送处理。而mnd_winTeleport.setHandler('cancel', this.onTeleportCancelled.bind(this))用于将事件标识符cancel与事件onTeleportCancelled进行绑定,当玩家在窗口中单击右键或按ESC键退出时由该方法进行取消操作的处理。
下面就分别实现onTeleportonTeleportCancelled方法,在LEARN_Teleport.js中增加以下代码:

Scene_ItemBase.prototype.onTeleport = function () {
    //TODO
}
Scene_ItemBase.prototype.onTeleportCancelled = function () {
    this.hideSubWindow(this.mnd_winTeleport);
};

onTeleportCancelled方法实现很简单,在取消传送时隐藏掉传送列点选择窗口就可以了。

显示传送动画实现传送功能

Scene_ItemBase.prototype.onTeleport方法用于当玩家在传送点选择窗口点击了一个传送点,此时就要对玩家队伍进行传送处理。先来看该方法的完整实现代码(在LEARN_Teleport.js中修改该方法):

Scene_ItemBase.prototype.onTeleport = function () {
    //如果施用传送功能的物品是可消耗的,则进行消耗处理;如果是传送技能,则消耗MP值
    var item=this.item();//玩家选中的物品或技能(也就是被设置成传送道具或传送技能的物品或技能)
    if (DataManager.isItem(item) && item.consumable) {//检查是不是使用的可消耗的传送物品
        SoundManager.playUseItem();     //播放物品使用音效
        $gameParty.loseItem(item, 1);   //物品被消耗
    }else if(DataManager.isSkill(item)){//检查是不是使用的传送魔法
        SoundManager.playUseSkill();    //播放魔法使用音效
        this.user().paySkillCost(item); //消耗MP
    }

    this.hideSubWindow(this.mnd_winTeleport);//隐藏传送点选择窗口
    SceneManager.goto(Scene_Map);//退回到游戏地图场景
    var index = this.mnd_winTeleport.currentExt(); //扩展数据中保存的是传送点在$gameMap.mnd_teleportPlaces中的索引
    var teleportPlace = $gameMap._mndTeleportPlaces[index];//获取玩家选择的传送点(目的地)信息
    $gamePlayer.gatherFollowers(); //集合队伍
    $gamePlayer._animationId = 117; //显示传送动画(动画ID为117,可以做成插件参数)
    //在等待1500毫秒后开始传送,这段时间主要用于等待传送动画显示完毕
    setTimeout(function (scene) {
        _isTeleporting = true; //用于标志本次传送是由我们自定义的传送道具或技能引起的,在传送完毕时,根据该开关可以作其它操作,比如展示传送完毕的动画
        $gamePlayer.reserveTransfer(teleportPlace.mapid, teleportPlace.x, teleportPlace.y, 0, 0); //传送队伍到指定地图的指定位置
    }, 1500, SceneManager._nextScene);
};
var _isTeleporting = false;

首先,使用DataManager.isItem(item) && item.consumable来检查当前使用的是不是可消耗的传送物品,如果是,则物品要被消耗掉,如果不是则用DataManager.isSkill(item)检查当前使用的是不是传送技能,如果是则消耗MP。
  其次,在正式传送时,先使用this.hideSubWindow(this.mnd_winTeleport)将传送点选择窗口隐藏掉。使用SceneManager.goto(Scene_Map)直接退回到游戏地图场景,所以菜单窗口也会自动消失。通过var index = this.mnd_winTeleport.currentExt()取得玩家选择的传送点在$gameMap._mndTeleportPlaces中的索引,然后使用var teleportPlace = $gameMap._mndTeleportPlaces[index]取得传送点信息。
  最后,传送准备全部完成后,使用$gamePlayer.gatherFollowers()集合队伍,使用$gamePlayer._animationId = 117来在玩家身上显示一个动画,这个动画是在数据库动画中定义的ID为117的动画(是的,要使用数据库中预置的动画,只需要将动画ID指定给玩家的_animationId即可),这将作为传送开始的一个效果动画(当然,最好将它做成插件参数以便可以配置)。我们需要等待动画完成或至少播放了一段时间后才开始场所转移,否则动画就看不到了。所以,使用setTimeout方法来等待1500毫秒,一旦等待结束,使用$gamePlayer.reserveTransfer(teleportPlace.mapid, teleportPlace.x, teleportPlace.y, 0, 0)将玩家队伍传送到指定地图的指定位置。_isTeleporting是个标记,表示这次的传送操作是由我们自定义的传送物品或技能造成的,这个标记主要用于辅助实现后面传送结束时再显示一个传送结束的动画的效果。

我们希望在传送结束时也能显示一个结束的传送动画。这里就需要重写Game_Player.prototype.performTransfer方法,这个方法看名字就是“执行转移”的意思,很显然,我们可以修改这个方法来达到目的。在LEARN_Teleport.js中添加以下代码:

Game_Player.prototype.performTransfer = function () {
    if (this.isTransferring()) {
        this.setDirection(this._newDirection);
        if (this._newMapId !== $gameMap.mapId() || this._needsMapReload) {
            $gameMap.setup(this._newMapId);
            this._needsMapReload = false;
        }
        this.locate(this._newX, this._newY);

        if (_isTeleporting) {
            $gamePlayer._animationId = endAnimId;
            _isTeleporting = false;
        }

        this.refresh();
        this.clearTransferInfo();
    }
};

这里我们主要是是插入这一段代码:

if (_isTeleporting) {
    $gamePlayer._animationId = 120;
    _isTeleporting = false;
}

意思是,如果传送操作是由我们自定义的传送物品或技能造成的,则让角色再显示一个ID为120的动画,这个动画将在角色传送到新地图后立即执行,最后将_isTeleporting置为false,以表示一个传送效果的完满终结。

到这里,可以测试一下完整的传送效果。


传送效果

禁止使用传送道具或传送技能

当要让某些地图禁止使用传送道具或技能时,我们约定,可以在地图备注中增加<!teleport>的代码作为标记(感叹号表示“否定”的意思)。另外,如果在战斗中,也要禁止使用传送道具或技能(虽然在战斗中实际使用传送道具或技能不会产生实质效果,但仍然应该禁止选择使用,因为这实际会导致损失一个回合操作)。
  要实现这种限制,需要重写Game_BattlerBase.prototype.meetsUsableItemConditions方法,这个方法用于设置物品、技能使用的条件。在LEARN_Teleport.js中添加以下代码:

var _Game_BattlerBase_meetsUsableItemConditions=Game_BattlerBase.prototype.meetsUsableItemConditions;
Game_BattlerBase.prototype.meetsUsableItemConditions = function(item) {
    if((!$gameMap.mndIsMapTeleportEnabled($gameMap.mapId()) || $gameParty.inBattle()) && item.note.contains("<teleport>")) return false;
    else return _Game_BattlerBase_meetsUsableItemConditions.call(this, item);
};

$gameParty.inBattle()用于检测角色当前是否正在进行战斗;item.note.contains("<teleport>")用于检测当前的要使用的物品或技能是否为传送物品或技能(这个标记代码前面已经说过了)。

$gameMap.mndIsMapTeleportEnabled($gameMap.mapId())是用于检测当前地图是否允许使用传送功能。这个方法还没有创建,它是Game_Map类的一个方法。要实现这个方法,我们还做多一点事情。和$gameMap._mndTeleportPlaces变量类似,我们需要另一个对象来记录哪些地图禁止使用传送功能。所以,回到重写的Game_Map.prototype.initialize方法中,修改为以下代码:

Game_Map.prototype.initialize = function() {
    _Game_Map_initialize.call(this);
    this._mndTeleportPlaces=[];
    this._mndTeleportUnableMaps={};//添加这条代码
};

$gameMap._mndTeleportUnableMaps就用于保存地图的传送功能是否开启,它是一个“词典”,key是地图的mapIdvalue是开启状态。比如,记录地图ID为1的地图的传送 功能是否开启,可以这样记录:$gameMap._mndTeleportUnableMaps[1]=true,而要查询地图2的开启状态就可以使用$gameMap._mndTeleportUnableMaps[2]来访问。
$gameMap._mndTeleportPlaces类似,还需要对<!teleport>的标记进行解析,这一步在Game_Map.prototype.setup方法中处理,将该方法修改为如下:

Game_Map.prototype.setup = function(mapId) {
    _Game_Map_setup.call(this, mapId);

    //=====添加代码=====
    if($dataMap.note.contains("<!teleport>")){ //表示该地图不允许使用传送道具或技能
        if(this._mndTeleportUnableMaps[this.mapId()]==undefined)
            this.mndSetMapTeleportEnabled(this.mapId(), false);
    }
    //=====添加结束=====

    if ($dataMap.note.contains("<teleport:")) {
    ...

这里还要实现mndSetMapTeleportEnabled方法,添加代码如下:

Game_Map.prototype.mndSetMapTeleportEnabled=function(mapid, enabled){
    if(this._mndTeleportUnableMaps==undefined) this._mndTeleportUnableMaps={};
    if(enabled) {
        if(this._mndTeleportUnableMaps[mapid]!=undefined)
            this._mndTeleportUnableMaps[mapid]=false;
    }else{
        this._mndTeleportUnableMaps[mapid]=true;
    }
}

这个方法是用来设置指定地图的传送功能的开启状态。前面已经说过,对于要保存到存档中的自定义数据,在使用前应当检测它的可用状态,如果不可用要重新初始化:if(this._mndTeleportUnableMaps==undefined) this._mndTeleportUnableMaps={};。如果地图允许使用传送功能,则将该地图的ID以key的方式添加到_mndTeleportUnableMaps中,并将其值设置为false,相反,如果禁用传送功能,则值为true

现在,可以去实现Game_Map.prototype.mndIsMapTeleportEnabled方法了,在LEARN_Teleport.js中添加以下代码:

Game_Map.prototype.mndIsMapTeleportEnabled=function (mapid) {
        //当地图未在_mndTeleportUnableMaps记录或者记录值为false时表示地图允许使用传送道具和技能
        if(this._mndTeleportUnableMaps==undefined) return true;
        else return !!!this._mndTeleportUnableMaps[mapid];
    }

!!!this._mndTeleportUnableMaps[mapid]相当于this._mndTeleportUnableMaps[mapid]==undefined || this._mndTeleportUnableMaps[mapid]==false。现在,再测试一下效果,如下图,“西之森”的地图备注中添加了<!teleport>代码,所以在进入西之森之后,“回城卷轴”是不可用状态,也就是禁止传送状态,退回到“世界之源”后,该地图并未禁止传送功能,所以传送道具又能使用了。在进入战斗后,想使用道具,发现道具中不显示“传送道具”,这就是Game_BattlerBase.prototype.meetsUsableItemConditions方法在启作用,因为在该方法中我们已经禁止在战斗时使用传送道具或技能。

禁止使用传送功能

实现插件命令

如何处理插件指令已经在以前的教程中讲过,这里只简单介绍一下最终的插件命令的实现代码。这些插件命令的作用,在本文一开始就作了说明。这里,除了重写Game_Interpreter.prototype.pluginCommand来处理插件指令外,同时还需要实现Game_Map.prototype.mndRemoveTelleportPlace用来删除登记的传送点,Game_Map.prototype.mndSetTeleportPlaceEnabled用来设置传送点的显示或隐藏,启用或禁用状态。

var params = PluginManager.parameters("MND_Teleport");
var startAnimId = Number(params["Teleport Start AnimationId"]) || 117;
var endAnimId = Number(params["Teleport End AnimationId"]) || 120;

var _Game_Interpreter_pluginCommand = Game_Interpreter.prototype.pluginCommand;
Game_Interpreter.prototype.pluginCommand = function (command, args) {
    _Game_Interpreter_pluginCommand.call(this, command, args);

    switch (command) {
        case "registerTeleportPlace"://插件命令格式:registerTeleportPlace mapid x y [enabled] [visible] [name]
            var mapid = Number(args[0]);
            var enabled = args[3] == undefined ? true : Boolean(Number(args[3]));
            var visible = args[4] == undefined ? true : Boolean(Number(args[4]));
            var name = args[5] || $dataMapInfos[mapid].name;
            $gameMap.mndRegisterTeleportPlace(mapid, Number(args[1]), Number(args[2]), enabled, visible, name);
            break;
        case "removeTeleportPlace"://插件命令格式:removeTeleportPlace mapid x y
            $gameMap.mndRemoveTelleportPlace(Number(args[0]), Number(args[1]), Number(args[2]));
            break;
        case "setTeleportPlaceEnabled"://插件命令格式:setTeleportPlaceEnabled mapid x y enabled visible
            $gameMap.mndSetTeleportPlaceEnabled(Number(args[0]), Number(args[1]), Number(args[2]), Boolean(Number(args[3])), Boolean(Number(args[4])));
            break;
        case "setMapTeleportEnabled"://插件命令格式:setMapTeleportEnabled enabled
            $gameMap.mndSetMapTeleportEnabled($gameMap.mapId(), Boolean(Number(args[0])));
        default:
            break;
    }
}

Game_Map.prototype.mndRemoveTelleportPlace=function(mapid, x, y) {
    var index = this.mndIndexOfTeleportPlace(mapid, x, y);
    if (index >= 0) {
        this._mndTeleportPlaces.splice(index, 1);
    }
}
Game_Map.prototype.mndSetTeleportPlaceEnabled=function(mapid, x, y, enabled, visible) {
    var index = this.mndIndexOfTeleportPlace(mapid, x, y);
    if (index >= 0) {
        this._mndTeleportPlaces[index].enabled = enabled;
        this._mndTeleportPlaces[index].visible = visible;
    }
}

完整的代码请到 这里 查看或下载。
by Mandarava(鳗驼螺) 2017.9.4

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

推荐阅读更多精彩内容