読者です 読者をやめる 読者になる 読者になる

エフアンダーバー

個人でのゲーム開発

ユニティちゃんをRe-パッケージング!!

Unity

味気ないサンプルに華を添えてくれる存在として当ブログでもお世話になっているユニティちゃん。 非常にありがたい存在ではあるのですが、いざパッケージをインポートしてみるといろいろと問題が多いことに気が付きます。 そこで、Unity初心者でも簡単に扱えるようにユニティちゃんを再パッケージしてみました。

ユニティちゃん

f:id:fspace:20170318174913p:plain

ユニティちゃんとは?

ユニティちゃんはUnity Technologies Japanが提供するキャラクターで、 『ユニティちゃんライセンス』という非常に緩いライセンスのもと、 3Dモデルやドット絵、ボイスなどのアセットが配布されています。

今回、再パッケージしたのは最もシンプルでよく使われている次の二つのパッケージです。

また、次のパッケージをインポートする必要がなくなります。

公式サイト:UNITY-CHAN! OFFICIAL WEBSITE

ユニティちゃんの問題点

現状、ユニティちゃんのパッケージには次のような問題があります。

  • 複数に分割されている
  • 大量に発生する警告メッセージ
  • 長いインポート時間
  • 難解かつコメントの少ないスクリプト
  • 複雑なフォルダ構造

長いインポートを待ちつつ、パッケージを順番に読み込んだ後に、大量の警告メッセージが残されているという状況には、 さすがにため息が出るので、この辺りを解消することが今回の目的です。

成果物

ダウンロード

Unity Package

GitHubより最新バージョンのパッケージをダウンロードしてください。

パッケージ名 内容 説明
UnityChan 基本アセット 3Dモデルやシェーダ、スクリプトなどのボイス以外のあらゆるアセット
UnityChan_Voices ボイス ボイスのみの追加アセット

プロジェクト

GitHubにて公開中。

github.com

ライセンス

オリジナルのパッケージに含まれるアセットやその改変物はユニティちゃんライセンスを継承する必要があるので、 ユニティちゃんライセンスが適用されます。 気を付けるべきはロゴ・ライセンス表記と営利目的の成人向けコンテンツくらいだと思います。

私が追加したものに関してはCC0にしておきます。 これは可能な限り著作権を放棄するというものです。 MITライセンスとかにすると著作権表記が必要になってしまって面倒なので。

正直どこまでがユニティちゃんライセンスの適用範囲で、どこまでが自分の著作物と言っていいのかよくわからないのですが、 私は使用に関して一切制限するつもりはないので、ユニティちゃんライセンスにだけ気を付けてお使いください。

使い方

基本アセットのUnity Packageをダウンロードし、 メニューの"Assets" > “Import Package” > “Custom Package…"からパッケージを選択してインポートします。 ボイスを利用したい場合にはボイスのUnity Packageをダウンロードして、同様の手順でインポートします。

ユニティちゃんを自分の作成したシーン上で動かしたい場合には、 “UnityChan"プレハブまたは"UnityChanDynamic"プレハブをシーンのヒエラルキービューにドラッグアンドドロップします。 "UnityChanDynamic"プレハブでは処理負荷が少し高くなりますが、髪の毛やスカート等が物理演算によって揺れます。

ユニティちゃんのモーションやポーズ、スクリプトの動作などを確認したい場合には、 “Demo"フォルダ内の各シーンを読み込むとサンプルを確認できます。

セットアップされていない生のデータを触りたい方は"Data"フォルダ内に各種データが配置してあります。 スクリプトの動作に関するドキュメントは用意していないため、詳細を確認したい場合には、 このフォルダ内のスクリプトファイルを開いて、コードやコメントを参照してください。

また使用に際して、"License"フォルダ内の各種規約をご一読ください。 “ReadMe"ファイルは一応元パッケージのものを含めていますが、内容が古くなっている部分があります。

DynamicsXXX

揺れもの関連のDynamicsXXX系のスクリプトについて、仕様がわかりづらいので簡単に説明しておきます。

元パッケージに含まれていたSpringXXX系のスクリプトは、スクリプト内の計算で簡易的に物理っぽい挙動を再現するものでした。 一方、本パッケージに含まれるDynamicsXXX系のスクリプトはUnity標準の物理エンジンを利用して物理演算をしています。 しかし、そのためにDynamicsXXX系のスクリプトは少し特殊な仕様となっています。

レイヤー設定

Unityには特定の相手と「だけ」衝突させるような機能は存在しません。 好ましくない衝突を回避するためには、レイヤーを用いて衝突相手をフィルタリングする必要があります。 このためDynamicsManagerには揺れものと専用コライダーのレイヤーを一括で設定する機能がついています。

しかし、このような設定をしなければ動かせないという仕様ではサンプルを用意できません。 また、レイヤーおよびその衝突関係の設定はプロジェクトごとの設定項目であるため、 これらの項目を勝手に書き換えるような仕様にするのも好ましくありません。

そこで、DynamicsManagerには実行時に使われていないレイヤーを自動的に検索してそれを利用する機能をつけています。 このとき、「使われていない」ということの判断には名前が設定されているかどうかを利用しており、 全てのレイヤーに名前が設定されているとエラーとなります。

ただし、この機能はサンプルを動かすための対処的なものです。 もしゲームに組み込むことを考えているのであれば、レイヤーと衝突関係を正しく設定した後にこの機能を切ることをおすすめします。

物理オブジェクトの実体

UnityにはKinematicでない剛体(Rigidbody)で階層をつくってはいけないという制限があります。 一方で、モデルのボーンは階層を成しています。 このため、各ボーンにそのまま剛体をアタッチしてしまうと制限に違反しておかしな挙動となってしまいます。

そこで、DynamicsXXX系のスクリプトでは物理演算用のゲームオブジェクトをシーンのルートに生成し、 それに剛体をアタッチすることで物理演算を行っています。 モデルのボーンにはそれらの演算結果を転写しているだけなので、 ボーンのTransformになんらかの変更を与えてもそれらは全て無視されます。

物理演算用のゲームオブジェクトは通常見えないように設定されていますが、 DynamicsManagerの設定を変更すると、エディタ上で確認できます。

リパッケージングについて

方針

再パッケージの方針というか、目指したところ。

  • パッケージは基本アセットとボイスの二つで構成
    • インポートが遅くなる最も大きな要因がボイスの容量であるため
  • 初心者でも簡単に扱えるように
    • ドラッグアンドドロップのみで使用可能に
    • どれが本体でどれが部品なのかを明確に
  • 初心者の手本となるようなソースコード
    • なるべくきれいに整理
    • コメントを多めにつける
    • 極力基本構文のみ
    • 汎用性やパフォーマンスよりも可読性優先

スクリプト

スクリプトは元々のパッケージに含まれているものと同等の機能を持つ新しいスクリプトへとすべて書き直しました (一応元コードを参考にして書きましたがほとんど原形はないです)。

スクリプトを書き直すにあたって、 初心者でも頑張れば読めるくらいのコードにしようと考えたのですが、 Unityの入門書にどの程度C#の構文について書いてあるのかわからなかったため、ほとんど基本構文のみで書いています。

プロパティ、インターフェース、Unity定義クラス以外の継承、デリゲート、LINQあたりは意図的に避けています。 そのためC#に慣れ親しんだ人にはちょっと不格好なコードに見えるかもしれません。

一方で、条件演算子(~?~:~)、列挙型(enum)、構造体(struct)あたりはコードを読みやすくするために、 属性(~Attribute)、ifディレクティブ(#if/#endif)あたりはエディタの利便性のために許容しています。 読んでいて意味がわからなかった場合には各自調べてください。

・・・とまあ、こだわっている風ではありますが、 実際には一部のスクリプトはそこそこ難しく、おそらく初心者には読めないだろうなと思います。 制約を貫いているのは単に意地になっているだけですね。 とりあえず、C#っぽくないコードになってるのはこういう理由だよ、ということだけ。

対応表

各スクリプトの対応と相違点。

元スクリプト 新スクリプト 相違点
AutoBlink AutoBlink
BlinkablePart
対象の指定をスクリプトをアタッチする方式に。
CameraControl ViewingModeCamera
FaceUpdate FacialAnimator レイヤー指定が必要に。
IdleChanger MotionSwitcher 操作方法を上下から左右に。
IKCtrlRightHand IKTarget 右手以外にも切り替え可能に。
RandomWind RandomWind
SpringBone DynamicsRoot
DynamicsBone
スクリプトのアタッチ対象が末端に向けてひとつスライド、根本に専用スクリプトが必要に。
SpringCollider DynamicsCollider 形状が球からカプセルに。
SpringManager DynamicsManager スクリプト内での計算から標準の物理エンジンを使用する方式に。
ThirdPersonCamera ThirdPersonCamera 位置の指定がTransformの参照から相対座標値に。
UnityChanControl-
ScriptWithRigidBody
UnityChanController
UnityChanStateController
StateMachineBehaviourを利用する方式に。その他全体的に変更。

変更点一覧

とりあえずメモしておいたものだけ。 何か思い出したら追記します。

  • 各モデルのノードを適切な名前に変更
  • 各モデルのアニメーションスタックを適切な名前に変更
  • 各モデルのブレンドシェイプを適切な名前に変更
  • “unitychan_all"アバターマスクを作成し、各アニメーションのマスク設定に適用
  • 各アニメーションモデルのRigに"unitychan.fbx"のアバターを適用
  • “BoxUnityChan.fbx"のRigに"unitychan.fbx"のアバターを適用
  • “Character1_Spine"に設定されていたアニメーションを削除
  • “JUMP00"アニメーションのカーブ"JumpHeight"と"GravityControl"を削除
  • “UnityChanActionCheck/ARPose"アニメーターコントローラーのパラメータ"Back"と"Next"をBoolからTriggerへ変更
  • “UnityChanLocomotions"アニメーターコントローラーの構造を変更
  • “UnityChanLocomotions"アニメーターコントローラーのジャンプモーションを"JUMP00"から"JUMP00B"へ変更
  • “unitychan_dynamic"の"J_L_SusoFront_01"のTransformがリセットされている問題を修正
  • ステージのマテリアルをシンプルなものに変更
  • 古いシェーダーが適用されていた"BoxUnityChan.fbx"のマテリアルを再インポート
  • 重複していたマテリアルを削除
  • “CreateLocatorHere"エディタ拡張を削除
  • 各プレハブから"LookPos"を削除
  • 不要になったプレハブを削除
  • スプラッシュスクリーン用のアセット群を削除
  • 内容が正しくなくなった一部のドキュメントを削除
  • ライセンス関係のファイルを最新のものに更新
  • すべてのテキストファイルを改行コードLFのBOM付UTF-8に統一
  • 全スクリプトを書き直し
  • 一部アセットの名前を変更

ユニティちゃんライセンス

ユニティちゃんライセンス

この作品はユニティちゃんライセンス条項の元に提供されています

おわりに

元々少し整理するだけだったはずなのですが、いつの間にやら大改修に・・・。 おかげでブログの更新も滞って、ネタがたまってきたのでこれから少しずつ放出していくつもりです。

ブログ更新の通知用にTwitter始めようと思います。 SNS使いこなせない人間なので、以前アカウントだけ作って挫折したのですが、今なら最悪ブログの更新だけでもつぶやけるので。 アカウントについてはプロフィールの部分に書いておきます。

パッケージに関して質問やバグ報告等あればいつでもどうぞ。



執筆時のUnityのバージョン:5.5.2f1

TypeScriptでRPGツクールMVプラグイン

RPGツクール

RPGツクールMVになってからプラグインの言語がJavaScriptになりました。 しかし、JavaScriptは静的解析がしづらく、開発ツールによるサポートが効きづらいのは誰もが知るところ。 そこで、JavaScriptの代替言語AltJSの中でも人気の高いTypeScriptでツクールプラグイン開発をする環境を整えてみました。

ついでにツクールのシリアライザを置き換えて、より多様な形式のオブジェクトをシリアライズできるようにしました。

TypeScriptとは?

f:id:fspace:20161202181938p:plain

TypeScriptはJavaScriptの代替言語AltJS(Altanative JavaScript)のうちの一つです。

JavaScriptはすべてのブラウザがサポートする、Web上唯一のプログラミング言語です。 しかし、現在主流のプログラミング言語の内ではやや特殊な仕様を持つことや動的型付け言語でツールのサポートが受けづらいこともあり、 JavaScriptは大規模Webアプリケーション開発には不向きでした。 そんな中、JavaScriptを置き換える言語として開発されたプログラミング言語群をAltJSと言います。

初めのうちは各ブラウザに直接実行されることを目指したAltJS言語もあったのですが、 各主要ブラウザが息を合わせて新しい言語をサポートするというのはなかなか難しかったようで、 現在はJavaScriptにコンパイルする形式が主流となっています。 そのような経緯の中、まったく別の言語ではなく、 JavaScriptを拡張したような言語仕様であったTypeScriptは比較的高い人気を誇っています。

TypeScriptはMicrosoftが開発した言語で、同じくMicrosoftが開発した言語であるC#の生みの親が開発に関わっていることで有名です。 文法もC#erにとって馴染みやすいものとなっており、それもまた人気の理由の一つです。

一方、JavaScriptに対する歩み寄りの姿勢も強く、ES6やES7の仕様を早いうちから取り込んでいます。 うれしいことに、TypeScriptではES6やES7の仕様に沿った文法で書いたコードを対応するES5のコードにコンパイルすることができます。 ツクールMVの環境はES5相当なので、プラグイン開発ではこの機能を活用します。

名前空間とモジュール

TypeScriptには大きく二つの開発形式があります。 ひとつは名前空間を用いたもの、もうひとつはモジュールを用いたものです。

名前空間は昔から使われているプログラムの管理方法です。 JavaScript自体には名前空間の機能はありませんが、オブジェクトを用いて同等の機能を得る手法は一般的によく用いられています。 TypeScriptではこの名前空間が言語仕様に取り込まれています。 当然ですが、コンパイル結果は従来JavaScriptで用いられてきた手法によるものになります。

モジュールは最近(というほどでもないが)JavaScriptで流行りの管理方法で、ES6の仕様にも採用された方式です。 名前空間が道具を整理して保管するためのものなら、モジュールは道具箱から逐一取り出して使うイメージです。 モジュールはTypeScriptの文法に取り入れられているものの、 ES5以前で使うためにはモジュールローダと呼ばれるスクリプトが必要となります。

ツクールMVの環境はES5相当なので、基本的には名前空間の方が楽なのですが、 今回はモジュールを扱えるようにしてみます。 理由は、モジュールでは依存関係が扱えるためです。 プラグインという形態である以上、スクリプトの配置はユーザに委ねるしかなく、 ここにプラグイン同士の順番についての制約を与えることはできる限り避けたいのです。 そのため、配置順に依らず依存先のプラグインが先に実行されるようにモジュールによる実装を採用します。 ただし、モジュールローダが必要となるため、この部分だけは配置順に制約がかかります。

また、現時点ではプラグインとして配置されていないスクリプトの動的読み込みには対応していません。 これはユーザに対して実行されるスクリプトの存在を明確に示すためにそうしたのですが、 単なるライブラリをプラグインとして配置する手間がかかるので、変更すべきかもしれません。

開発環境の構築

TypeScriptによる開発環境の構築例について説明していきます。

あくまでも一例なので、TypeScriptの設定くらい自分でできるよ、という人は適宜飛ばしながら読んでください。

TypeScriptとVSCodeの設定

TypeScriptがMicrosoft製なので、 Microsoft製のテキストエディタであるVSCode(Visual Studio Code)はTypeScriptに対するサポートが厚いです。 ということで、ここではVSCodeの使用を前提に話を進めていきます。 無料なので、インストールしていない方は公式サイトよりダウンロードしてください。

フォルダ構成

ここではRPGツクールのプロジェクトフォルダ下にtsフォルダを作成し、その内部に開発用のファイルを配置します。

フォルダ構成は次の通り。

  • プロジェクトフォルダ
    • ts
      • (.vscode)
        • 自動生成されるVSCodeの設定フォルダ
      • decl
        • 型定義ファイル(*.d.ts)
      • src
        • TypeScriptソースファイル(*.ts)
      • tsconfig.json
        • TypeScriptの設定ファイル
      • tsconfig-debug.json
        • TypeScriptの設定ファイル(デバッグ用)
    • js
    • その他(*.rpgproject, index.html, 各種フォルダ)

tsconfig.json

tsconfig.jsonはTypeScriptのコンパイラ(tsc)に対する引数をまとめた設定ファイルです。 コンパイル時にこのファイルを指定することでまとめて設定できるようにします。

{
    "compilerOptions": {
        "target": "es5",
        "module": "system",
        "baseUrl": "src",
        "outDir": "../js/plugins",
        "inlineSourceMap": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "removeComments": true,
        "experimentalDecorators": true
    },
    "include": [
        "src/**/*",
        "decl/**/*"
    ]
}

設定の意味は次の通り。

設定項目 重要度 説明
target 必須 出力のJavaScriptバージョン。
module 必須 出力のモジュール仕様。
baseUrl 必須 モジュール探索のルートURL。
outDir 必須 出力先のディレクトリ。
(inlineSourceMap) 推奨 ソースマップ(入力-TypeScript-と出力-JavaScript-のコード位置の対応表)を出力ファイル内に埋め込むかどうか。
noFallthroughCases
InSwitch
任意 switch文にてフォールスルー(breakせずに後続のcaseに移行すること)をエラーとするかどうか。
noImplicitAny 任意 暗黙的なany型(型チェックされない型)をエラーとするかどうか。
noImplicitThis 任意 関数(メソッドでない)内にて型指定なしのthisをエラーとするかどうか。
strictNullChecks 任意 undefinedとnullを型として厳密にチェックするかどうか。
removeComments 任意 コメントを自動的に削除するかどうか。
experimentalDecorators 推奨 試験的に導入されているデコレータの機能を有効にするかどうか。

tsconfig.jsonとtsconfig-debug.jsonの設定内容はほぼ同じですが、 tsconfig-debug.jsonの方でのみinlineSourceMapなどのソースマップに関する設定を有効にしておくと、 TypeScriptのソースコードでデバッグができるようになります。

「任意」と書いたオプションについて、もし意味がわからないのであればすべて無効にしておくといいと思います。 これらはそれぞれ厳密な書き方を強制するためのもので、意図を知らずに遵守するのはなかなかに苦痛ですので。

removeCommentsはソースコード中のコメントを削除しますが、/*!...*/と!をつけた形式で書かれたブロックコメントは削除されません。 これを利用して/*!/*:...*/という形式でプラグインコメントを書くと、削除されずにプラグインの説明を書くことができます (ツクールの雑な寛容な処理のおかげで助かった)。

experimentalDecoratorsは必須ではありませんが、 いくつかの機能をデコレータできれいに書けるようにしているため、 基本的には有効にしておいてください。

tasks.json

TypeScriptのコンパイラ(tsc)を直接実行してもいいのですが、 VSCode上からより簡単に実行するためにタスクを定義します。 そのための定義ファイルがtasks.jsonで、.vscodeフォルダ内に配置されます。

tsフォルダをVSCodeで開いた状態で、Ctrl+Shift+P(Windowsの場合)でコマンドパレットを開き、 Configure Task Runnerコマンドを実行すると、自動的にtasks.jsonが生成されます。 これを次のように書き換えます。

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "0.1.0",
    "command": "tsc",
    "isShellCommand": true,
    "showOutput": "silent",
    "tasks": [
        {
            "taskName": "debug",
            "args": ["-p", "./tsconfig-debug.json"],
            "suppressTaskName": true,
            "problemMatcher": "$tsc",
            "isBuildCommand": true
        },
        {
            "taskName": "release",
            "args": ["-p", "./tsconfig.json"],
            "suppressTaskName": true,
            "problemMatcher": "$tsc"
        }
    ]
}

あとはコマンドパレットでRun Taskコマンドを実行し、debugかreleaseかを選択すれば、 それぞれの設定でソースコードがコンパイルされます。 また、コマンドパレットかCtrl+Shift+B(Windowsの場合)でRun Build Taskコマンドを実行すると、 debug設定でコンパイルされます。これはisBuildCommandオプションがdebugに設定されているためです。

コアスクリプトの型定義ファイル

コアスクリプトの型定義ファイルについて、 とりあえずWeb上を探してみたところ公開してくれている方がいたのですが、 入れてみると設定が悪かったのかエラーが大量に発生したので、TypeScriptの学習も兼ねて自分で作成しました。 さすがにコードをがっつり追うのは厳しかったのでnullabilityあたりはかなり怪しいですが、公開しておくのでご自由にお使いください。

github.com

注意点として、lib.d.tsに変更を加えたものが含まれるため、その他のlib系ファイルは使えません。 これはRPGツクールのWindowクラスとグローバルオブジェクトとしてのWindowクラスの競合を防ぐためです。 どうしても他の定義ファイルを使いたい場合には同様の変更を施して置き換えてください。

前述の方法でTypeScriptの環境構築をした方は、declフォルダ内に型定義ファイル(*.d.ts)をすべて放り込んでください。 declフォルダ内の構成はどのようになっていてもかまいません。

モジュールローダ

モジュールローダはES6 Micro Loaderの実装を参考に簡易的なものを自作しました。 モジュールを動的に読み込むことはしないので、元ソースコードからロード機能を削って、TypeScriptで書き直したような代物です (ロードしてないのでモジュールローダというよりモジュールマネージャですね)。

Gist: PluginSystem.ts

System.register形式なので、TypeScriptコンパイラのmodule設定をsystemにしておく必要があります。 動的な依存解決ができる形式としてAMDにしようかとも思ったのですが、 AMDだと関数定義を文字列として解析する必要があるようなのでやめました。

モジュールローダプラグインは他のどのプラグインよりも先に実行される必要があります。 ツクールのプラグイン設定で必ず最初に配置するようにしてください。

前述の方法でTypeScriptの環境構築をした方は、srcフォルダ内にPluginSystem.tsファイルを配置すると型情報を使うことができます。 PluginSystem.ts自体をコンパイル対象にしたくない場合には、TypeScriptコンパイラを使用して型定義ファイルを生成し、 それをdeclフォルダ内に配置してください。

プラグインの記述

ここまでの設定でTypeScriptによるプラグイン開発は可能ですが、 より簡単に書くために、以降では、モジュールローダプラグインのプラグイン開発支援機能について説明します。

TypeScriptの構文に関する説明はここではしないので、公式のドキュメントやWeb上の記事を参照してください。

プラグイン情報の取得

各種プラグイン情報にアクセスするためのプラグインオブジェクトを取得するにはMVPlugin.get静的メソッドを使用します。 MVPlugin.getメソッドはプラグイン名を引数に、その名前のプラグインオブジェクトを返します。

let plugin = MVPlugin.get(__moduleName);

__moduleNameというのはモジュール内からそのモジュールの名前を取得するための変数で、 特別な設定がなされていない限りはプラグイン名が格納されています。

パラメータの取得

プラグインパラメータの取得にはプラグインオブジェクトを使用します。

let plugin = MVPlugin.get(__moduleName);
let params = plugin.parameters.value;

parametersプロパティで取得できるのはパラメータオブジェクトで、 単なる連想配列を得たい場合にはさらにvalueプロパティを参照します。

パラメータオブジェクトには手軽に値を取得するためのメソッドがいくつか用意されています。

let intValue = plugin.parameters.int('XXXCount');

プラグインオブジェクトのvalidateメソッドを使うと、パラメータのバリデーションがシンプルに書けます。

let plugin = MVPlugin.get(__moduleName);
let params = plugin.validate($ => {
    return {
        intValue: $.int('IntParam'),
        floatValue: $.float('FloatParam'),
        boolValue: $.bool('BoolParam'),
        stringValue: $.string('StringParam'),
        UPPER_VALUE: $.upper('UpperParam'),
        lower_value: $.lower('LowerParam'),
    };
});

コアスクリプトの書き換え

ツクールの基本的な拡張スタイルはメソッドの置き換えなので、次のような形式のコードを書くことが多いと思います。

var _Foo_bar = Foo.prototype.bar;
Foo.prototype.bar = function() {
    _Foo_bar.call(this);

    ...
};

TypeScriptでもこの書き方はできるのですが、 せっかくクラスベースで書いている中にprototypeが出てくるのは嫌なので、デコレータを使った代替記法を用意しました。 ただし、デコレータを利用するにはクラスを定義する必要があるため、コード量は少し増えます。

拡張クラスの定義

まずは拡張のためのクラスを定義します。 "~Extensions"のような名前をつけたクラスにMVPlugin.extensionデコレータを付けます。

@MVPlugin.extension(Foo)
class FooExtensions {}

MVPlugin.extensionの第一引数は拡張対象クラスのコンストラクタです。 このため、拡張クラスは対象クラスごとにひとつずつ定義する必要があります。 また、静的メソッドを置き換えたい場合にはMVPlugin.extensionの第二引数にtrueを設定します。

@MVPlugin.extension(Foo, true)
class FooStaticExtensions {}

特定条件下でのみ置き換えたい場合にはMVPlugin.extensionIfを使用してください。

@MVPlugin.extensionIf(Foo, plugin.parameters.bool('Enabled'))
class FooStaticExtensions {}

メソッドを置き換えるには拡張クラス内に次のようなメソッドを定義します。

@MVPlugin.method
public static bar(base: Function) {
    return function(this: Foo) : void {
        base.call(this);

        ...
    };
}

注意すべき点は四つ。

  • @MVPlugin.methodデコレータを付けること。
  • 静的メソッドであること。
  • 置き換える対象と同じ名前のメソッドであること。
  • 置き換える処理の関数を返すこと。

メソッドの引数baseはもともと定義されていた関数です。 これに対してcallapplyを呼ぶことで、元の処理を実行することができます。 拡張対象クラス内に指定した名前のメソッドが存在しない場合には、この値はundefinedとなり、 単純に新しいメソッドが定義されることになります。

プロパティを置き換えたい場合にはMVPlugin.propertyデコレータを使用します。

@MVPlugin.property
public static get baz(this: Foo): number { return this._baz; }
public static set baz(this: Foo, value: number) { this._baz = value; }

プロパティの場合には元の処理を参照することはできません。

また特定の条件下でのみ置き換えたい場合には、MVPlugin.methodIfデコレータやMVPlugin.propertyIfデコレータも使用できます。

型定義の変更

拡張のために新しいメンバーを定義した場合には、型定義を変更する必要があります。 型定義の変更は次のようなコードで行います。

declare global {
    interface Foo {
        _baz: number;
        baz: number;
    }
}

モジュール内でグローバル空間の型定義を変更する場合には、 declare globalでグローバル空間に定義されていることを明示する必要があります。 また、定義済みクラスのインターフェースを拡張する場合にはclassキーワードではなくinterfaceキーワードを使います。

シリアライズ

ツクール標準のシリアライズ機能では、グローバル空間に定義されたクラス以外はプロトタイプオブジェクトが保持されません。 そのため、モジュールに書いたクラスをシリアライズしようとすると型情報が抜け落ちてしまいます。

この問題を解決するため、モジュールローダプラグインを実行すると、 自動的にシリアライズ処理が独自のものに切り替わります。 グローバル空間以外のクラスの型情報が復元できるほか、 循環参照の保持やシリアライズ・デシリアライズイベントの取得ができるようになります。

なお、元のシリアライズ方式との互換性はないためご注意ください。

型情報の登録

グローバル空間以外に定義されたクラスの型情報を復元するには、 あらかじめ型情報を登録しておく必要があります。 登録にはSystem.Typeクラスのregister静的メソッドを使用します。

class Foo {}
System.Type.register(Foo, 'HogeModule');

第一引数がクラスのコンストラクタで、第二引数がモジュール名です。 通常はコンストラクタの関数名がIDとして用いられますが、第三引数にIDを指定するとそちらが優先されます。

また、プラグインオブジェクトとデコレータを用いた指定もできます。

@plugin.type
class Foo {}

IDを指定したい場合にはtypeAsデコレータを使用します。

@plugin.typeAs('FOO')
class Foo {}

イベントハンドラ

クラスに特定の名前のメソッドを定義しておくと、対応するタイミングで呼び出されます。

シグネチャ 説明
onSerialize():void シリアライズ直前に呼び出されます。
onDeserialize():void デシリアライズ直後に呼び出されます。
toJSON():any シリアライズ時に呼び出され、戻り値をオブジェクトの代わりにシリアライズします。
fromJSON(data:any):void デシリアライズ時に呼び出され、引数にデシリアライズしたデータが渡されます。

fromJSONが定義されている場合、オブジェクトはクラスのプロトタイプオブジェクトがセットされただけの状態で初期化されます。 fromJSONにてthisに値を代入し、オブジェクトを復元してください。

シリアライズ形式

各オブジェクトはオブジェクトテーブルに分離して保存されることで参照を保持します。

シリアライズ後のJSONデータは次のような形式となります。

{
    root: { "@ref": 0 },
    table: [
        {
            "@": "@HogeModule.Foo",
            "_": {
                "_baz": 100,
                "_fuga": { "@ref": 1 }
            }
        },
        ...
    ]
}

記述例

記述例。処理に特に意味はないです。

/*!
/*:
 * @plugindesc サンプルプラグイン
 * @author F_
 *
 * @param MyParam
 * @desc 私のパラム。
 * @default True
 */

import { Foo } from 'HogeModule';

let plugin = MVPlugin.get(__moduleName);
let params = plugin.validate($ => {
  return {
    mine: $.bool('MyParam')
  };
});

declare global {
  interface Game_Map {
    readonly mine: MyClass;
  }
}

@plugin.type
export class MyClass {
  public constructor(public mine?: boolean);
  public sweep():void { delete this.mine; }
}

@MVPlugin.extension(Game_Map)
class GameMapExtensions {
  @MVPlugin.property
  public static get mine(this: Game_Map): MyClass { return new MyClass(params.mine); }

  @MVPlugin.method
  public static checkPassage(base: Function) {
    return function(this: Game_Map, x: number, y: number, bit: number): boolean {
      this.mine.sweep();
      return base.call(this, x, y, bit);
    };
  }
}

let foo = new Foo();
foo.bar();

前回記事のプラグインでも利用しているので、そちらもご参照ください。

www.f-sp.com

おわりに

せっかく環境整えたのでとりあえず公開してみたのですが、 こうまでしてTypeScript使いたい人ってどれくらいいるんでしょう? 他人のスクリプト利用してまで使おうなんて人は限りなく0に近いような・・・。

まあ、読み物として楽しんでもらえたなら幸いです。



執筆時のRPGツクールMVのバージョン:1.3.4

続・RPGツクールでローグライクな雰囲気を醸してみた

RPGツクール

なんやかんやでまた書き始めたRPGツクールMVのローグライクプラグイン。

www.f-sp.com

JavaScriptでの整理が難しくなったのでTypeScriptに切り替えたり、 循環参照を保つためにシリアライザを置き換えたりしていたのですが、 それでもなお拡張に限界を感じたため、 とりあえずここまでの成果を曝して、また無期限延期とすることにしました。

ああ、やっぱり今回も駄目だったよ。

進捗

基本的に内部をいじっていたので、見てわかる変化は階段と敵が増えたくらいです。 機能としては階層管理機能とターン制の導入になります。

f:id:fspace:20161201153300g:plain
階層移動

f:id:fspace:20161201153301g:plain
ターン制

仕様

一応プラグインとして作る以上、エディタから設定しやすいものにしたいな、と思いいろいろと考えていました。

階層管理

階層管理(どの階層にどういったフロアを割り当てるか)は専用のタグ<blueprint>を設定したマップを作成することでできるようにしています。

二次元のマップの左上から順に一次元のインデックスを割り当て、階段などで特定階層へと移動する際には、 移動先階層と一致するインデックスの位置に設定されたイベントのコマンドを実行するという仕様になっています。 ここに特定マップへと移動するコマンドを書き込んでおけば、階層とフロア設定が関連付けられるという仕組みになっています。 また、イベントコマンドならなんでも書き込めるので、体力回復やダンジョンでない通常のマップへの移動などもできます。

インデックスの0番(一番左上)は特殊な番号として扱っていて、 現在は階層管理マップに直接移動したときのダンジョン初期化用として使っています。 現状未実装ですが、ひとつのイベントには複数のイベントページを作成できるので、 階層管理マップ範囲外のインデックスにアクセスした際のデフォルトコマンドの設定等への活用も考えていました。 これを実装すると、ダンジョン最後の階層の階段を上った際に実行するコマンドの設定ができるようになります。

f:id:fspace:20161201153258p:plain
全8階層の階層管理マップ

フロア管理

フロア管理(フロアのデザインや敵の設定等)もまた専用のタグ<dungeon>を設定したマップを作成することでできるようにしています。

マップ上の好きな場所に対応する設定を施した上で、専用のタグを設定したイベントを配置することで設定が読み込まれます。 例えば、フロアの壁に使用するタイルを設定したい場合には、好きな場所に壁用のタイルを設定し、その場所に空のイベントを配置、 イベントのメモ欄に<wall>タグを設定することで、そのタイルがダンジョン生成時に壁となった場所に配置されます。 敵や階段の設定も同様で、好きな場所に配置したイベントに<enemy>タグや<stairs>タグを設定し、適当な画像を設定しておくと、 そのイベントが自動的に複製されて、ダンジョン内に配置されます。

また、生成されるダンジョンの大きさはフロア管理マップの大きさと同じになります。 ダンジョン生成に使われるアルゴリズムもフロア管理マップにタグをつけることで切り替えられるような仕組みになっています (現状テスト用のシンプルなアルゴリズムひとつしかありませんが)。

f:id:fspace:20161201153259p:plain
森っぽいフロア管理マップ

拡張の限界

言い訳という名の愚痴。

階層やフロアの管理などをつくっている段階まではまだよかったのですが、 敵を表示するためにイベントを動的に生成し始めたあたりから管理に限界を感じ始めたため、実装を中止することにしました。

ツクールをローグライク化するためには、なかなかに大規模な変更を加える必要があるため、 直接コアエンジンのクラスを拡張していくと他のプラグインとの競合が発生しやすくなってしまいます。 そこで、今回はローグライク関連のモデルをツクールとは独立で作成し、 そのモデルに合わせてツクール側の要素を調整することでローグライク化を実現しようとしていました。 が、結局このツクール側の要素を調整するという部分が非常に面倒だったのです。

基本的にツクールはツクール自体の仕様を実現するためだけにできており、拡張性はほとんど考慮されていません。 具体的な例を挙げると、イベントの動的生成ができません。 マップにイベントを追加する機能がないのはもちろん、 そもそもイベントの内容はすべてグローバル変数として読み込まれているデータを直接参照していて、新たな挙動のイベントインスタンスが作れません。 グローバル変数のイベントデータはシリアライズされないので、ここにイベントデータを追加してもセーブ&ロードで消えてしまいます。 また、クラスの分割単位が非常にでかいので、同じインターフェースのクラスをつくって誤魔化すことも現実的ではありません。 さらには、これらの問題を解決してイベントインスタンスを生成したとしても、 マップの初期化がすでに終わっているので、プライベート変数を繰り、スプライトの追加等々の調整をする必要があります。

・・・とまあ、何かしようとするたびにやたらとコストがかかるのです。 そして変更に弱いというリスクも抱えます。

もともと想定されていない機能を盛り込むわけなので、もちろんある程度のコストは仕方のないことなのですが、 もう少しオブジェクト指向っぽい設計になっていればなぁ、と思うことがたびたび。 さすがにコスパが悪いのでここらで切り上げることにしました。

ソースコード

前回と同じとこに置いてあります。

github.com

ライセンスはMITです。

おわりに

今回、とりあえず一旦休止しますが、もしもいい設計案とか思いついたらまた始めるかもしれません。 前回も、もうやめるとか言っておきながらやってますので・・・。

次回の記事は、冒頭で述べたTypeScriptやらシリアライザやらの話になる予定です。



執筆時のRPGツクールMVのバージョン:1.3.4