遠い昔、はるか彼方の銀河系で…3DスペースシューティングをProcessingで作ろう:改造編
毎年恒例、Processingをテーマにした技術系アドベントカレンダー「Processing Advent Calendar 2015」に今年も参加しました。いつもみなさんの記事を読むのを楽しみにしているのですが、自分の記事は土壇場で作るので、毎回結構冷や汗をかいています。
さて、12月のイベントといえばクリスマーーいや、今年は新作が公開されたばかりのスターウォーズでしょう! でも「チケットはあるのに見るのはまだ先」になりそうなこの気持ちは、プログラムにぶつけるしかない!
そんなわけで、今年は(余裕がなかったので)以前作ったスペースシューティングを改造して遊びます。
↑これが今回作る(改造後の)プログラムのスクリーンショットです。
以前作ったもの
以前作ったのはこういうやつです。
(こちらのコードは以前Writing CafeやOpenProcessingにアップしてあります)
今回の3Dシューティングゲームは、遊んで作るスマホゲームプログラミングの6章で3Dモデル表示に対応したものを作って解説しているのですが、今回はそのソースコードを元にしつつ、改造していきます。
改造後のプログラムダウンロードと操作方法
完成プログラムのダウンロードはこちら(ShootingStarP3.zip)。
ソースコードについて
ShootingStarP3.pdeがゲーム本体で、
Unit.pdeが敵やプレイヤー、エフェクトなどのユニットのプログラムです。
MQOModel.pdeはmqo形式のモデル(Metasequoia用)を読み込んで描画するためのプログラムです。
実行方法
すぐ実行して試してみたい方は下記ファイルを実行してください。
- Windows環境
「ShootingStarP3/application.windows32/ShootingStarP3.exe」
- OS X環境
「ShootingStarP3/application.macosx/ShootingStarP3.app」
もちろん「Processing 3でソースを読み込んで実行」でも大丈夫です。
操作方法
操作は、マウスカーソルを動かして向きを変えて、左クリックで弾を発射です。右クリックかスペースキーで加速、離すと減速です。10体の敵をすべて倒せばクリアです。
あなたは何秒で敵を殲滅できるでしょうか!?
解説
Processing 2のコードをProcessing 3に対応させる
size()関数でフルスクリーンのサイズを指定するため変数を使っていた箇所がエラーになって動かなかったため、Processing 3で追加されたfullScreen()関数に置き換えます。
size(displayWidth, displayHeight, P3D);
↓
fullScreen(P3D);
今回必須だった変更点は以上です。
惑星を綺麗に表示する
惑星を表現するため、最初の300lineのコードでは球、書籍のサンプルでは3Dモデルを使っていましたが、実は2D画像の方が簡単にそれっぽい表現ができたりします。実際にやってみましょう。
画像はパブリックドメインの地球の画像を使わせていただきます。遠い昔の惑星ではないですが、未来の人から見れば、遠い昔です……ま、まぁ、いいじゃないですか。
それをスケッチのdataフォルダに入れておき、setup()内でloadImage()して読み込んでおきます。
PImage earthImg; void setup() { 〜 earthImg = loadImage("earth.jpg"); 〜 }
そして惑星を描画していた部分のコードを、1枚の画像を描くように書き換えます。
// 惑星 noStroke(); translate(0,0,-300); pushMatrix(); scale(5); rotateY(radians(180)); planetModel.draw(); popMatrix();
↓
// 惑星 noStroke(); translate(0,0,-300); imageMode(CENTER); image(earthImg, 0, 0, 1000, 1000);
ただ前方に一枚の板として地球を描いてるだけですね。これが前方ではなく右とか上とか別の向きだった場合にはカメラに向けて板を回転させておく必要があります。ここでは処理を簡単にするためにただ前方(Zマイナス方向)に地球の写真を表示しています。
でもそのままだといきなり地球が目の前にあって見栄えがイマイチなので、resetStage()内でplayer.lookAtPos(new PVector(-200, 0, 0.0f))として左向きにプレイヤーの戦闘機の向きを変えて、右側に少し地球が見えるような感じに変更しました。
※書籍のコードにもあるUnitクラスのlookAt()は、lookAtDir()関数に名前を変えて、特定位置を向くlookAtPos()関数を追加してあります。
あと、drawInfo()内になんとなくコックピットのフレームっぽい丸枠を追加しました。
// コクピット用丸フレーム noFill(); stroke(220); strokeWeight(3); ellipse(centerX, centerY, baseHeight*0.4f, baseHeight*0.4f); strokeWeight(10); ellipse(centerX, centerY*1.1f, baseHeight*0.88f, baseHeight*0.88f);
これを見るとだいぶ投げやりな感じですが、あとで行うプロジェクターを使った投影のときにこのフレームがあった方が しっくりきたのです、たぶん。
遠景の星と近景の塵
惑星を描く処理の前あたりに、遠くの星々と、近くの塵を描く処理があります。ここのコードはまったく変えていないのですが、ちょっとだけ解説しておきます。
translate(player.pos.x, player.pos.y, player.pos.z); int seed = int(random(1000)); randomSeed(0); float range = 500.0; PVector starPos = new PVector(); for(int i=0; i<150; i++) { // 遠くの星々 strokeWeight(int(random(1,3) * screenScale)); stroke(random(128,255)); starPos.set(random(-100, 100), random(-100, 100), random(-100, 100)); point(starPos.x, starPos.y, starPos.z); // 近くの塵(プレイヤーのまわりに常にあるようにループさせる) starPos.set(random(range), random(range), random(range)); starPos.x = modulo(-player.pos.x + starPos.x, range) - range * 0.5; starPos.y = modulo(-player.pos.y + starPos.y, range) - range * 0.5; starPos.z = modulo(-player.pos.z + starPos.z, range) - range * 0.5; line(starPos.x, starPos.y, starPos.z, starPos.x - player.vel.x * (range * 0.001) + 0.001f, starPos.y - player.vel.y * (range * 0.001), starPos.z - player.vel.z * (range * 0.001)); } randomSeed(seed);
まず、星や塵の位置は150個の配列に保存してーーいるのではなく、すべてランダムに決めています。ただ毎回ランダムだと毎フレーム別の位置に星が移動してしまうので、randomSheed(0)にすることで「毎回変わらないランダムな位置」にしています。うまくランダムを使うといくらでもデータを捏造できるので便利ですね。
遠くの星はpoint()で点を描いてるだけで、最初にtranslate(player.pos.x, player.pos.y, player.pos.z)とプレイヤー位置に移動してまわりに散らばらせているので、近づくことはできません。
塵の方は、プレイヤーの戦闘機の速度をプレイヤーに伝えるために、現在の速度にあわせてline()で線を描いています。塵は遠景ではなく近景にあってどんどん追い越して去っていってほしいものなので、translate(player.pos.x, player.pos.y, player.pos.z)を無効化するためにplayer.posを引いています。ただそれだけだと、通り過ぎた一定範囲の外には塵がなく、大量の塵を空間にばらまくといくら塵があっても足りないので、modulo()という関数で余りを計算してプレイヤーの戦闘機のまわりで塵をループさせています。
// aをbで割った余りを返す float modulo(float a, float b) { return a - floor(a / b) * b; }
この塵の数を調節したり、線を長くひっぱればワープっぽい表現もできるので、はるか彼方の銀河系にワープしたい方は改造してみてください。
爆発エフェクトをビルボード化
地球を綺麗にしたら爆発もどうにかしたくなったので、画像を使うようにしてみます。こちらもパブリックドメインのものを適当に加工させていただきます。
EffectクラスのdrawShape()で赤い球を描いて爆発にしていたのを、画像と差し替えます。
// 形状の描画 void drawShape() { damage(2); matrix.scale(1.04); matrix.rotateX(0.1); fill(255, 64, 32, map(life, 0, 100, 0, 128)); sphereDetail(7); sphere(radius); }
↓
// 形状の描画 void drawShape() { damage(2); lookAtPos(player.pos); // プレイヤーの方を向くことでビルボードにする float alpha = norm(life, 0, 100); tint(255, 255 * alpha); float s = 5000 * (1.0f - alpha); imageMode(CENTER); image(explosionImg, 0, 0, s, s); tint(255); }
ビルボードというのは、常にカメラの方を向く板のようなものです。ここではプレイヤーの戦闘機=カメラの位置なので、UnitクラスのlookAtPos()を使って、プレイヤーの位置の方を向くようにしています。あとはlifeの減り具合に応じて大きく、薄く表示していくような処理を書いています。
適当なのでペラペラ感がありますが、HMDを装着してVRで立体的に見えるようなゲームにすると、それがより強調されそうです。
コクピットにいる気分を盛り上げるためにプロジェクターを使う
ついでにプロジェクター(Qumi Q2)を使ってコクピット気分を盛り上げてみましょう。
使うのは、これだ!
家にあるものでドームっぽいものがこのパラソルぐらいしかなかったので、こいつをスクリーンのかわりにします。
しかし、下地が緑色なので全体的に緑っぽくなります。そこで投影時に緑色成分を減らす処理を加えました。描画の一番最後にこんな感じのコードを書きます。
// 緑色成分を減らす blendMode(MULTIPLY); fill(255, 180, 255, 255); rect(0, 0, width, height); blendMode(BLEND);
乗算モードで画面いっぱいに緑成分だけ減らした白(=紫がかった白)の四角形を描いているだけです。
あとパラソルが丸型なので、画面外側を黒く、内側の円の中だけ映像が映るような処理も加えます。
// 丸く縁取ってまわりを暗くする noFill(); stroke(0); strokeWeight(height/2); ellipse(width/2, height/2, height*2.0f, height*1.8f);
これは、極太の黒い線の輪郭で丸を(塗りつぶしなしで)描いてるだけです。
さぁ、実際に投影してみましょう!
近くで見るとまぁまぁ視界を覆われる感覚もあって、なかなか楽しいです。最初はちゃんと形状にあわせて変形させようかと思っていたのですが、そんなに違和感なかったのでそのまま投影です。
ただ、スクリーン生地の性能が悪く、暗めにはなっちゃいますが(なにせ、ただのパラソル)。ドームかHMDほしいなぁ。
あと変な態勢でマウス操作し、はっきり見えない映像で遊ぶため、激ムズです!
じゃあ、普通のスクリーンにプロジェクションしたらどうなるでしょう?
平面ですが、綺麗。なんだ、こっちの方がいいじゃないか…。やっぱり下地の色や反射性能は大事です。フードプロジェクションマッピングしたときもごはんよりチーズの方が断然性能が上でしたし。
さて、なんだかよく分からない記事でしたが、これでまた皆様のプログラミング・フォースが覚醒しましたね。Processingと共にあらんことを。