よく跳ねるほど遊びやすい。でも跳ねすぎると手に負えない── ワイヤーで星をつなぐゲーム「ほしつなぎ」を作りました

6本目のゲーム「ほしつなぎ」を公開しました。

暗い画面に星が5つ散らばっていて、ワイヤーを撃って引っかけ、振り子のように振られながら順に集めていきます。集め終わると、通った跡が星座になります。

作り始めたときは、まったく違うものでした。横に進み続けるだけの、星も星座も無いものだったのです。そこから今の形になるまでの調整に、これまでの5本で一番時間がかかりました。

今回は、その調整の話を書きます。ゲームの話ですが、中身はほとんど数値の設計の話です。私は業務システムを33年書いてきた人間なので、そういう目で見てしまいがちなのかもしれません。

目次

ボールが暴れて、手に負えなくなった

まず、遊んでいて起きたことを書きます。

このゲームのボールはピンポン玉のように跳ねます。壁に当たると跳ね返り、床でも跳ね返る。よく跳ねるほど星に届きやすくなるので、跳ね返りは強いほうが遊びやすい。

そう思って跳ね返りを強くしていったら、ある日から手に負えなくなりました。

ボールがものすごい勢いで飛び回って、狙って撃つどころではない。星は見えているのに、当てられない。かといって跳ね返りを弱くすると、今度は床で止まってしまって、そこから何もできなくなります。

よく跳ねるほど遊びやすいが、跳ねすぎると遊べない。

この両立が、今回いちばん苦労したところでした。

原因は、自分で塞いだ穴でした

調べてみて、原因が分かりました。

もともとこのゲームには空気抵抗が入っていました。速度が少しずつ減る仕組みです。ところが、その空気抵抗のせいでボールが床にすぐ止まってしまう問題が起きていました。止まった場所から届く星が無いと、そこで詰みます。

そこで空気抵抗をゼロにしました。宇宙が舞台なので、理屈も付きます。

その結果、盤面から「速度が減る場所」が一つも無くなりました。

跳ね返りは0.97、つまり1回あたり3%しか減りません。一度速くなったボールは、いつまでも速いまま。詰みは無くなったが、代わりに暴走が生まれた。片方を潰したら、もう片方が出てきたわけです。

速いときだけ減らす

空気抵抗を戻せば暴走は収まります。でも、それだと元の詰みが戻ります。しかも、ふわふわ漂っている感じが削られてしまう。そこが気持ちよかったので、失いたくありませんでした。

考えた末に、こうしました。

壁の跳ね返りを、速さに応じて変える。

ゆっくり当たったとき … 0.97(よく跳ねる。今までどおり)
速く当たったとき     … 0.70(大きく減る)

ゆっくり漂っているときは何も変わりません。暴走したときだけ、壁がブレーキをかける。

減らす場所を「時間」から「壁」に移した、という言い方もできます。空気抵抗は常に効きますが、壁は当たったときだけ効く。効かせたい場面だけを選べます。

実際に測ると、こうなりました。

落ち着くまで壁に当たった回数
減衰なし31.4秒34回
減衰あり4.2秒4回

制御不能の時間が8分の1になりました。そして詰みは戻っていません。何も操作せずに180秒放置しても、ボールは止まりませんでした。

閾値を下げると、閾値の位置が動く

ここからが、今回いちばん調整をしていて面白かったところです。

「速いとき」の境目をどこに置くか。この数字を決めるのに、思ったより手間がかかりました。

最初は700にしていました。ゲーム中の速度分布を見ると、700は上位10%あたりに位置していて、ちょうどよさそうに見えたのです。

ところが、あとから重力を弱くする調整を入れました。すると全体がゆっくりになり、同じ700が上位5%の位置に上がってしまいました。 20回に1回しか効かない。

そこで下げようとして、おかしなことに気づきます。

閾値を下げると、壁がよく食うようになってボールが遅くなる。すると速度分布そのものが下にずれて、下げたはずの閾値の位置が押し戻される。

実際の数字がこうでした。

閾値分布の中の位置
70095.4%
64092.8%
60091.2%
56088.2%

測ろうとしている対象が、測ることで変わってしまう。 だから一度の計算では決まりません。候補を入れた状態で測り直す、を繰り返して、目標の帯(75〜90%)に入るいちばん高い値として560を選びました。

業務システムでも似たことがありますよね。性能を改善したら負荷の分布が変わって、ボトルネックが別の場所に移る。あるいは、キャッシュのヒット率を上げたらアクセスの傾向が変わって、また測り直しになる。

系に手を入れると、系そのものが変わる。 頭では分かっていたことですが、自分で作ったものでこれを踏むと、感触が違いました。

計算の順番を変えるだけで、誤差が溜まらなくなる

もうひとつ、コードを読み返していて驚いたところがあります。

このゲームの振り子は、物理エンジンを使っていません。角度と角速度を持って、三角関数で回しているだけです。1秒に120回、こういう計算をしています。

加速度 = -(重力 / ロープの長さ) × sin(角度)
角速度 += 加速度 × 刻み
角度   += 角速度 × 刻み

小学校で習う「速さ×時間=距離」を、細かく刻んで繰り返しているだけです。

こういう計算は、誤差が溜まるのが普通です。刻むたびに少しずつずれて、長く回すほど狂っていく。振り子なら、勝手に振れ幅が大きくなったり、逆に止まっていったりします。

ところが実際に測ってみると、こうでした。

回した時間エネルギーの変動幅
10秒±0.6%
600秒±0.6%
3600秒±0.6%

1時間回しても、幅が広がりません。

理由は、計算の順番にありました。

上の3行をよく見ると、角速度を先に更新して、その新しい角速度で角度を進めています。 これを逆にして、角度を先に進めてから角速度を更新すると、誤差が一方向に溜まっていきます。

順番が違うだけです。書く行数も、計算量も同じ。それだけで、誤差が溜まるか、有限の幅の中で振動するだけかが変わる。

これには名前が付いていて、半陰的オイラー法(シンプレクティック・オイラー法)と呼ばれるそうです。名前を知ったのは今日でした。

そして、この誤差は気にしなくてよかった

もうひとつ、実務的な話を付け加えます。

この振り子には、意図的に減衰を入れています。放っておくと永遠に振れ続けてしまうので、少しずつ勢いが落ちる仕組みです。

その減衰の強さを測ると、10秒で推進するエネルギーが元の10.3%まで落ちます。

一方、計算の誤差は±0.6%。

桁が2つ違います。 精度を心配していた誤差は、自分で入れた減衰に完全に埋もれていました。

これも、業務システムでよく見る形です。マイクロ秒を削る改善に時間をかけたら、その処理の前後で秒単位の待ちが入っていた、というような。

気にすべき場所を、数字で確かめてから決める。 分かっているつもりで、やはり間違えてしまいました。

遊んでみてください

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次