Project1

标题: RPGmaker MV插件源码解析-Yoji Ojima'EnemyBook.js [打印本页]

作者: FREEstriker    时间: 2018-4-9 16:45
标题: RPGmaker MV插件源码解析-Yoji Ojima'EnemyBook.js
    这篇帖子是本人原创,首先发布在了CSDN博客上https://blog.csdn.net/qq_30180107/article/details/79856119
    希望给那些有意向自己编写MV插件的吧友一些启发。
    未经本人允许,不得转载。正文如下:
    想写这篇博客很久了,我大概是在高一的时候在网上发现RPGmaker这个软件的,当时最新的版本还是VX Ace,当时住校,只有在放假的时候才能尝试一下这个软件,那时我的梦想是成为一个游戏设计师,而RPGmaker对小白来说非常友好,不需要知道编程语言,不需要自己作画,也可以制作出自己心中的那部游戏(虽然有许多限制),我自己在放假时也自己搞出来一个算是DEMO的东西,让我兴奋了好一阵子。过了一段时间,我看到贴吧里有人发新闻说新的RPGmaker要发布了,并且支持安卓和IOS了,并且语言也不再是Ruby而是JS了,但是由于学业渐紧,我也没有时间来搞这个业余爱好了。

    去年我考上了大学,阴差阳错的上了“计算机科学与技术”这个专业,第一学期学习了C语言并且出色完成了大作业后,我决定重拾我这个爱好,趁着打折购买了RPGmaker MV,并且在寒假时自学了基础的JS基础,大概就了解到对象和继承那一块,这学期开始我就开始研究如何写插件,当时看到动辄5000行的源代码很是头疼,最近才开始研究规模小很多的插件源码,还是挺有收获的,在这里给大家分享一下。

    这篇文章会涉及一些JS基础,纯小白大概会看不懂,纯小白可以看一下B站上的网课或《JavaScript高级程序设计》这本书(我是看的这本书)。而我由于只系统的接受过面向过程的C语言的教育,所以对面向对象的JS可能会有理解偏差,如出现错误,还恳请大佬指正,我会认真学习并改正的。

    这个插件来源于正版RPGmaker MV自带的插件,是由Yoji Ojima编写的EnemyBook.js,在此向他(她)表示感谢,本文章只是学习插件的编写方法,如涉及版权我会主动删除。

    首先来看一下插件的帮助信息:



    这个插件的命令语句有五句,备注有三条,需要初始化的数据有一个,这些信息都包含在源码开头的注释中(由于编写者大概来自个日本,所以包含了日语版本),只要按照这个样子写注释,就会被软件识别:

[javascript] view plain copy
//=============================================================================  
// EnemyBook.js  
//=============================================================================  
  
/*:
* @plugindesc Displays detailed statuses of enemies.
* @author Yoji Ojima
*
* @param Unknown Data
* @desc The index name for an unknown enemy.
* @default ??????
*
* @help
*
* Plugin Command:
*   EnemyBook open         # Open the enemy book screen
*   EnemyBook add 3        # Add enemy #3 to the enemy book
*   EnemyBook remove 4     # Remove enemy #4 from the enemy book
*   EnemyBook complete     # Complete the enemy book
*   EnemyBook clear        # Clear the enemy book
*
* Enemy Note:
*   <desc1:foobar>         # Description text in the enemy book, line 1
*   <desc2:blahblah>       # Description text in the enemy book, line 2
*   <book:no>              # This enemy does not appear in the enemy book
*/  
  
/*:ja
* @plugindesc モンスター図鑑です。敵キャラの詳細なステータスを表示します。
* @author Yoji Ojima
*
* @param Unknown Data
* @desc 未確認の敵キャラの索引名です。
* @default ??????
*
* @help
*
* プラグインコマンド:
*   EnemyBook open         # 図鑑画面を開く
*   EnemyBook add 3        # 敵キャラ3番を図鑑に追加
*   EnemyBook remove 4     # 敵キャラ4番を図鑑から削除
*   EnemyBook complete     # 図鑑を完成させる
*   EnemyBook clear        # 図鑑をクリアする
*
* 敵キャラのメモ:
*   <desc1:なんとか>       # 説明1行目
*   <desc2:かんとか>       # 説明2行目
*   <book:no>              # 図鑑に載せない場合
*/  
    注释过后,就是代码正文部分,正文部分被包含在一个大括号中,如下,这样可以防止编写的插件中的变量与源代码中的变量冲突:

[javascript] view plain copy
(function() {  
    /*全部的代码*/  
})();  
    这个function内部,首先是对命令语句的实现:

[javascript] view plain copy
var parameters = PluginManager.parameters('EnemyBook');  
    var unknownData = String(parameters['Unknown Data'] || '??????');  
  
    var _Game_Interpreter_pluginCommand =  
            Game_Interpreter.prototype.pluginCommand;  
    Game_Interpreter.prototype.pluginCommand = function(command, args) {  
        _Game_Interpreter_pluginCommand.call(this, command, args);  
        if (command === 'EnemyBook') {  
            switch (args[0]) {  
            case 'open':  
                SceneManager.push(Scene_EnemyBook);  
                break;  
            case 'add':  
                $gameSystem.addToEnemyBook(Number(args[1]));  
                break;  
            case 'remove':  
                $gameSystem.removeFromEnemyBook(Number(args[1]));  
                break;  
            case 'complete':  
                $gameSystem.completeEnemyBook();  
                break;  
            case 'clear':  
                $gameSystem.clearEnemyBook();  
                break;  
            }  
        }  
    };  
    这段代码本质上是向Game_Interpreter原型中的pluginCommand添加了新的对变量command的判断,如果变量command是EnemyBook,就再判断输入的命令的下一个单词是什么,之后通过switch语句实现相应的功能。注意由于输入的命令open的效果是打开打开窗口并显示数据,所以它的实现是将一个窗口对象入栈到SceneManger中;而其他几个命令都是对数据的修改,不涉及显示窗口,所以其余的实现都是调用$gameSystem的方法来完成修改数据的。

    接下来是对上面switch语句中方法的实现,如下:

[javascript] view plain copy
Game_System.prototype.addToEnemyBook = function(enemyId) {  
        if (!this._enemyBookFlags) {  
            this.clearEnemyBook();  
        }  
        this._enemyBookFlags[enemyId] = true;  
    };  
  
    Game_System.prototype.removeFromEnemyBook = function(enemyId) {  
        if (this._enemyBookFlags) {  
            this._enemyBookFlags[enemyId] = false;  
        }  
    };  
  
    Game_System.prototype.completeEnemyBook = function() {  
        this.clearEnemyBook();  
        for (var i = 1; i < $dataEnemies.length; i++) {  
            this._enemyBookFlags = true;  
        }  
    };  
  
    Game_System.prototype.clearEnemyBook = function() {  
        this._enemyBookFlags = [];  
    };  
    由以上代码可知,作者对Game_System的原型创建了一个名为_enemyBookFlags的数组,里面存放了各个怪物是否在怪物书中的判定,向书中添加怪物时只要把怪物编号对应的数组元素赋值true即可,去除时赋值false,清空时直接设为空数组,完成时先将其清空再全赋值true。

    接下来是一些游戏中自动执行添加怪物的实现:

[javascript] view plain copy
Game_System.prototype.isInEnemyBook = function(enemy) {  
        if (this._enemyBookFlags && enemy) {  
            return !!this._enemyBookFlags[enemy.id];  
        } else {  
            return false;  
        }  
    };  
  
    var _Game_Troop_setup = Game_Troop.prototype.setup;  
    Game_Troop.prototype.setup = function(troopId) {  
        _Game_Troop_setup.call(this, troopId);  
        this.members().forEach(function(enemy) {  
            if (enemy.isAppeared()) {  
                $gameSystem.addToEnemyBook(enemy.enemyId());  
            }  
        }, this);  
    };  
  
    var _Game_Enemy_appear = Game_Enemy.prototype.appear;  
    Game_Enemy.prototype.appear = function() {  
        _Game_Enemy_appear.call(this);  
        $gameSystem.addToEnemyBook(this._enemyId);  
    };  
  
    var _Game_Enemy_transform = Game_Enemy.prototype.transform;  
    Game_Enemy.prototype.transform = function(enemyId) {  
        _Game_Enemy_transform.call(this, enemyId);  
        $gameSystem.addToEnemyBook(enemyId);  
    };  
    第一个函数是判断参数enemy是否存在于标志数组中,后面三个都是向软件原有的对象添加了加入怪物书的函数,第一个是依次判断一个怪物小队中的所有怪物并将没有的加入书中,第二个是将新出现的怪物添加到书中,第三个是将新传送(?我也不知道这个是啥?)来的敌人添加到书中。

    以上代码是比较容易读懂的。而下面是这个插件的核心部分——窗口中信息的显示,这部分有些难懂,主要是因为有些属性,方法或函数我还不知道具体是做什么的(将来懂了之后会一一补上),很多this搞得我有些混乱,而且存在来回调用的情况,所以如果下面我说错了的话,请各位多多包涵,我会学习并改正的。

    首先,作者创建了一个Scene_EnemyBook的对象(这个是在开头的switch语句中压入SceneManager的栈中的那个),下面是它的实现:

[javascript] view plain copy
function Scene_EnemyBook() {  
        this.initialize.apply(this, arguments);  
    }  
  
    Scene_EnemyBook.prototype = Object.create(Scene_MenuBase.prototype);  
    Scene_EnemyBook.prototype.constructor = Scene_EnemyBook;  
  
    Scene_EnemyBook.prototype.initialize = function() {  
        Scene_MenuBase.prototype.initialize.call(this);  
    };  
  
    Scene_EnemyBook.prototype.create = function() {  
        Scene_MenuBase.prototype.create.call(this);  
        this._indexWindow = new Window_EnemyBookIndex(0, 0);  
        this._indexWindow.setHandler('cancel', this.popScene.bind(this));  
        var wy = this._indexWindow.height;  
        var ww = Graphics.boxWidth;  
        var wh = Graphics.boxHeight - wy;  
        this._statusWindow = new Window_EnemyBookStatus(0, wy, ww, wh);  
        this.addWindow(this._indexWindow);  
        this.addWindow(this._statusWindow);  
        this._indexWindow.setStatusWindow(this._statusWindow);  
    };  
  
    Window_EnemyBookIndex.prototype.setStatusWindow = function(statusWindow) {  
        this._statusWindow = statusWindow;  
        this.updateStatus();  
    };  
    先大概浏览一下代码,这个Scene_EnemyBook的原型就是把Scene_MenuBase的原型复制了一下,之后在create这个方法里给Scene_EnemyBook加上了两个窗口对象_indexWindow和_statusWindow(都是通过构造函数和原型模式创建的),从名字来猜一下,_indexWindow应该是选择窗口,_statusWindow应该是显示信息的窗口。最后通过setStatusWindow这个方法把_statusWindow加到_indexWindow的原型里,并进行首次_statusWindow的首次更新:



    接下来的代码就是对_indexWindow和_statusWindow的创建,首先是Window_EnemyBookIndex(用于创建_indexWindow):



[javascript] view plain copy
function Window_EnemyBookIndex() {  
        this.initialize.apply(this, arguments);  
    }  
  
    Window_EnemyBookIndex.prototype = Object.create(Window_Selectable.prototype);  
    Window_EnemyBookIndex.prototype.constructor = Window_EnemyBookIndex;  
  
    Window_EnemyBookIndex.lastTopRow = 0;  
    Window_EnemyBookIndex.lastIndex  = 0;  
  
    Window_EnemyBookIndex.prototype.maxCols = function() {  
        return 3;  
    };  
  
    Window_EnemyBookIndex.prototype.maxItems = function() {  
        return this._list ? this._list.length : 0;  
    };  
  
    Window_EnemyBookIndex.prototype.setStatusWindow = function(statusWindow) {  
        this._statusWindow = statusWindow;  
        this.updateStatus();  
    };  
  
    Window_EnemyBookIndex.prototype.update = function() {  
        Window_Selectable.prototype.update.call(this);  
        this.updateStatus();  
    };  
  
    Window_EnemyBookIndex.prototype.updateStatus = function() {  
        if (this._statusWindow) {  
            var enemy = this._list[this.index()];  
            this._statusWindow.setEnemy(enemy);  
        }  
    };  
  
    Window_EnemyBookIndex.prototype.drawItem = function(index) {  
        var enemy = this._list[index];  
        var rect = this.itemRectForText(index);  
        var name;  
        if ($gameSystem.isInEnemyBook(enemy)) {  
            name = enemy.name;  
        } else {  
            name = unknownData;  
        }  
        this.drawText(name, rect.x, rect.y, rect.width);  
    };  
  
    Window_EnemyBookIndex.prototype.processCancel = function() {  
        Window_Selectable.prototype.processCancel.call(this);  
        Window_EnemyBookIndex.lastTopRow = this.topRow();  
        Window_EnemyBookIndex.lastIndex = this.index();  
    };  
  
    Window_EnemyBookIndex.prototype.initialize = function(x, y) {  
        var width = Graphics.boxWidth;  
        var height = this.fittingHeight(6);  
        Window_Selectable.prototype.initialize.call(this, x, y, width, height);  
        this.refresh();  
        this.setTopRow(Window_EnemyBookIndex.lastTopRow);  
        this.select(Window_EnemyBookIndex.lastIndex);  
        this.activate();  
    };  
  
    Window_EnemyBookIndex.prototype.refresh = function() {  
        this._list = [];  
        for (var i = 1; i < $dataEnemies.length; i++) {  
            var enemy = $dataEnemies;  
            if (enemy.name && enemy.meta.book !== 'no') {  
                this._list.push(enemy);  
            }  
        }  
        this.createContents();  
        this.drawAllItems();  
    };  

    Window_EnemyBookIndex的原型就是把Window_Selectable的原型复制了一遍,这里就可以确定刚才由Window_EnemyBookIndex创建的_indexWindow就是一个选择窗口。然后给两个属性lastTopRow 和lastIndex赋了值,lastIndex是选择项的初始位置,0就是代开窗口时选择项是第一个。接着定义方法maxCols和maxItems分别返回每行最多显示的选项个数和选项总的个数。然后向update方法中添加了执行updateStatus方法,效果就是每次更新选项窗口时也会更新信息窗口,实现了它们的同时更新。它下面的这个updateStatus就是更新信息窗口的方法,我们先略过它。下面的drawItem是在选项窗口中画出文字方法,如果index号怪物在怪物书中,就画出它的信息,若不在就画出unknownData,而这个unknownData的值是在对命令语句实现的代码的第二行赋的,其中的rect相关是被选择时的阴影的相关信息。然后向processCancel中添加了一些东西(抱歉我还不知道它是干啥的。。。)。最后是对initialize的初始化,制定了它的大小(fittingHeight(6)是6行的高度),继承Window_Selectable的initializ,然后执行更新(refresh),选择(setTopRow和select)和用户操作(activate)。然后下面就是refresh的实现,将所有怪物(不包括加了注释不进入怪物书的怪物)都压入_list,然后画出信息。

    下面是对Window_EnemyBookStatus(用于创建_statusWindow)的实现:

[javascript] view plain copy
function Window_EnemyBookStatus() {  
        this.initialize.apply(this, arguments);  
    }  
  
    Window_EnemyBookStatus.prototype = Object.create(Window_Base.prototype);  
    Window_EnemyBookStatus.prototype.constructor = Window_EnemyBookStatus;  
  
    Window_EnemyBookStatus.prototype.initialize = function(x, y, width, height) {  
        Window_Base.prototype.initialize.call(this, x, y, width, height);  
        this._enemy = null;  
        this._enemySprite = new Sprite();  
        this._enemySprite.anchor.x = 0.5;  
        this._enemySprite.anchor.y = 0.5;  
        this._enemySprite.x = width / 2 - 20;  
        this._enemySprite.y = height / 2;  
        this.addChildToBack(this._enemySprite);  
        this.refresh();  
    };  
  
    Window_EnemyBookStatus.prototype.setEnemy = function(enemy) {  
        if (this._enemy !== enemy) {  
            this._enemy = enemy;  
            this.refresh();  
        }  
    };  
  
    Window_EnemyBookStatus.prototype.update = function() {  
        Window_Base.prototype.update.call(this);  
        if (this._enemySprite.bitmap) {  
            var bitmapHeight = this._enemySprite.bitmap.height;  
            var contentsHeight = this.contents.height;  
            var scale = 1;  
            if (bitmapHeight > contentsHeight) {  
                scale = contentsHeight / bitmapHeight;  
            }  
            this._enemySprite.scale.x = scale;  
            this._enemySprite.scale.y = scale;  
        }  
    };  
  
    Window_EnemyBookStatus.prototype.refresh = function() {  
        var enemy = this._enemy;  
        var x = 0;  
        var y = 0;  
        var lineHeight = this.lineHeight();  
  
        this.contents.clear();  
  
        if (!enemy || !$gameSystem.isInEnemyBook(enemy)) {  
            this._enemySprite.bitmap = null;  
            return;  
        }  
  
        var name = enemy.battlerName;  
        var hue = enemy.battlerHue;  
        var bitmap;  
        if ($gameSystem.isSideView()) {  
            bitmap = ImageManager.loadSvEnemy(name, hue);  
        } else {  
            bitmap = ImageManager.loadEnemy(name, hue);  
        }  
        this._enemySprite.bitmap = bitmap;  
  
        this.resetTextColor();  
        this.drawText(enemy.name, x, y);  
  
        x = this.textPadding();  
        y = lineHeight + this.textPadding();  
  
        for (var i = 0; i < 8; i++) {  
            this.changeTextColor(this.systemColor());  
            this.drawText(TextManager.param(i), x, y, 160);  
            this.resetTextColor();  
            this.drawText(enemy.params, x + 160, y, 60, 'right');  
            y += lineHeight;  
        }  
  
        var rewardsWidth = 280;  
        x = this.contents.width - rewardsWidth;  
        y = lineHeight + this.textPadding();  
  
        this.resetTextColor();  
        this.drawText(enemy.exp, x, y);  
        x += this.textWidth(enemy.exp) + 6;  
        this.changeTextColor(this.systemColor());  
        this.drawText(TextManager.expA, x, y);  
        x += this.textWidth(TextManager.expA + '  ');  
  
        this.resetTextColor();  
        this.drawText(enemy.gold, x, y);  
        x += this.textWidth(enemy.gold) + 6;  
        this.changeTextColor(this.systemColor());  
        this.drawText(TextManager.currencyUnit, x, y);  
  
        x = this.contents.width - rewardsWidth;  
        y += lineHeight;  
  
        for (var j = 0; j < enemy.dropItems.length; j++) {  
            var di = enemy.dropItems[j];  
            if (di.kind > 0) {  
                var item = Game_Enemy.prototype.itemObject(di.kind, di.dataId);  
                this.drawItemName(item, x, y, rewardsWidth);  
                y += lineHeight;  
            }  
        }  
  
        var descWidth = 480;  
        x = this.contents.width - descWidth;  
        y = this.textPadding() + lineHeight * 7;  
        this.drawTextEx(enemy.meta.desc1, x, y + lineHeight * 0, descWidth);  
        this.drawTextEx(enemy.meta.desc2, x, y + lineHeight * 1, descWidth);  
    };  
    这个与上面的Window_EnemyBookIndex比较类似,它的initialize是初始化了一些图片参数,refresh就是设定了一些显示信息时的格式,其中的clear会清空这个窗口,然后重新画新的信息,它本质上就是一个更新窗口的方法。作者又向update中添加了一些对显示的图片的参数,这样每次更新画面时都会让显示的图片大小合适。

    最后我们来看上一个我们略过的updateStatus,将它和它调用的setEnemy和setEnemy调用的refresh一起看,如果选择的这个怪物与刚刚显示的怪物不是同一个时,就更新信息窗口。它就是通过这几个函数实现信息窗口显示信息随选择窗口选择怪物的变化而变化,关系图如下:



    通过这样的流程,就可以实现像EnemyBook这样的功能,希望对大家有所帮助,中间可能有些混乱,下次我会试着更加有条理一些,请大家多多包容,如有错误,请务必指正。

    谢谢大家!




欢迎光临 Project1 (https://rpg.blue/) Powered by Discuz! X3.1