2015年11月05日の日記です


jsdeferred でアニメーション  2015-11-05 17:29:41  コンピュータ

お手伝いしているゲーム作成での話。


いわゆるソーシャルゲームで、スマホ端末を使って Javascript で画面などを動かしている。

ゲームエンジン自体はサーバー側で持っていて、端末側は結果をそれらしく見せるだけ。


Javascript 側では、「それらしく見せる」ために、jsdeferred を使っている。

最近の流行というか、普通に使われる書き方ね。



next(function(){
 処理 A
 return wait(1.0)
}).
next(function(){
 処理 B
 return wait(1.0)
}).
next(function(){
 処理 C
 return wait(1.0)
});


とか書くと、処理 A の 1 秒後に処理 B を、さらに1秒後に処理 C を行ってくれる。


なんでこんな書き方するのか、については言及しない。

上のような処理を Javascript で普通に書いていると、非常に面倒くさいことになるのだ、とだけ言っておこう。




僕ではない誰かが作ったアニメのファイルを改造してほしい、と頼まれた。

特定の状況で、今までにはない別のアニメを割り込ませる。


上に書いたように、next で処理をつなぐ書き方なら、割り込みは難しくなさそう、に思える。



でも、実際にはそうではない。

かなりややこしい呼出し手順…古い言い方なら「スパゲティプログラム」になっていた。



function 処理A {
 ~~~
 処理B();
}

function 処理B {
 ~~~
 処理C();
}

function 処理C {
 ~~~
 処理D();
}


まず、概略としてはこんな感じになっている。

~~~で示してある部分は、いろんな処理が入っていると思って。もちろん next をつないである。

そして、最後の next の中で、次の処理を呼び出している。



なんでひとつながりの next にしないで、ある程度のまとまりで関数化してあるかというと、アニメは「タップで飛ばす」ことができるからだ。

処理 A の最中にタップされると、処理 A の部分のアニメの残り部分は飛ばされて、処理 B の部分から始まる。


その処理のために、別の関数が用意してある。



function タップ {
 switch(シーン番号){
 case 0:
  処理A の終了処理
  処理B();
  break;
 case 1:
  処理B の終了処理
  処理C();
  break;
 case 2:
  処理C の終了処理
  処理D();
  break;
 }
}


あぁ、なるほどなるほど。タップしたらスキップするのだから、そういうことになるわな。


…でも、このプログラムは実はおかしい。


ここで「終了処理」と書いてあるのは、アニメの最後の状態に持っていく、というだけ。

処理 A の終了処理であれば、強制的に処理 A のアニメの最後の状態にする。


でも、処理 A のプログラムは動き続ける。next を使っていると、一定時間後には次の部分が動いてしまうから。


一応、元のプログラムを作った人も考えたようで、 wait で指定した「待ち時間」をキャンセルする処理を入れていた。

これ、すぐに次の next に移る、というだけで、プログラムはやっぱり止まらない。


だから、プログラム中にすごい数の…しつこいくらいに、現在のシーン番号を確認して、致命的な処理をしないためのプログラムが入っていた。

致命的でなければ…つまりは、アニメがおかしくならなければ、無駄な処理をしてもかまわない、という書き方。

本当に無駄なだけなら構わないけど、やっぱりいつバグが出るかわからない、恐ろしいプログラム。




さて、本当に恐ろしいのはここから。


上の例はなんとなく「流れ」を読んでほしかったので、処理は A B C D …と進むように書いてある。


でも、本当はそうではない。状況によって間が飛ばされる。

次への呼び出しはこんな感じ。



function 処理A {
 ~~~
 if(フラグ1){
  処理B();
 }else if(フラグ2){
  処理C();
 }else{
  処理D();
 }
}


「フラグ」と簡単に書いたところは、複合条件のこともある。


そして、類似した内容が処理ごとの最後に繰り返し書かれているし、タップした際のスキップ処理ごとにも入っている。


条件で「間を飛ばす」だけではなく、排他的に呼び出したりもする。

とにかく、多数のややこしい処理の条件分岐を、多数の場所に同じように書いてあって、でも少しづつ記述が違う。

そして、それらが全部同じように動くことが期待されている。



スパゲティプログラム、っていうのは、goto 文が当たり前だった時代の、混乱した流れを持つプログラムの蔑称だ。


でも、goto 文があるからスパゲティプログラムができるんじゃない。

現代にだってスパゲティを生み出すプログラマは存在する。



1つ弁護しておくと、このプログラムは一人の手によるものではない。

最初は素直な流れだったのだけど、ソーシャルゲームでは毎週のように「新機能」が追加されるので、拡張を繰り返すうちにおかしなことになったのだ。


そして、今手伝っている仕事では、「困った事態」になると僕が呼び出される。

もう、誰の手にも負えない状態になったプログラムに、さらに新しい機能を突っ込む必要が出て、何とかならないかと相談されたのだった。




さて、換骨奪胎。

プログラムの一番「中心となる部分」…つまり、処理を呼び出す流れの部分を総入れ替えして、周辺の部分はそのまま使いまわそう。


処理の流れを整えるための関数を、一つ用意する。

各シーンの最後は、「次のシーンへ」飛ぶ代わりに、この関数を呼び出すようにする。


また、画面タップで処理を飛ばす際も、引数付きでこの関数を呼び出す。



var sceneNum=0;
nextScene(){
 switch(sceneNum)
  // シーンの終了処理(スキップした時用)
 }

 sceneNum++;

 //条件によりシーンを飛ばす処理
 if(sceneNum==1 && フラグ1)sceneNum++;

 switch(sceneNum){
  // シーンの呼び出し処理
 }
}


これで、複雑怪奇なスパゲティ部分をなくすことができる。



next の中で使われる wait に関しては、sWait という関数を使って置き換える。

sWait の中身は後で書くことにして、先に呼び出し側を示そう。



function 処理A {
 next(function(){
  処理
  return wait(1.0)
 });
}


というプログラムなら、次のように変える



function 処理A {
 var scene = sceneNum;
 next(function(){
  waitHandle = sWait(1.0,scene)
  処理 A
  return waitHandle;
 });
}


変わった点は2つ。

現在の sceneNum を処理の冒頭でコピーしておき、sWait に渡すこと。

そして、return で直接 wait を書くのではなく、処理の前に wait の値を得る処理を行い、最後にその値を return すること。


なんでそんな風にするのかは、 sWait の中身を示せばわかる。



function sWait(sec,scene){
 if(scene!=sceneNum)throw "force end";
 return wait(sec);
}


scene にコピーされた値が、sceneNum と変わった場合…つまり、タップされてシーンが変わったら、throw 命令で例外を発生する。

そうでなければ、通常の wait と同じように振る舞う。


throw で例外が起きると、jsdeferred は next でつながれた処理を、全部打ち切ってしまう。

だから、シーンが変わったらこれですべての処理を打ち切ることができる。


また、処理の「前」に sWait 呼び出しを書くのは、前回の wait で示した時間待っている間にタップされ、シーンが変わった際には、「処理」を行ってはならないからだ。



これで、「シーンが変わったのに、前のシーンの処理が実行されておかしくなる」ことは無くなる。

そうした事態に備えて入れられていた、小さな条件分岐は全部なくしてしまって構わない。

ごちゃごちゃとしたプログラムが、すっきりとしたものになる。



処理をまとめた関数の冒頭で scene を定義する。

wait を全部書き換える。

最後の「次の呼び出し処理」を全部書き換える。

シーン番号を確認する条件分岐を全部消す…


というのは、結構な作業量ではある。でも、機械的な作業だから難しくはない。


そして、これによりスパゲティとおさらばすれば、新たなシーンの追加も難しくなくなる。




「処理」と「処理の流れ」は別物なのだけど、これをしっかり分離して考えるのは、プログラムにかなり慣れた人だけだと思う。


ゲームなんか作っている人は理解しているかもしれない。

ゲームって、条件分岐の塊だから、その分岐をうまく分離しておかないと無用な混乱を引き起こす。



jsdeferred は、javascript が苦手とする「小さな流れ」を作り出すためのツールで、結構有用だ。

でも、それはあくまでも小さな流れ。条件分岐を必要としない流れのみ。


条件分岐を必要とする大きな流れは、別の個所でまとめて処理して、流れを「整流」してやる必要がある。

小さな流れの中に条件分岐をごちゃごちゃと書き込むと、バグの元だ。




上に書かれている程度なら、ここに書いたようなややこしい手法を使わないでも、力技でも乗り切れる。

でも、力技で乗り切っていれば、いつかバグが出る。


実際に任されたプログラムは、他の理由による処理の混乱も混ざっていて、本当に「ちょっといじればバグが出る」状況になっていた。



ここでは jsdeferred の話として書いたけど、こういう仕事は昔から時々やっている。

自分のプログラムが拡張に従ってややこしくなって…という時もあるけど、大抵は他人のプログラムの整理だな。



その時々で、どう書き直せばスマートになるかは違うので、一概に方法を示すことはできないのだけど、とにかく「処理」と「処理の流れ」を分離することが重要。


この考え方は、いつだって役に立つ。




同じテーマの日記(最近の一覧)

コンピュータ

関連ページ

ServiceWorker と indexedDB【日記 19/05/03】

Programming Tips

別年同日の日記

08年 忙中閑有

12年 区民祭り

13年 Burn ALL GIFs day

18年 モーターアシスト


申し訳ありませんが、現在意見投稿をできない状態にしています


戻る
トップページへ

-- share --

0000

-- follow --




- Reverse Link -