スプーキーズのちょっとTech。

SPOOKIES社内のより技工的な、専門的なブログページです。

3D空間で「野球っぽい挙動」を表現するために学んだこと

はじめまして!アルバイトのkouhiraです!

いきなりですが、僕は三度の飯よりも野球が好きだと自負しています。三度の飯もかなり好きな方なので、野球に対するはそんじゃそこらの人には負けない自信があります。

そしてもちろん、プログラミングも大好きです。こちらはまだまだ勉強することの多い身ですが、毎日楽しくスプーキーズでもコードを書いています。

野球オタクがプログラミングという趣味を持った場合、当然「自分で野球ゲームを表現してみたい!」という目標を持ちますよね?(持ちませんか?)

僕もご多分に漏れずそんな目標を抱いていて、その足がかりとして、「Minecraft」というゲーム内で野球をするプラグイン(Steamゲーにありがちな「MOD」の亜種みたいなものだと思ってください)を趣味で作成しています。

そして先日、社内勉強会で発表する機会があり、僕はそのプラグイン作成時に学んだ知識について発表しました。(スプーキーズの社内勉強会は業務内容と微塵も関係なくても温かく聞いてもらえます!)今回は、その時に発表させていただいた内容を紹介させていただこうと思います!

前提としている環境はかなり特殊ですが、環境に依存しない内容に絞って紹介するつもりですので、もし役に立ちそうな知識があれば、スポーツゲーム作成などの際の参考にしていただければ嬉しいです!

※物理の知識について書いた箇所がありますが、書いている僕は高校物理すら習っていない根っからの文系です…なにか誤りなどありましたら申し訳ありません…。

前提

  • SpigotというMinecraftの公式サーバーソフトウェア上が動作環境。
  • MinecraftのJava Editionで動作するものなので、開発言語はJava。
  • イベント駆動(ゲーム中にこういうエンティティがこういう挙動を行ったとき、といったものに対応してリスナーを書いていく形式)で概ね書く。
  • イベントとは無関係に定期的に実行したい動作はRunnableというものを定義し、実行間隔などを指定して呼び出す形式。
  • 基本的なプレイヤーの動きはMinecraftにデフォルトで搭載されているものが利用でき、野球の根幹を成す「投げる」という動作は雪玉というアイテムを利用している。
  • 物体の移動は「速度」として三次元ベクトルを付与する形。
    • その瞬間(0.05秒=1tick)の(xの移動量,yの移動量,zの移動量)というのがここでの「ベクトル」(yが上下方向)

投球

  • 野球における投球を満足できる程度に再現しようとすると、球速や軌道をある程度プレイヤーの思うように操れる必要がある
  • つまり、変化球を実装しなくてはならない
  • 投球を行った時に起動し、以降毎tick実行されるRunnableの中で投じられた雪玉の運動のベクトルを少しづつ操ってやるようにした
  • プレイヤーがどの球種を投げようとしているかを指定するのは、Minecraftにデフォルトで存在する「アイテムに名前を付ける」という機能を用い、「この名前ならば毎tickこれだけの変化」というのを定義しておき、それぞれのプレイヤーが投球前に名付けを行う形にしている
import org.bukkit.entity.Projectile;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;

public class BallMovingTask extends BukkitRunnable {
    private Vector move;
    private Projectile ball;
    public BallMovingTask(Projectile ball, Vector move) {
        this.ball = ball;
        this.move = move;
    }
    @Override
    public void run() {
        // ボールの速度を取得し
        Vector velocity = ball.getVelocity();
        // 変化のベクトルを加えて
        velocity.add(move);
        // ボールの速度を上書き
        ball.setVelocity(velocity);
    }

}

問題

  • このままだと、「真上方向にストレート(上方向に変化するボールとして定義したとする)を投げた時」などに(特に)挙動がおかしくなる
  • 上に上に変化し続けて、ただなかなか落ちてこないというだけのボールになってしまう
  • 投球だけなら真上方向に投げるのがレアケースなのでそこまで問題にならないが、現実の野球でフライが伸びたり切れたりするのを考えると、この「発射後の軌道の変化」は打球にも適用したい
  • となると、真上のフライはそこそこありうる打球なのでよくない

    解決策

  • そもそも期待する挙動はなにか?
  • バックスピンで上方向に上がる打球というと現実でいうとキャッチャーフライ
  • http:// https://youtu.be/sdRoiiI8KeQ?t=5

  • 上記動画を見ると、必ずキャッチャーはキャッチャーフライ捕球時に後ろ(バックネット方向)を見て捕球している

  • 体の前に飛ぶ打球なのにわざわざ後ろを向いて捕球するのは、キャッチャーフライが後ろに向かって上がった後、前に戻ってきながら落ちてくる打球だから(キャッチャー経験者は小学生の時期に習います)
  • ゲーム内でもこの挙動を再現したい
  • つまり「ℓ(リットル)」の字のような軌道になってほしい

現実の物理

  • そもそも回転する球の軌道が変化するのは、「マグヌス効果」というものによるらしい
  • バックスピンなら、上部の空気がより早く後ろに流される→上の方の空気が薄くなってそちらに引っ張られるという原理(※自分のレベルでの理解)
  • 引っ張られる力は運動のベクトルと回転のベクトル表現の「外積」に比例する
  • 向きを回転軸(反時計回りになるように取る)、大きさを回転の大きさとして定義されるのが「回転のベクトル表現」
  • あるベクトルともう一つの他のベクトルに対し、そのそれぞれに垂直なベクトルが「外積
  • 同じ方向のベクトル同士の外積は零ベクトルになる
    • 進行方向と回転軸が一致する縦のスライダーと、回転数がそもそも少ないフォークボールはともに重力によって「落ちる」球になる
  • ちなみに変化量はその瞬間の球速や回転量に依存するらしい
  • だいたいボールの直径を73mm、大気の状態が標準状態として、1Tickあたり1/8 * π * π * 1.205 * 0.073^3 * 球速 * 一秒あたりの回転数くらい(参考にしたスライド)になる。
import org.bukkit.entity.Projectile;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;

public class BallMovingTask extends BukkitRunnable {
    // 回転のベクトル表現(ややこしいので大きさは適当)
    private Vector spinVector;
    private Projectile ball;
    // 1秒あたり何回転するか
    private int rps;
    public BallMovingTask(Projectile ball, Vector spinVector, int rps) {
        this.ball = ball;
        this.spinVector = spinVector;
        this.rps = rps;
    }
    @Override
    public void run() {
        //バウンドしたり打たれてボールが死んだらタスクをキャンセルする
        if(ball.isDead()){
            this.cancel();
        }
        // ボールの運動
        Vector velocity = ball.getVelocity();
        if(spinVector.length() != 0){
            // 運動と回転の外積
            Vector actualMove = velocity.getCrossProduct(spinVector);
            if(actualMove.length() != 0){
                // 外積を一度長さ1のベクトルに直してから正しい大きさにする
                actualMove.normalize().multiply(1/8 * Math.PI * Math.PI * 1.205 * Math.pow(73/1000, 3) * velocity.length() * rps);
            }
            // 運動のベクトルに加える
            velocity.add(actualMove);
        }
        ball.setVelocity(velocity);
    }

}
  • 実際にやってみると、

javaw_20181212_000139F.gif (30.3 MB)

  • できた。

    打撃

  • 打撃を再現するためには、
    • 多様な打球が飛ぶ
    • タイミングが問われる
    • 振るコースや高さの調整が要求される
  • といった要素が必要だと考えられる
  • 一番簡単なのは、何らかの操作があった時に「この範囲ならバットに当たる」という箱を作って判定する形
  • 箱の表面で当たり判定を行うのではなく、箱の中にボールが入っているような位置関係だったら「当たった」ということにする
    • コースや高さは目線操作で設定
    • クリックのタイミングが問われる
  • バットに当たった後どういう打球になるかは、その箱の中心と当たった瞬間のボールとの位置関係から考えればいい
    • ボールの上を振るとゴロに、下を振るとフライに
    • 振り遅れると打球が後ろに飛ぶ
    • 概ね感覚に合うのでは?
  • 打球の強さはその二点の距離が大きいほど弱く、小さいほど強くなるようにする
    • 「芯」に当たればいい打球、外すと弱い打球
    • これにはn-距離を使うといい感じだった(1~0の変域で連続的な数値になるので)

問題

  • 「打球の回転」を定義しようとすると問題が発生する
    • 基本的にはバットとボールの位置関係が回転にも影響するのでそれを使いたい
    • 箱の中心からボールの位置までのベクトルをとり、それとボールの運動のベクトルとの外積をとれば逆に「その方向に切れていく回転」のベクトル表現が得られるはず
    • しかし今回の場合運動のベクトルとそれがまったく一致してしまい、結果が零ベクトルになる
  • 全く同じベクトルでさえなければ零にはならないため、
  • 「箱の中での位置関係」以外に打球の方向を左右するものが必要
  • 普通に考えれば「スイング軌道」(バット自体の動き)

    スイング軌道実装の困難

  • 「これが正しい」というのが存在しない
  • 選手ごとに全然違うのはもちろん、理論的にこれが正解というのもわからない
  • あれば球児は悩まない(愚痴)
  • 理論武装が出来ないので、「多くの人にとって納得できる」のをでっちあげる必要がある

    野球界の迷信を取り入れる

  • 正解が存在しないので、「それっぽく見える」やつを使いたい
  • 野球界には古くから「理想のスイング軌道」についての迷信がある
    • 「最短距離(直線)」
  • しかしこれはもはやほとんどの人が信じていない、古い考え方とされている(※僕の主観です
    • 「野球 最短距離」とかで検索しても否定的な記述が目立つ
  • ここ5年くらい(※主観)で多くの人に信じられるようになりつつある新たな迷信(※主観)がある
    • 「最速降下曲線」というもの
  • 「任意の2点間を結ぶ全ての曲線のうちで、曲線上に軌道を束縛された物体に対して重力 (に代表される保存力) のみが作用する仮定の下、物体が速度0でポテンシャルが高い方の点を出発してからもう一方の点に達するまでの所要時間がもっとも短いような曲線」らしい。
    • 要するに「ある地点からある地点まで何かを転がすときに一番早く転がる軌道」
    • https://www.youtube.com/watch?v=L2_d7MPeJks
    • こういう感じらしい
    • 「構えたところからインパクトまでがこの軌道だったら一番早いはず」という理論で紹介されていることが多い
    • お昼のワイドショーでも「大谷翔平のスイング軌道がこうだ」と取り上げられたらしい
  • 実際はスイング時は重力以外も関係する(というか人間が振り回す力が主)なので…?
  • が、「多くの人が納得してくれる」は達成できそう

    実装

  • 詳しい説明は理解できていないが、最速降下曲線の式は、Minecraftのワールド上で描こうとすると(x=θ-sinθ, y=1-cosθ)という感じになるらしい
  • プレイヤーの目のところを出発地点とし、θの値を0-πまで増加させて描いてみるとこんな感じ 2018-06-01_17.01.18.png (286.2 kB)
  • これをスイング時のバットの縦の動きと考える
  • そのまま横向きに回転させる
public static Location getBatPosition(Location eye, double roll , int rollDirection){
    // 目の位置
    Location eyeLoc = eye.clone();
    // 与えられた角度の分だけ横回転させる
    eyeLoc.setYaw(eyeLoc.getYaw() - (float)(90 * rollDirection) - Math.toDegrees(roll));
    // 回転させた方向にまっすぐ伸びるベクトルを得る
    Vector push = eyeLoc.getDirection().setY(0).normalize();
    double theta = Math.abs(roll * 2);
    // 最速降下曲線のx座標を求める式
    double x = push.normalize().getX() * (theta - Math.sin(theta));
    // 最速降下曲線のy座標を求める式
    double y = -(1 - Math.cos(theta));
    // 最速降下曲線のx座標を求める式
    double z = push.normalize().getZ() * (theta - Math.sin(theta));
    // 元の目の位置にそれぞれ得られた値を足した座標を得る
    return eye.clone().add(x,y,z);
}
  • こうなる 2018-06-01_18.12.51.png (184.7 kB)
  • この軌跡をどこか任意の点で微分して得られたベクトルを「バットのスイングによって与えられる運動」として加える
  • 自分は一番手っ取り早いので近くの2点を選んでその2点の差をとって使っている
  • 基本は一番低くなっているところとそのすぐ近くのもう一点
  • ※この軌跡をまるごとスイングとして使うのではなく、あくまで「バットに当たった瞬間のバットの動き」としてこの軌跡の一部分を用いる
  • 箱の中心からボールまでのベクトルと違うベクトルになるため、打球の回転のベクトルを得ることができるようになった

    副作用

  • 基本的には最速降下曲線の一番「底」の部分を使っているが、「任意の点」を選んでスイングのベクトルを得ることができるようになった
  • 一番底の部分を微分するとY方向の傾きは小さくなるはず
    • いわゆる「レベルスイング」=標準的なスイング軌道と考えることができる
  • 底以外の部分を使うことで打者の打球傾向をある程度変えられる
    • 底に至るより前のところを使えばゴロが多い「ダウンスイング」に
    • 後の方を使えば「アッパースイング」になる
    • パワプロで言う「弾道」のような要素として使える
    • ちなみに調整を行わずさっきの関数をそのまま用いると横回転の度合いも変わってくるため、「フライの打ちやすさと引っ張りの多さ」「ゴロの打ちやすさと逆方向への打球の多さ」が相関してしまうが、
    • それも統計上正しいようなのでよしとしている。

      参考資料

  • Spigot公式JavaDoc
  • 『野球の投手が投じる様々な変化球の特徴 ~移動速度,回転速度,回転軸の向きに着目して~』
  • ゴルフゲームでUnityの限界を突破する方法
  • Wikipedia「最速降下曲線」
  • 物理のかぎしっぽ「最速降下曲線」
  • BASEBALL GATE 「プルヒッティングのすすめ」