#はじめに
1年ほど前から、Minecraftのマルチプレイ用サーバーソフトウェアSpigot上で機能するプラグイン、SnowballGameを作っている。
打つ、投げる、捕るの基本機能に加え、変化球やストライクゾーンの自動判定、ある程度の精度のあるノック機能など、自分としてはそこそこ満足のいくものが作れていると思っている。
##が、
自分はこれがほぼ人生で初めて書いたJavaのコードに近く、現在も初心者を脱したとは到底言えないレベルにある。加えて、かなり簡素な作りで基本部分を作った後にどんどん機能を追加していく形で開発していったので、このプラグインは極めてコードが汚い。
一応Spigotのライセンスに則ってGithubにソースコードを公開してはいるが、とても人様にお見せできるような代物ではなく、ほぼ全編読みづらい個所と突っ込みどころで構成されている。加えて、一行もコメントがない。(英語で書くべきかどうか迷ってめんどくさくなってしまったため)
ここまではある程度定期的に追加したい機能が浮かんだりして中身を触ってきたためなんとなく把握できているが、このままではいずれ自分でも何が何だか分からなくなってしまうだろう。なので、今後とりあえず形になる程度にはリファクタリングしていきたいと思っている。
その作業のためにまずはコードではなく日本語で「何を考えて書いたか」などといったことをまとめておこうと思ってこの記事を書いた。
そのため主には自分用に書いた記事ではあるが、他にスポーツ系プラグインを開発したいと思う方がいれば、役立てていただければ幸いです。
#バウンド
まずはどんな球技においても必ず必要になると思われるボールのバウンド部分の実装。
Minecraftには斜め向きの面が存在しないため、跳ね返りという運動は、「ぶつかった面に応じて物体の移動ベクトルのX,Y,Zいずれかの座標一つのみを反転させる」こととして置き換えられる。
Spigotにおいては、ボールとして利用するものがProjectileであれば、ProjectileHitEventによって「ボールがブロック/エンティティに衝突した」ことを検知できる。
どの面にぶつかったかということに関しては、SpigotにはBlockFaceという型があるため、event.getHitBlock().getFace(event.getEntity().getLocation().getBlock())
とやってやればおおむね取得できる。あとは取得したBlockFaceがUPまたはDOWNならボールのVelocityのYを、EASTまたはWESTならXを、SOUTHまたはNORTHならZを反転させてやればいい。
なお、ProjectileHitEventが呼ばれた時点で既に元のボールの実体はゲーム内から消えてしまっているため、World.spawnEntity(Location, EntityType)
で新しいボールを呼び出しそれに反転させたvelocityをセットしてやる必要がある。
##問題
しかし、この方法には問題がある。Block.getFace(Block)は、元のブロックと引数のブロックが隣接している場合にしかBlockFaceを返してくれない(nullになる)。ボールの速度がある程度以上速い場合なんかには、イベント発生時のボールの位置とブロックの位置が1ブロック以上離れていることがあるため、これは大きな問題だ。
加えて、BlockFaceは上下東西南北の6つ以外にも南西や北北東といった斜めの位置の値も返してくる。こっちの問題は「BlockFaceをtoStringしてEASTかWESTが含まれている場合にはXを、さらにSOUTHやNORTHが含まれていればZを反転させる」ように書けば解決できそうに見えるが、やってみるとかなり「斜め判定」される範囲が広く、ちょっと直観に反する動きになってしまうのがわかると思う。
##解決策
そのため、返ってきたBlockFaceがnullであったりSOUTH_EASTのように_を含んだりする場合に、上下東西南北いずれかの形に修正してやる必要がある。そこの部分はBlockIteratorを使ってこんな風に書いた。
if(hitFace == null || hitFace.toString().contains("_")){
BlockIterator blockIterator = new BlockIterator(hitLoc.getWorld(), hitLoc.toVector(), velocity, 0.0D, 3);
Block previousBlock = hitLoc.getBlock();
Block nextBlock = blockIterator.next();
while (blockIterator.hasNext() && (!Util.doesRepel(nextBlock) ||nextBlock.isLiquid() || nextBlock.equals(hitLoc.getBlock()))) {
previousBlock = nextBlock;
nextBlock = blockIterator.next();
}
hitFace = nextBlock.getFace(previousBlock);
}
要するに、ぶつかったときのボールの座標からボールの移動のベクトル方向に進んだ方向にあるブロックを取得し、順番に「バウンドしてもよいブロックか」をチェックしてOKであればループを抜けてその方向にあるブロックが元のブロックに隣接する面を「ぶつかった面」として返す、という仕組みになっている。ここ自体も突っ込みどころはあるのだが、基本的にはこれでうまくいっている。
##備考
###違和感への対処
この方式で実装するとたいまつなどの当たり判定があってほしくないブロックにも跳ね返ったり、当たった時の角度によってカーペットやレッドストーンワイヤーの横面に当たったと判定されて違和感のある挙動になったりする。
それを防ぐため「あたったのがこの種類のブロックであればUPに上書きする」「この種類のブロックはぶつかった際ボールが避けるようにする(ただし、ProjectileHitEventはCancellableでないので自前で動きを書き換える必要がある)」といったリストをあらかじめ準備しておくといいかもしれない。
自分もそうした形式で書いてはいるのだが、そこの部分のコードにあまり自信がないためここでは割愛する。
###物理的に誤った特徴を付与する
この方式で記述しボールの反発係数に見合う程度に速度を減衰させれば、とりあえず正しい跳ね返りは実装できるのだが、そのように作るとどうも「ゴロが転がる距離を現実に近づけようとすると高いフライのワンバウンド目が跳ねすぎる」「フライのバウンドを適切な水準にすると鉄球のような転がり具合になる」といった問題が発生する。(一時期話題になった某有名原作野球ゲームにおけるボールの異常な転がらなさはこれが原因か?)
そのため自分は、「バウンド時の球速が速いほどより大きく減速する」「バウンドした面の法線ベクトルとボールのバウンド後のベクトルがなす角が小さいほどより大きく減速する」というように書いている。
速いほど、直角にぶつかるほどボールがつぶれやすく地面が掘れやすいためロスする力も大きいだろうという見立てでの実装だが、きちんとしたデータがあるわけではなく自分の感覚で調整している。このあたりの適当さから題に「似非」物理学とつけた。
加えて、ワンバウンドのカーブが変化とは逆向きに跳ね返る取りづらさやスライスしながら切れていくフライがバウンドするとよりファールゾーンに逃げていく苛立たしさをどうにか表現したいと思い、投球時や打撃時にボールのスピンを回転ベクトルの形でメタデータとしてボールに付与し、この値をバウンド時に用いてある程度回転が反映されるようにも作っている。
ただこれがなかなか面倒なもので、一般的には回転がボールの跳ね返り時の運動に与える影響は、「地面の法線ベクトルとボールの回転ベクトル(回転のベクトル表現)の外積に比例する」そうなのだが、これでは上記のような要件を満たさない。
右翼手の視点からみてファールゾーンに向かってスライスしていく打球というのは右投げの一塁手がライト側に向かってシュートを投げるのと同じような回転軸を持っていると考えられるが、これを満たそうとすると前者と後者でバウンド時の変化の向きが全く逆になってしまう。
この現象の正しい原因は自分にはよくわからない。そのため、「たぶん外野まで飛んでこないような低いライナー性の軌道ならフェアゾーン側に、フライならファールゾーン側に跳ねるようにすればいいのだろう」と適当にあたりをつけて、「ボールの回転ベクトルとバウンド前のボールの運動のベクトルを反転させたもの」の外積に比例する方向にバウンドを変えるように書いている。
ただ、これだとテニスのスライスサーブと投手の投げるカーブの跳ねる向きが逆なことに説明がつかない。そのため恐らくこちらは物理的に厳密ではないどころが明らかに誤った実装になっていると思われる。が、自分には正しい物理法則を見出して実装する能力がないため諦めをつけている。
物理に詳しい方この件に関して教えてもらえるとすごく嬉しいです。回転数が大きくてバウンド時にボールが滑るとファールゾーンに逃げていくのか?など考えていることはありますがどのように記述するべきかわからないです。