RPGツクールMVのランタイムコードを読む - Sceneを理解する

ゲームを作るよりランタイムコードを読んだりプラグインを作るほうが楽しい。 この記事ではランタイムコードを読んだメモとして、プラグイン作者向けにSceneオブジェクトがどのような動きをするのかを解説します。
今後のアップデートで役に立たなくなるかもしれません。 メモの本体はgithubにあります。

github.com

Sceneクラスを理解する

3行で

  • ツクールで作られたゲームは、Sceneオブジェクトがゲームの状態を毎フレームごとに更新することで進行する。
  • Sceneオブジェクトは現在の場面に沿った処理を行い、次にどのSceneに遷移するかを知っている。
  • ゲーム中にアクティブになれるSceneは同時にただ1つだけで、SceneManagerが切り替え処理などを管理する。

SceneManger

グローバル名前空間に定義されたシングルトンオブジェクト.

責務

  • 現在有効なSceneの保持
  • Sceneの切り替え
  • 毎フレームごとにSceneに対して更新メッセージを送る
  • スクリプトがエラーを起こした時にダイアログを表示する

主なメソッド

  • goto(Scene): void Sceneを切り替える。
  • push(Scene): void Sceneを切り替える。gotoと異なり新しいSceneをスタックに積むので遷移先のSceneがpopメソッドで直前のSceneに戻ることができる.
  • pop(Scene): void 直前のSceneに遷移する。
  • snap(): Bitmap 現在のSceneのスクリーンショットを返す
  • snapForBackground(): Bitmap snap()で取得したスクリーンショットにブラーエフェクトをかけたものを返す

Sceneオブジェクト

ゲームを場面にそって駆動し、次にどのSceneに遷移するかを管理するオブジェクト。

Sceneオブジェクトのライフサイクル

SceneManagerの実装を読んだところ、Sceneオブジェクトのライフサイクルは下図のようになっているらしい。 コード上に明示的に書かれているわけではないのでステート名は適当なものをつけた。 しかしSceneオブジェクトはready,busy,activeの3つのプロパティしかもっていないため自身がライフサイクルのうちどの状態にあるのかを知ることができない。 プラグインを作るときには要注意。

scene_state

  • initialized 初期化直後の状態。
  • ready 開始に必要なリソースの読み込みなどが完了した状態。
  • start_transition Sceneを切り替えるアニメーションを行っている状態
  • running Sceneを実行している状態
  • stop_transition Sceneを切り替えるアニメーションを行っている状態
  • stopped 新しいSceneに画面が置き換わり破棄される準備が整った状態
  • terminated Sceneの破棄に必要な処理(ゲームウィンドウオブジェクトの破棄など)が終わった状態。

Sceneどうしの遷移図

Scene同士のつながりをイメージしやすくするため、ざっくりとした遷移図を作った。ユーザー定義イベントで任意のSceneに遷移することができるため、全ての遷移を書ききれているわけではない。 scene_transition_diagram

  • ※ (prev)とある遷移は「直前のSceneに戻る」という処理。
  • ※ "game event"はユーザーが定義したイベントによって発生する遷移。

Sceneの各クラスと拡張指針

Scene関連のクラスの概要とプラグインを作るときにどのクラスを拡張すればよいかの指針を紹介する。

Scene_Base

Sceneの抽象クラス。全Sceneで共通の処理がまとめてある。

拡張できる機能

  • 全てのSceneで毎フレームごとの処理
  • 全てのScene切替時のフェードイン・フェードアウト処理
  • ゲームオーバー判定

Scene_Boot

htmlのエントリポイントから呼び出されるScene。 システム画像の読み込み・データベースの読み込み・フォントの読み込み・重要な音声データの読み込みを待ったあとでScene_Titleに遷移させる。 ただしオプション付きで起動された場合は戦闘やマップのデバッグのために直接Scene_BattleまたはScene_Mapに遷移させる。

拡張できる機能

  • ゲーム初期化時の追加処理
  • 画像・データベース・フォント・音声データ以外のデータを先読みさせる機能
  • デバッグオプションから遷移することができるSceneを増やす

Scene_Title

タイトル画面。 Scene_Title

拡張できる機能

  • タイトル画面に独自の情報を表示させる
  • タイトル画面のメニュー項目

Scene_Map

マップ画面。 マップ上の具体的な処理(マップ描画・イベント発生判定処理・乗り物処理など)はGame_Mapに移譲している。 Scene_Map

拡張できる機能

  • 戦闘画面に遷移するときのエフェクト(画面点滅・BGMを停止する)
  • メニュー画面に遷移するときのエフェクト

Scene_MenuBase

メニュー画面の抽象クラス。背景設定と「パーティーからアクターを選ぶ機能」の実装をもつ。

拡張できる機能
  • メニュー画面の背景

Scene_Menu

メニュー画面。 Scene_Menu

拡張できる機能
  • メニューに表示される項目
  • 所持金ウィンドウの表示・非表示
  • ステータスウィンドウの表示・非表示

Scene_ItemBase

Scene_ItemとScene_Skillの親クラス。選択された「アクター」にGame_Itemを適用する実装がある。 読んでいる時にちょっと混乱したのだが、ツクールでは「道具(アイテム)」「スキル」「武器」「防具」をまとめてGame_Itemクラス1つで表現している。


ここから下で紹介するクラスは機能が薄くプラグインを作る上で見どころがないので簡単に紹介する。

Scene_Item

アイテム画面 Scene_Item

Scene_Skill

スキル画面 Scene_Skill

Scene_Equip

装備画面 Scene_Equip

Scene_Status

ステータス画面 Scene_Status

Scene_Options

オプション画面 Scene_Options

Scene_File

Scene_Save・Scene_Loadの親クラス。セーブ画面を表示して選択する処理が定義されている。

Scene_Save

セーブ画面 Scene_Save

Scene_Load

ロード画面 Scene_Load

Scene_Gameend

ゲーム終了画面 Scene_GameEnd

Scene_Shop

ショップ画面 Scene_Shop

拡張できる機能
  • ショップでの購入・売却処理(質問に答えるとおまけで安くする機能とか?)

Scene_Name

名前設定画面 Scene_Name

Scene_Debug

デバッグ画面 Scene_Debug

Scene_Battle

戦闘画面。実際の戦闘処理はBattleManagerに移譲している。 Scene_Battle

Scene_Gameover

ゲームオーバー画面 Scene_Gameover

拡張できる機能
  • ゲームオーバー時に表示する情報(死因の表示など)

次はイベント周りの実装を読みたい。

レガシーなJavaScriptコードをTypeScriptを使って整備したメモ

TypeScriptはレガシーなJavaScriptコードを安全に書き換えるのにも使えるよというメモ。

整備した目的

いちから書き直す時間をもらえない状況だったので短時間で安全に書き換える必要があった。そこでTypeScriptに書き換えてみたところ、書き換えたい処理がコンパイルエラーとして検出されたので簡単かつ安全に整備することができた。

TypeScriptを選んだ理由

  • 型宣言を明示的に追加していくことでレガシーコードを安全に把握したいため
  • 多くのエディタでサポートが充実している
  • そこそこ読めるJavaScriptを吐くのでTypeScript自体を捨てやすい

書き換えた手順と今後やりたいこと

  1. 拡張子を.js -> .tsに書き換えてコンパイルが失敗することを確認する
  2. ソースコードが依存しているグローバル変数に対してアンビエント宣言を追加していく
  3. prototype.js由来のメソッドをprototypeやグローバル変数を汚染しない関数の呼び出しに置き換える
  4. グローバル変数を外から注入するようにする
  5. 型宣言を追加し --noImplicitAnyが通るようにする
  6. テストを追加してリファクタリング
  7. TypeScriptを捨てる?

1.拡張子を.js -> .tsに書き換えてコンパイルエラーを確認した

mv legacy.js legacy.ts
tsc legacy.ts

エラーが検出されるのを確認する

legacy.ts(392,22): error TS2304: Cannot find name 'SomeGlobalSettingVar'.
legacy.ts(127,65): error TS2304: Cannot find name 'Enumerable'.
legacy.ts(337,13): error TS2304: Cannot find name 'Ajax'.
legacy.ts(392,22): error TS2304: Cannot find name 'Class'.
#...300行ぐらいエラーが出る

これらのエラーを修正していくことで安全に整備していった。

2. ソースコードが依存しているグローバル変数に対してアンビエント宣言を追加していく

TypeScriptコンパイラは宣言されていない変数にアクセスする処理をコンパイルエラーにしてくれる。 このエラーによってグローバル変数に依存する処理を検出することができた。

var id = SomeGlobalSettingVar.id; //legacy.ts(392,22): error TS2304: Cannot find name 'SomeGlobalSettingVar'.

この処理をコンパイルに通すためにはソースコードアンビエント宣言を追加する必要がある。アンビエント宣言というのはソースコード外で初期化される変数を明示的に宣言するための仕組みのことだ。 今回の整備ではグローバル変数コンパイルエラーとして検出するたびに、このアンビエント宣言をレガシーコードの先頭に追加していった。

interface SomeGlobalSettingVar {
    id: number;
}

declare var SomeGlobalSettingVar: SomeGlobalSettingVar;

こうすることで依存しているグローバル変数名とそのシグネチャを明確にすることができた。

3.prototype.js依存の処理をprototypeやグローバル変数を汚染しない形に書き換えて、呼び出し元を修正する

TypeScriptコンパイラはブラウザに組み込まれている関数の情報を持っているので、prototype.jsが組み込みオブジェクトを勝手に拡張した箇所を検出してくれる。下の例ではString.prototype.sliceは元からStringに生えているメソッドなのでコンパイルが通るが、String.prototype.succはprototype.js由来のメソッドなのでエラーになっている。

var someString = "some string";
var foo = someString.slice(0, 2); //ok
var bar = someString".succ(); //legacy.ts(9,16): error TS2339: Property 'succ' does not exist on type 'string'.

prototype.js依存の処理をグローバル変数やprototypeを汚染しない形に書き換えて、呼び出し元を修正する。

function succ(str: string): string {
    return str.slice(0, str.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
}

var bar = succ("someString");

これを繰り返すことでprototype.jsを使わないように書き換えることができた。 さらに、prototype.jsの機能のうち使っているものだけを切りだしたので全体のファイルサイズを大きく削減することができた。

4.グローバル変数を外から注入するようにする

2と3を繰り返すことでTypeScriptのコンパイルが通るようになった。 依存しているグローバル変数を全てファイルの先頭にアンビエント宣言として一箇所に列挙することができたので、それらの変数を一つにまとめて関数の引数として注入するように書き換えた。 ひとまずグローバル変数prototype.jsの依存を断つという目標を達成できたので実際の業務では一旦終わりとした。 ここから下は実際にはやっていないが今後やりたいことを列挙してみる。

5.型宣言を追加し --noImplicitAnyが通るようにする

今後レガシーコードに対してこれからも機能追加があるようなら、より整備しやすいように修正するのがいいだろう。例えば変数に型注釈を追加していって型カバレッジ?を上げていくとよりリファクタリングが安心してできるようになるはずだ。 型宣言が足りなくて暗黙的にany型になった変数があればコンパイルエラーになる--noImplicitAny オプションをつけてコンパイルが通るようにすることを目標にするのが良いと思う。

6.テストを追加してリファクタリング

ひと通りコードを把握することができたらレガシーコードを片手にひたすらテストを書いてリファクタリングもしたい。

7.TypeScriptを捨てる?

もしTypeScriptが廃れた場合は出力されたJavaScriptコードだけを保守するようにすればよい。TypeScriptが出力するJavaScriptコードはそこそこ読める。ES6コードとして出力したコードをbabelを使って開発するという手も考えられる。

リファクタリング・ウェットウェアを読んだメモ

リファクタリング・ウェットウェアを読んだのでメモ。 この本は上達するとはどういうことかについてや、効率的な学習方法について紹介する本。

自分が特に気になったのは学習の部分について。 人間の学習にもTDD的な方法論は使えるというのが発見だった。 TDDのテストに相当するのが目標で以下のように目標を設定すると良いのだとか。

  • 適切な粒度に分割する
  • 数値でassert可能な目標を設定する
  • timeoutを設定する

TDDでいうリファクタリングは自分の知っている情報から「全体像」をつかむということらしい。知識を整理するにはマインドマップを書くといいよという話だった。

f:id:ganr:20150105205356p:plain

メモ代わりにとった写真を雑に貼っておく。スキャナが欲しい。 単語を並べたマインドマップは他人から見たらカッコ良い単語を並べただけのように見えて悪い意味で意識高い感じになるね。

f:id:ganr:20150105210529j:plain

f:id:ganr:20150105210534j:plain

リファクタリング・ウェットウェア ―達人プログラマーの思考法と学習法

リファクタリング・ウェットウェア ―達人プログラマーの思考法と学習法

TypeScriptの型定義ファイルを作るときに行儀の悪いコードもコンパイルできるようにするべきか?

追記あり。(ついでにタイトルを「TypeScriptの型定義で悩んでいること」から改題) TypeScriptの型定義で悩んでいること。

前提

  • 型定義を作ろうとしているライブラリは自分が作ったものではない
  • せっかくなのでDefinitelyTypedにプルリクエストを送りたい

悩んでいること

emitter.addListeners({
  "click": function(){/* ... */},
  "keyup": function(){/* ... */}
})

上のようなキーバリューのペアで設定する引数のためにインターフェイスを定義したくなった。

interface EventsListeners {
  [event: string]: Function
}

こうすることでリスナーに関数以外のものを指定して実行時例外になってしまうコードをコンパイル時に検出できる。 しかし、これだとコンパイルが通らないケースがあることに気づいた。

class MyEventListeners {
  constructor(public click: Function);
}

var onClick = function(){ console.log("The event was raised!"); };
var listeners = new MyEventListeners(onClick);
emitter.addListeners(listeners);

行儀の悪い使い方だと思うけど、内部でfor(i in listeners) + hasOwnPropertyで回しているので動いてしまう。 両方の例をコンパイルに通すためにはObject型を指定しないといけないはず。 型をより厳しく書くために下のケースのようなコードはエラーにしてしまいたいけど、DefinitelyTypedに置くのならJavaScriptでできることは全てコンパイルを通るようにしないといけないのだろうか?

追記

この記事の内容を@vvakameさん(DefinitelyTypedのコラボレータ)に質問したところ、いくつかアドバイスをいただきました。(今回型定義を作ろうとしたファイルはeventemitter

これらはあくまでvvkameさんの場合ということで、明確な基準はないそうです。とはいえひとつの方針としてこれから型定義を作るときの参考になりますね。

あるファイルに依存するファイルを返すgulpプラグインgulp-resolve-dependentsを作った

与えられたファイルを参照しているファイル群をパイプラインに流してくれるプラグインgulp-resolve-dependentsを作った。 サンプルはこんな感じ。

var gulp = require('gulp'),
    sass = require('gulp-sass'),
    path = require('path'),
    resolveDependents = require('gulp-resolve-dependents');

 gulp.src('./src/lib/example.scss')
.pipe(resolveDependents({
      files: './src/**/*.scss',
      resolver: sassImportResolver,
      basePath: './src'
}))
.pipe(sass())
.pipe(gulp.dest('./dest/css'));

function sassImportResolver(filePath, fileContents){
  var match, result = [],
      pattern = /import "(.+?)";/mg;
  while((match = pattern.exec(fileContents)) !== null){
    result.push(path.resolve(path.dirname(filePath), match[1]));
  }
  return result;
}

gulp-resolve-dependeciesというプラグインを参考にしたのだが、これはあるファイルが依存しているファイルをパイプラインに流してくれる。

一方gulp-resolve-dependentsは逆方向に依存性を解決する。

例えば以下のような依存関係になっているとする。 依存解決前

sourceがパイプラインから流れてくると、対象となるファイル(dependent)は下の図のようになる。

依存解決後

依存の解決方法自体はこのプラグインを使う人が外から渡すようになっているので、TypeScriptやsassなど任意のプロジェクトに対して使うことができる。

自分はこのプラグインをTypeScriptのコンパイル用に使っていて、gulp-filterと組み合わせて実際にhtmlから読み込まれるTypeScriptファイルのうち、今編集したファイルが影響するものだけをコンパイルするようにしている。

TypeScript+ブラウザ向けプロジェクトのためのテンプレートエンジン Chonmage を作った

TypeScriptの勉強用に小さなテンプレートエンジンを作った。 型チェックができるmustacheを目指して作り始めたんだけど、そもそもmustacheが型にゆるふわ過ぎる仕様だったので途中で後悔した。結局mustacheの仕様からいろいろ削って何とか形にはなったので公開してみる。

こんな感じ

テンプレートエンジン Chonmage (https://github.com/ryiwamoto/chonmage) Chonmageを使ったプロジェクトのサンプル(https://github.com/ryiwamoto/chonmage-sample)

テンプレートに渡すデータのinterface

テンプレートファイル

生成されるTypeScriptコード

テンプレートの描画

Chonmageの特徴

テンプレートを取得するためのキーやテンプレートに渡した変数がインターフェースを満たしているかをコンパイル時にチェックできる

これは「特殊化されたオーバーロードと文脈依存型推論の合わせ技(http://b.hatena.ne.jp/entry/qiita.com/progre/items/6b85993af5b4a7f1e792)」をつかっている。

Chonmageではライブラリが

interface ITemplateStore{
    get(key: string): void;
}

というテンプレートをどんなキーで取得しても常にvoidを返すインターフェースを提供していて、

各テンプレートをコンパイルするごとにキー名とコンパイルされたインスタンスを紐づける形でinterfaceを拡張している。

interface ITemplateStore{
    get(key: "test"): new Chonmage.Compiled<IRenderingContext>(/*..*/);
}

これによって正しいキーでテンプレートを取得しているかをコンパイル時にチェックできるようになる。 また、renderメソッドに渡すデータが指定したinterfaceを満たしているかのチェックもできる。

出力する変数にstringかnumber型以外を渡すとコンパイルエラーにする

これには賛否両論だと思うけど、自分はデータを表示用に加工する処理をテンプレートのレンダリング時に行うのは良くないと思っている。(あと実装を楽にするためでもある…。)

Chonmageができないこと

  • テンプレートに渡すinterfaceに配列を除くジェネリクス型を定義できない
  • 変数名が存在しない場合は空文字列を出力する
  • ラムダ(mustacheの仕様にのっとってラムダをサポートすると動的にテンプレートを作って挿入できてしまうので諦めた。)
  • グローバル変数の参照
  • Partial (これは時間がいずれやりたい)

そのほか

  • TypeScriptが提供しているLanguageServiceはあくまでエディタ向けを想定して作られているようだ。そのせいか型情報を取得してもほぼ文字列として帰ってきてしまう(TypeScript.Services.MemberInfo)。なのでもっとリッチな型情報が欲しいならコンパイラソースコードからいろいろ引っ張ってくる必要がありそう。
  • なぜかclassのプロパティは型情報の取得(getTypeAtPosition)ができなかった。なぜ?
  • TypeScriptの型情報は実行時には失われてしまうので、_.templateみたいにその場でテンプレートをコンパイルをして出力するのは難しそう。
  • IntelliJのTypeScriptサポートはかなり残念。
  • Mustacheのサブセットではなく、Razor Syntaxのようなもっとプログラム側に寄った記法のほうがTypeScriptを活かせたかもしれない。
  • TypeScriptの勉強のために書いたけど、型空間と変数空間の違いとか外部モジュールと内部モジュールの使い分けとか、クラサバ両方で使うためのコードをどう書くかみたいな知見が足りていないのでTypeScriptリファレンスを読むことにする。

東京Node学園 11時限目に参加してきたメモ

これからのNodeの話をしよう

気になったところ

  • tracing API: プロファイラが面白くなるかもとのこと。
  • koa expressの作者が注力しているフレームワークだそう。これによってフレームワークの勢力図が大きく変わるかも、とあった。なんでもyieldでミドルウェアを呼び出したりなどexpressに比べてエレガントに記述できるとか。
  • realtimeライブラリ:Socket.IO 1.0は新しいPerl6であるという皮肉を紹介。乱立するrealtimeライブラリの現状がわかりやすく説明されていた。ライブラリに決定打がないときは博打をうつよりもひとつ上のレイヤーで吸収するという解決案が素晴らしかった。

browserifyことはじめ 〜その仕組みと活用〜

  • browserify vs Require.jsの比較軸としてpreproccesing vs client drivenと紹介していたのがかなりしっくりきた。
  • 前処理をStreamを使って柔軟に処理できるtransform機構について非常に詳しく紹介されていてわかりやすい。
  • 既存のプロジェクトを近代化するならdeglobalifyが便利そう。ただ(function(export){ export.hoge = ""})(window)だと動かないとかnpmとバッティングすると挙動がわかりづらいなどいくつか罠がある模様。
  • Grunt/gulpとのすみわけが難しいという問題。個人的にはAltJSの変換を除いて.js->.js以外は認めない方針のほうが運用しやすいように思った。

その他

上二つのセッション目当てに行ったのだけど、LT枠がどれも面白かった。勉強会の途中からビールが配られて驚いたけど、結果として謎のテンションで会場が盛り上がったのは良かったと思う。

browserifyがサバクラ両方で動作するコードのために作られたようだという話を聞いて、クライアントとサーバーで処理を分散させたりその上にtorrentとかbitcoin的な仕組みをのせたら面白うそうだなと思ったのが今回の感想です。 発表した皆様お疲れ様でした。