RPGツクールMVのゲームエンジンはJavaScriptで書かれています。
ということは、中のソース見放題、解析し放題なわけで…。
しかし、このゲームエンジン。仕方ないっちゃ仕方ないのですが、ファイルの内訳が割と大雑把に分けられてて、数千行に及ぶソースコードの中に無数のクラスが散りばめられてます。
あまり長いソースコードって見る気しないし、せめてクラス単位でファイル分かれててほしいなぁ…ということで、ファイルを分割、および実行時のソースをビルドする環境を自分用に作成したので、作業内容とつまずいた箇所について記録しておこうと思います。
ソースリーディング用の資料やゲームエンジンのカスタマイズ時の参考にはなると思います。
ちなみに、ほんとにファイル分割してるだけなので、分割した成果物は公開してません(bitbucketにプライベートリポジトリで上げてある)。
あと、下記記事で作られたリポジトリの内容を参考にさせていただいてます。
- 今一番JSで熱いゲームエンジン、RPGツクールMVのランタイムコードを読んでみた - Qiita
http://qiita.com/mizchi/items/ff3bc3f52b61d3e599c6
ファイル構成
以下のようなファイル構成に変更します(ちょっと長いです)。
rpgmv-restructure
+--src(分割したソースコードを格納)
| +--core
| | +--Utils.js
| | +--Point.js
| | +--Rectangle.js
| | +--Bitmap.js
| | +--Graphics.js
| | +--Input.js
| | +--TouchInput.js
| | +--Sprite.js
| | +--Tilemap.js
| | +--TilingSprite.js
| | +--ScreenSprite.js
| | +--Window.js
| | +--WindowLayer.js
| | +--Weather.js
| | +--ToneFilter.js
| | +--Stage.js
| | +--WebAudio.js
| | +--Html5Audio.js
| | +--JsonEx.js
| +--jsextensions
| | +--Number.js
| | +--String.js
| | +--Array.js
| | +--Math.js
| +--managers
| | +--DataManager.js
| | +--ConfigManager.js
| | +--StorageManager.js
| | +--ImageManager.js
| | +--AudioManager.js
| | +--SoundManager.js
| | +--TextManager.js
| | +--SceneManager.js
| | +--BattleManager.js
| | +--PluginManager.js
| +--objects
| | +--Game_Temp.js
| | +--Game_System.js
| | +--Game_Timer.js
| | +--Game_Message.js
| | +--Game_Switches.js
| | +--Game_Variables.js
| | +--Game_SelfSwitches.js
| | +--Game_Screen.js
| | +--Game_Picture.js
| | +--Game_Item.js
| | +--Game_Action.js
| | +--Game_ActionResult.js
| | +--Game_BattlerBase.js
| | +--Game_Battler.js
| | +--Game_Actor.js
| | +--Game_Enemy.js
| | +--Game_Actors.js
| | +--Game_Unit.js
| | +--Game_Party.js
| | +--Game_Troop.js
| | +--Game_Map.js
| | +--Game_CommonEvent.js
| | +--Game_CharacterBase.js
| | +--Game_Character.js
| | +--Game_Player.js
| | +--Game_Follower.js
| | +--Game_Followers.js
| | +--Game_Vehicle.js
| | +--Game_Event.js
| | +--Game_Interpreter.js
| +--scenes
| | +--Scene_Base.js
| | +--Scene_Boot.js
| | +--Scene_Title.js
| | +--Scene_Map.js
| | +--Scene_MenuBase.js
| | +--Scene_Menu.js
| | +--Scene_ItemBase.js
| | +--Scene_Item.js
| | +--Scene_Skill.js
| | +--Scene_Equip.js
| | +--Scene_Status.js
| | +--Scene_Options.js
| | +--Scene_File.js
| | +--Scene_Save.js
| | +--Scene_Load.js
| | +--Scene_GameEnd.js
| | +--Scene_Shop.js
| | +--Scene_Name.js
| | +--Scene_Debug.js
| | +--Scene_Battle.js
| | +--Scene_Gameover.js
| +--sprites
| | +--Sprite_Base.js
| | +--Sprite_Button.js
| | +--Sprite_Character.js
| | +--Sprite_Battler.js
| | +--Sprite_Actor.js
| | +--Sprite_Enemy.js
| | +--Sprite_Animation.js
| | +--Sprite_Damage.js
| | +--Sprite_StateIcon.js
| | +--Sprite_StateOverlay.js
| | +--Sprite_Weapon.js
| | +--Sprite_Balloon.js
| | +--Sprite_Picture.js
| | +--Sprite_Timer.js
| | +--Sprite_Destination.js
| | +--Spriteset_Base.js
| | +--Spriteset_Map.js
| | +--Spriteset_Battle.js
| +--windows
| | +--Window_Base.js
| | +--Window_Selectable.js
| | +--Window_Command.js
| | +--Window_HorzCommand.js
| | +--Window_Help.js
| | +--Window_Gold.js
| | +--Window_MenuCommand.js
| | +--Window_MenuStatus.js
| | +--Window_MenuActor.js
| | +--Window_ItemCategory.js
| | +--Window_ItemList.js
| | +--Window_SkillType.js
| | +--Window_SkillStatus.js
| | +--Window_SkillList.js
| | +--Window_EquipStatus.js
| | +--Window_EquipCommand.js
| | +--Window_EquipSlot.js
| | +--Window_EquipItem.js
| | +--Window_Status.js
| | +--Window_Options.js
| | +--Window_SavefileList.js
| | +--Window_ShopCommand.js
| | +--Window_ShopBuy.js
| | +--Window_ShopSell.js
| | +--Window_ShopNumber.js
| | +--Window_ShopStatus.js
| | +--Window_NameEdit.js
| | +--Window_NameInput.js
| | +--Window_ChoiceList.js
| | +--Window_NumberInput.js
| | +--Window_EventItem.js
| | +--Window_Message.js
| | +--Window_ScrollText.js
| | +--Window_MapName.js
| | +--Window_BattleLog.js
| | +--Window_PartyCommand.js
| | +--Window_ActorCommand.js
| | +--Window_BattleStatus.js
| | +--Window_BattleActor.js
| | +--Window_BattleEnemy.js
| | +--Window_BattleSkill.js
| | +--Window_BattleItem.js
| | +--Window_TitleCommand.js
| | +--Window_GameEnd.js
| | +--Window_DebugRange.js
| | +--Window_DebugEdit.js
| +--Engine.js
| +--Globals.js
| +--Core.js
| +--JsExtensions.js
| +--Managers.js
| +--Objects.js
| +--Scenes.js
| +--Sprites.js
| +--Windows.js
+--www(ビルドした結果を格納)
| | +--js
| | | +--rpg.js(ビルド結果)
| | | +--main.js(従来のファイル+α)
| | | +--plugin.js(従来のファイル)
| | +-- :(略)
| | +--index.html
+--scripts(ビルド用スクリプト等を格納)
+--package.json
(2015/12/10修正・追記)
当初、プロジェクトファイルを配置するフォルダはbinとしていましたが、実際にツクールからデプロイした場合、フォルダはwwwになります。
binでも問題ないと思っていたのですが、どうやらwwwにした場合はセーブデータをwww直下ではなく実行パス直下(package.jsonのある場所)へsaveフォルダを作成するようです。
ゲームエンジンにもそういう意図の処理が書かれており、これが想定された正しいパスと判断しましたので、フォルダ構成を訂正します。
フォルダはそれぞれ、rpg_xxxx.jsの内容に対応しており、中にクラス単位でファイルを格納しています1。
とりあえず、Node.jsのプロジェクトを作成し、そこにせっせことクラスごとに分割したファイルを作っていきます。
rpg_managers.jsの先頭、DataManagerで$付きのグローバル変数を初期化している箇所はGlobals.jsへ移動しておきます。
ツクールMVのNode.jsでの実行環境作りは以下の記事を参照してください(手前味噌)。
- RPGツクールMVのプロジェクトをnw.jsで実行する
http://qiita.com/RaTTiE/items/6cd640ce1f3cc0a08d98
browserifyを使ってファイルを結合する
分割したファイルの結合には、言わずと知れたbrowserifyを使います。
- browserify
http://browserify.org/
一応どういったものか大雑把に説明すると、browserifyはNode.jsのパッケージの一つで、ブラウザ上のJavaScriptでもrequireが利用できるようにソースを再構成してくれるツールです。
これを使い、分割したファイルを結合していきます。
分割したファイルを読み込むスクリプトを作成
各フォルダに存在するソースコードをひとしきりrequireするファイルを作成していきます。
require('./core/Utils.js');
require('./core/Point.js');
require('./core/Rectangle.js');
require('./core/Bitmap.js');
require('./core/Graphics.js');
:
…ちなみに、「そういう使い方じゃねーから!」という異論はあると思いますが、今回はリファクタリングが目的ではなく、(なるべく)元のソースや構造には手を入れずにソースの分割・統合することが目的なので、本来のrequireとは違う使い方をしてます。
プラグインなど、元々の作りでクラスはすべてグローバルにあることが想定されているので、下手にカプセル化しない方がいいのではないかという判断もあったりします。
後々リファクタリングすることを考えないなら、そもそもbrowserify使わなくてもいいんじゃ…という気はしないでもないです2。
一応言っときますが、間違ってもrequireとはこう使うのだとか誤解するとエラい目見るので注意してください。
あとは、それぞれのフォルダの中身をrequireしたファイルを集約するファイルを作成します。
これがエンジンのエントリポイントになります。
require('./Globals.js');
require('./JsExtensions.js');
require('./Core.js');
require('./Managers.js');
require('./Objects.js');
require('./Scenes.js');
require('./Sprites.js');
require('./Windows.js');
##が…駄目っ……!
とりあえずファイルの体裁は整いましたが、これでは全然だめだったりするので、ちょこちょこと手を加えます。
上でもちょろっと触れましたが、元々requireというのはグローバル変数汚染上等のJavaScriptの現状を是正すべく、JavaScriptのモジュール化を目的に作られた仕組みです。
上記で行っているrequireというのはただファイルを読み込んでるという訳ではなく、それぞれのファイルはローカルスコープに閉じられた状態で実行されます。
よって、上記のファイルを実行しても、それぞれのファイルで宣言されたクラスはグローバルスコープ(windowオブジェクト上)には作られません。
なので、ちょっと汚いですが、作成したクラスを明示的にwindowへ代入し、グローバル変数へと公開していきます(マサカリが飛んできそう…)。
//-----------------------------------------------------------------------------
// DataManager
//
// The static class that manages the database and game objects.
function DataManager() {
throw new Error('This is a static class');
}
// ↓の一文を追加
window.DataManager = DataManager;
:
こうすることでrequireを実行すると中のファイルが呼び出され、グローバル変数内にクラスの定義が展開されるようになります。
また、同様の理屈で$付きのグローバル変数を初期化している箇所もwindowに代入するようにしないと変数が宣言されません。
Globals.jsの内容を以下のように変更します。
window.$dataActors = null;
window.$dataClasses = null;
window.$dataSkills = null;
window.$dataItems = null;
window.$dataWeapons = null;
window.$dataArmors = null;
window.$dataEnemies = null;
window.$dataTroops = null;
window.$dataStates = null;
window.$dataAnimations = null;
window.$dataTilesets = null;
window.$dataCommonEvents = null;
window.$dataSystem = null;
window.$dataMapInfos = null;
window.$dataMap = null;
window.$gameTemp = null;
window.$gameSystem = null;
window.$gameScreen = null;
window.$gameTimer = null;
window.$gameMessage = null;
window.$gameSwitches = null;
window.$gameVariables = null;
window.$gameSelfSwitches = null;
window.$gameActors = null;
window.$gameParty = null;
window.$gameTroop = null;
window.$gameMap = null;
window.$gamePlayer = null;
window.$testEvent = null;
##実はもう一つ落とし穴が…
これで概ねOKなんですが、実はもう一つ修正の必要がある箇所があります。
というのも、このゲームエンジン、元々処理の中でrequireを呼んでいる箇所があるのです3。
どういうことかと言うと、broserifyというのは元来、requireが実行できないブラウザ環境上にrequireができるように相当する関数を定義する仕組みです。
そして、そのrequireが参照するファイルは(Node.jsで言うところの、NODE_PATHにあたるもの)、あらかじめビルド時にrequireのファイルで指定されたもののみが対象となります。
つまり、browserifyでビルドの対象としたファイル以外をrequireしようとすると呼び出すことができません。
なので、nw.jsなど、元々requireが実行できる環境でrequireを呼び出す場合、broserifyで定義するrequireとは分ける必要があります。
エンジン内でrequireを実行している箇所を、window.requireに変更します(例はInputクラス)。
/**
* @static
* @method _wrapNwjsAlert
* @private
*/
Input._wrapNwjsAlert = function() {
if (Utils.isNwjs()) {
var _alert = window.alert;
window.alert = function() {
// var gui = require('nw.gui');
var gui = window.require('nw.gui');
:
同様の箇所が6箇所ほどあるので修正します。
- SceneManagerのinitNwjsメソッド
- SceneManagerのonKeyDownメソッド
- StorageManagerのsaveToLocalFileメソッド
- StorageManagerのloadFromLocalFileメソッド
- StorageManagerのlocalFileExistsメソッド
- StorageManagerのremoveLocalFileメソッド
また、UtilsクラスのisNwjsメソッドはrequireが使えるかどうかでnw.jsで実行されているかどうかを判定しているため、この処理を変更します。
/**
* Checks whether the platform is NW.js.
*
* @static
* @method isNwjs
* @return {Boolean} True if the platform is NW.js
*/
Utils.isNwjs = function() {
// return typeof require === 'function' && typeof process === 'object';
return typeof window.require === 'function' && typeof window.process === 'object';
};
##ビルドする
以下のコマンドでゲームエンジンのビルドを行います。
scriptsフォルダにbatなりbashなり作って保存しとくといいでしょう。
browserify ../src/Engine.js -o ../www/js/rpg.js
index.htmlを修正する
作成したrpg.jsを読み込み、代わりに元々のjs読み込みを削除します。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="viewport" content="user-scalable=no">
<link rel="icon" href="icon/icon.png" type="image/png">
<link rel="apple-touch-icon" href="icon/icon.png">
<link rel="stylesheet" type="text/css" href="fonts/gamefont.css">
<title></title>
</head>
<body style="background-color: black">
<script type="text/javascript" src="js/libs/pixi.js"></script>
<script type="text/javascript" src="js/libs/fpsmeter.js"></script>
<script type="text/javascript" src="js/libs/lz-string.js"></script>
<script type="text/javascript" src="js/rpg.js"></script>
<!--
<script type="text/javascript" src="js/rpg_core.js"></script>
<script type="text/javascript" src="js/rpg_managers.js"></script>
<script type="text/javascript" src="js/rpg_objects.js"></script>
<script type="text/javascript" src="js/rpg_scenes.js"></script>
<script type="text/javascript" src="js/rpg_sprites.js"></script>
<script type="text/javascript" src="js/rpg_windows.js"></script>
-->
<script type="text/javascript" src="js/plugins.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</body>
</html>
#動かしてみる
nw.jsを実行し、wwwに配置したゲームが起動すれば成功です。
nw
#まとめ
これで、ゲームエンジンのソースをクラスごとに分割し、同等の動作をする環境ができあがりました。
プラグインを介さず、ゲームエンジンを直接改造する場合も、プラグインを作成するために処理を追う場合でも、こちらのほうが幾分作業しやすいのではないかと思います。
plugins.jsは消さずに残してあるので、ツクール側からプラグイン設定しても中身は壊れないはず。…実は確認してないけど。
個人的にはRPGツクールのゲームエンジンからRPG部分を取っ払って別のゲーム作る環境作ったり、マップチップとかスプライトの仕様を根本からいじったりしてみたい(できるとは言ってない)けど、元のゲームエンジンのメモリ問題絡みでまだ改善が入りそうなので、今のバージョンをベースにあれこれするのは時期尚早な可能性も…。
ともあれ、何かの参考になれば幸いです。