ゲームレビュー「Superflight 」 美しい浮島を滑空する洗練されたゲーム
今年のゴールデンウイークは外に出ずゲーム漬けの日々を過ごした人も多いではないだろうか。もし大作ゲームに満足したなら、小さなゲームをデザート代わりに遊んでみるのもいいかもしれない。そんなときにSuperFlightはうってつけだ。
SuperFlightは空中に浮かぶ島を滑空しながらスコアを稼ぐアクションゲームで、デベロッパーはドイツのGrizzly Games。
このゲームの特徴はゲーム性とグラフィックがともにシンプルでありながら、奥深い魅力を持つ点だ。 操作は向いている角度を上下左右に変えることだけ。たったこれだけの操作だ。しかし思った通りに飛ぶのはなかなか簡単ではない。プレーヤーは滑空しているため、曲がろうにもトンビのように緩やかな旋回しかできないし、上に角度がつきすぎると失速して墜落してしまう。ゲームを始めたばかりのころは遅い旋回を使いこなせるようになるまで少しばかり練習が必要だろう。なぜこんなにも操作が難しいのか。それはリスクとリターンを追及するSuperFlightのゲームシステムに関係がある。
SuperFlightのスコアシステムについて説明しよう。基本となるスコアは浮島に近づいて飛び続けた時間で評価される。時間が長いほどさらにボーナススコアが加算されるようになっている。そしてSuperFlightのキモとなるのが、狭い場所をくぐることで大きなスコアを稼ぐことができるシステムだ。通り抜けるオブジェクトの幅が狭ければ狭いほどボーナスは高くなる。なお、一度岩肌から離れてしまうとボーナスがリセットされるし、岩肌に激突するとゲームオーバーだ。
浮島は入り組んだ構造をしているので、内部を通り抜けることは安全ではない。しかし入り組んだ場所を通り抜けなければ高いスコアを望めない。安全とリスク秤にかけながらギリギリを飛ぶスリルがSuperFlightの魅力だ。
ユーザーが挑む浮島はランダムに自動生成されている。そのためプレーヤーはゲームを開始するたびに新しい島で遊ぶことになる。生成される島は一つとして同じ形はなく、自動生成ゆえに道が途中で行き止まりになっていることもある。プレーヤーは未知の島を観察し飛行経路を組み立ていかなければならない。生成アルゴリズムはよくできていて、形にいくつかのパターンはありつつも、細部が少し異なるだけで飛び方はまったく違うものになるため、新しい島に着くたびに新鮮な感覚を味わえる。
本作はサウンドも良くデザインされている。BGMはなく、聞こえてくるのはうるさいほどの風切り音とかすかに聞こえるホワイトノイズだけだ。少しさみしいと感じるかもしれないが、余計な音が聞こえないからこそオブジェクトに飛び込んでいく瞬間の臨場感を高めてくれる。また、スコアが加算されるときと確定するときの音が、コインが積み重なる音とレジから現金を引き出す音になっていて気持ち良い。危険を犯してスコアを高めたプレーヤーに心地よい音で報いてくれる。
リプレイ性も高い。リスクを求め、失敗すれば一瞬でゲームオーバーになってしまう本作のゲーム性だけに試行錯誤もしやすくなっている。ゲームオーバーになっても瞬時に初めからやり直せるし、島の形が気に入らなければ島を通過することで新たな島にたどり着くことができる。一つの島をじっくり攻略して最高得点を狙うもよし、スコアを稼ぎやすいロケーションを探しに放浪の旅に出るのも良い。やめどきがみつからない良作だ。現在Steamで¥310で手に入る。土日の暇つぶしにぜひおすすめだ。
レガシーなJavaScriptコードをTypeScriptを使って整備したメモ
TypeScriptはレガシーなJavaScriptコードを安全に書き換えるのにも使えるよというメモ。
整備した目的
- レガシーコードからグローバル変数への依存をなくす
- prototype.jsを使わないように安全に書き換える
いちから書き直す時間をもらえない状況だったので短時間で安全に書き換える必要があった。そこでTypeScriptに書き換えてみたところ、書き換えたい処理がコンパイルエラーとして検出されたので簡単かつ安全に整備することができた。
TypeScriptを選んだ理由
- 型宣言を明示的に追加していくことでレガシーコードを安全に把握したいため
- 多くのエディタでサポートが充実している
- そこそこ読めるJavaScriptを吐くのでTypeScript自体を捨てやすい
書き換えた手順と今後やりたいこと
- 拡張子を.js -> .tsに書き換えてコンパイルが失敗することを確認する
- ソースコードが依存しているグローバル変数に対してアンビエント宣言を追加していく
- prototype.js由来のメソッドをprototypeやグローバル変数を汚染しない関数の呼び出しに置き換える
- グローバル変数を外から注入するようにする
- 型宣言を追加し --noImplicitAnyが通るようにする
- テストを追加してリファクタリング
- 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でいうリファクタリングは自分の知っている情報から「全体像」をつかむということらしい。知識を整理するにはマインドマップを書くといいよという話だった。
メモ代わりにとった写真を雑に貼っておく。スキャナが欲しい。 単語を並べたマインドマップは他人から見たらカッコ良い単語を並べただけのように見えて悪い意味で意識高い感じになるね。
リファクタリング・ウェットウェア ―達人プログラマーの思考法と学習法
- 作者: Andy Hunt,武舎広幸,武舎るみ
- 出版社/メーカー: オライリージャパン
- 発売日: 2009/04/27
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 475回
- この商品を含むブログ (150件) を見る
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)
@ryiwamoto なかなか難儀なライブラリですね…。DefinitelyTypedに置く場合、全てのコンパイルが通るようにする必要は実はありません。お行儀のよい書き方の時だけコンパイルできる…というのでもOKです。問題があったら後から改善すればOKです。
— わかめ@TypeScriptカッコガチ (@vvakame) 2014, 11月 1
@ryiwamoto 僕が型定義ファイルを書く時は、利用側の事は考えず、ライブラリ本体のコードを読みつつそのライブラリが実際に想定している型のみを書いています。
— わかめ@TypeScriptカッコガチ (@vvakame) 2014, 11月 1
@ryiwamoto 極論、今回のブログのような場合は emitter.addListeners(<any>listeners); でOKやろ!というスタンスの型定義ファイルでもOKです。クラスのインスタンス直接渡すような行儀の悪いコード書くなオラァン!でよいかと。
— わかめ@TypeScriptカッコガチ (@vvakame) 2014, 11月 1
これらはあくまで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的な仕組みをのせたら面白うそうだなと思ったのが今回の感想です。 発表した皆様お疲れ様でした。