はじめに
はじめまして、1回生のνron(neuron)です。今回が初のゲーム作成でしたが、自分がプレイしたいと感じるゲームを目指した結果、この作品「らんだむしゅーてぃんぐ」が完成しました。自分がやりたいようにやったので、終始モチベーションを保ってゲームを作ることができました。
というわけで、今後も完全俺得ゲームを作っていくと思います()。
作品はこのページの一番下にあるリンクからプレイすることができます。
このゲームについて
よくある弾幕STGです。ゲームシステムについてはゲーム本体に記載しているのでここでは割愛して、このゲームのゲーム性について書こうと思います。
このゲームは何ステージ突破できるかを競うゲームであり、最大100ステージとはしつつも、100ステージ突破を前提とした難易度設定ではありません。一応、目安としては、[初心者→0-20ステージ突破、中級者→21-50ステージ突破、上級者→51-80ステージ突破、超上級者→81ステージ突破-完走]くらいを想定した難易度にしたつもりです。100ステージにたどり着かなくても凹まないでくださいね?
反省
今回作ったゲームの出来に関しては、unityを初めて使った割にはそこそこいいものができたんじゃないかなと思います。ただ、やはり粗が目立つ部分が結構多かったという点が大きな反省点ですね。
まず、私自身がイラストを描けないので、全体的に安っぽい見た目になってしまいました。また、このゲームにはBGMもSEもついていません。本当はBGM等も自作してやろうなんて考えていたのですが、当然のごとく挫折しました(誰か曲の作り方教えて,,,)。その結果、BGMは各自でバックグラウンドで好きな曲を流してもらうというスタンスで行くことになりました。(おい)
まあ結論としては、ほぼすべての素材(らんだむちゃんとwecaryちゃんのイラストを除く)を自作のもので押し通そうとした結果安っぽさが前面に出てしまったので、もっとフリー素材などを活用すべきだったということですね。
技術的な話
ここからは知識の共有も兼ねて技術的な話を書きます。私自身unity超初心者で基礎的な部分にしか触れていないので、同じく初心者の方にも理解しやすいと思います(?) あと、普通にもっと効率が良いやり方もあると思います。
ゲームシステム管理
現在のステージや、そのステージが始まって何フレーム経過したか、プレイヤーの残機とボム数、敵のHPなどの各情報は、とりあえずゲームマネージャーとなるスクリプトを作りそこに全部突っ込んで必要な時に呼び出すようにしました。 そしてどんどん増えるゲームマネージャー内の変数、、、 ゲームマネージャーとなるスクリプトに関しては調べれば出てくるので詳しくは書きませんが、これを作って使うことでシーン間やスクリプト間で共有したい情報を簡単に扱えます。
ちなみに、ステージが始まってからのフレーム数は弾幕を作る上で重要になります。
弾と当たり判定
全ての当たり判定にはコライダーを用いました。ここで、本当は弾側にもリジッドボディー2D(以下rb2D)を付けたかったのですが、大量に生成される弾に物理演算を付けるとかなり重くなるため、弾にはコライダーだけを付けて自機と敵にコライダーとrb2Dを付け、自機・敵側で弾のコライダーの侵入を検知するようにしました。(ここで弾を検知するとき Destroy(collision.gameObject)
で侵入してきた側のオブジェクトを削除できます。)また、rb2Dを呼び出すためのGetComponentもかなり処理が大きいようでこれによるラグもかなり大きいです(体験談)。やたら等間隔でがくがく動くような動作になったので原因を調べたところ、弾を生成するタイミングでGetComponentが1フレーム間に多数回呼び出されていたことが原因でした。
本当はさらなるラグ軽減のために、逐一Instantiate/Destroyでオブジェクトの生成/削除をせずにObjectPoolでオブジェクトの使いまわしをすべきだったのですが、そこまでの実装力はありませんでした。
弾幕作成
さて、弾幕STG作成の醍醐味である弾幕の作成について書きます。とは言いつつも、弾幕作成はここまでのシステム構築と比べるとかなり楽です。
あまり細かい説明が思いつかないので、今回作成した弾幕のうちいくつかを例として紹介します。コードと弾幕の紹介を照らし合わせて、それぞれのコードで何をしているかを見てみてください。共通する基本構造として、指定したタイミング・生成場所に弾を生成するスクリプトと、弾につける、弾の動きを制御するスクリプトの2つを合わせて弾幕を作っています。ここで紹介するコードは実際に使用しているものからの抜粋なので、変数の宣言などが抜けているものがあります。また、使用している主な変数について、stageは現在のステージ、countは現在のステージが始まってから経過したフレーム数(以下カウント)で、生成側のスクリプトは敵の子オブジェクトにつけています。GMはゲームマネージャーとして作成したスクリプトです。
まずは円形に弾を放つシンプルな弾幕。
これの生成側のスクリプトは
void enemy_bullet1()
{
if(GM.instance.count >=180 && GM.instance.count % (30 - (GM.instance.stage - GM.instance.stage % 10) / 5) == 0)
{
rnd_num = Random.Range(0, 180);
for (int i = 0; i < (20 + ((GM.instance.stage - GM.instance.stage % 10) / 5)); i++)
{
GM.instance.e_bl_angle.eulerAngles = new Vector3(0, 0, rnd_num + (180 * i / (20 + ((GM.instance.stage - (GM.instance.stage % 10)) / 5))));
Instantiate(bullet_enemy_1, rb2d_e.position, GM.instance.e_bl_angle);
}
}
}
これはカウントが180以上(fpsは約60なので約3秒)になってから30カウント(約0.5秒)(+stageによる補正)ごとにForの繰り返し数wayの弾を円形に放つようにしています。
弾側のスクリプトは
void Start()
{
speed = GM.instance.e_bl_base_spd * (1.0f + GM.instance.stage / 80.0f );
}
void Update()
{
move_delta = new Vector2(speed * Mathf.Cos(transform.rotation.eulerAngles.z * Mathf.PI / 180), speed * Mathf.Sin(transform.rotation.eulerAngles.z * Mathf.PI / 180));
transform.Translate(move_delta);
if (transform.position.x > 900 || transform.position.x < -100 || transform.position.y > 900 || transform.position.y < -100 || GM.instance.count < 180 || GM.instance.char_muteki_cnt > 0)Destroy(gameObject);
}
まず弾のスピードを指定して、スピードと弾が向いている方向から1フレームごとの移動距離を計算して毎フレームその分移動するようにしています。この場合、弾は直線に等速で移動します。そして弾が画面外に出たり、ステージが切り替わったり、プレイヤーが無敵状態になると弾が消えるようにしています。ちなみに、ところどころstageによる補正が入ってますがこれによりステージが進むごとに弾幕の難易度が上がるようになっています。
ここで、弾の軌道を曲げたい場合は例えば、
if (count > 30 && count <= 120) transform.Rotate(0, 0, 0.8f);
を弾側のUpdate()内に入れることで、指定カウント間1フレームごとに弾を徐々に回転させることになり曲がった軌道を疑似的に再現できます。なめらかに曲げる必要が無い(カクっと曲げたい)時は指定カウントで一気に回転でOKです。
また、弾の速度を変えたい場合はspeedをUpdate()内で指定カウントで変えるようにして、なめらかに速度を変えたい場合は上と同様に指定カウントを範囲で指定して徐々に変えるようにすればOKです。
基本的にはここまで書いたことで大体作りたい弾幕を作ることができると思います。思い浮かべている弾幕を、弾の生成タイミング・場所、弾の軌道に分けて考えて、それぞれをコードに起こせば完成です。ね、簡単でしょ?
本当はほかにも紹介したかったのですが、ここまでで既に文量がすごいことになっているので今回作成に一番苦労した弾幕の弾生成スクリプトを供養して最後とします。
星形弾幕
サムネと同じ弾幕です。
これの弾生成スクリプト(の一部)
if (GM.instance.count >= 180 && GM.instance.count % (120 - (GM.instance.stage - GM.instance.stage % 10) / 5) == 0)
{
size = 80;
for (int i = 0; i < (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15)); i++)
{
GM.instance.e_bl_angle.eulerAngles = new Vector3(0, 0, Mathf.Atan2(vangle.y, vangle.x) * 90 / Mathf.PI);
b_position = new Vector2(rb2d_e.position.x + size * (Mathf.Cos(18 * Mathf.PI / 180) + (Mathf.Cos(162 * Mathf.PI / 180) - Mathf.Cos(18 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))), rb2d_e.position.y + size * Mathf.Sin(18 * Mathf.PI / 180));
vangle.x = rb2d_e.position.x + size * (Mathf.Cos(18 * Mathf.PI / 180) + (Mathf.Cos(162 * Mathf.PI / 180) - Mathf.Cos(18 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))) - GM.instance.enemy_position.x;
vangle.y = rb2d_e.position.y + size * Mathf.Sin(18 * Mathf.PI / 180) - GM.instance.enemy_position.y;
GM.instance.e_bl_angle.eulerAngles = new Vector3(0, 0, Mathf.Atan2(vangle.y, vangle.x) * 90 / Mathf.PI);
Instantiate(bullet_enemy_7, b_position, GM.instance.e_bl_angle);
b_position = new Vector2(rb2d_e.position.x + size * (Mathf.Cos(162 * Mathf.PI / 180) + (Mathf.Cos(306 * Mathf.PI / 180) - Mathf.Cos(162 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))), rb2d_e.position.y + size * (Mathf.Sin(162 * Mathf.PI / 180) + (Mathf.Sin(306 * Mathf.PI / 180) - Mathf.Sin(162 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))));
vangle.x = rb2d_e.position.x + size * (Mathf.Cos(162 * Mathf.PI / 180) + (Mathf.Cos(306 * Mathf.PI / 180) - Mathf.Cos(162 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))) - GM.instance.enemy_position.x;
vangle.y = rb2d_e.position.y + size * (Mathf.Sin(162 * Mathf.PI / 180) + (Mathf.Sin(306 * Mathf.PI / 180) - Mathf.Sin(162 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))) - GM.instance.enemy_position.y;
GM.instance.e_bl_angle.eulerAngles = new Vector3(0, 0, Mathf.Atan2(vangle.y, vangle.x) * 90 / Mathf.PI);
Instantiate(bullet_enemy_7, b_position, GM.instance.e_bl_angle);
b_position = new Vector2(rb2d_e.position.x + size * (Mathf.Cos(306 * Mathf.PI / 180) + (Mathf.Cos(90 * Mathf.PI / 180) - Mathf.Cos(306 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))), rb2d_e.position.y + size * (Mathf.Sin(306 * Mathf.PI / 180) + (Mathf.Sin(90 * Mathf.PI / 180) - Mathf.Sin(306 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))));
vangle.x = rb2d_e.position.x + size * (Mathf.Cos(306 * Mathf.PI / 180) + (Mathf.Cos(90 * Mathf.PI / 180) - Mathf.Cos(306 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))) - GM.instance.enemy_position.x;
vangle.y = rb2d_e.position.y + size * (Mathf.Sin(306 * Mathf.PI / 180) + (Mathf.Sin(90 * Mathf.PI / 180) - Mathf.Sin(306 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))) - GM.instance.enemy_position.y;
GM.instance.e_bl_angle.eulerAngles = new Vector3(0, 0, Mathf.Atan2(vangle.y, vangle.x) * 90 / Mathf.PI);
Instantiate(bullet_enemy_7, b_position, GM.instance.e_bl_angle);
b_position = new Vector2(rb2d_e.position.x + size * (Mathf.Cos(90 * Mathf.PI / 180) + (Mathf.Cos(234 * Mathf.PI / 180) - Mathf.Cos(90 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))), rb2d_e.position.y + size * (Mathf.Sin(90 * Mathf.PI / 180) + (Mathf.Sin(234 * Mathf.PI / 180) - Mathf.Sin(90 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))));
vangle.x = rb2d_e.position.x + size * (Mathf.Cos(90 * Mathf.PI / 180) + (Mathf.Cos(234 * Mathf.PI / 180) - Mathf.Cos(90 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))) - GM.instance.enemy_position.x;
vangle.y = rb2d_e.position.y + size * (Mathf.Sin(90 * Mathf.PI / 180) + (Mathf.Sin(234 * Mathf.PI / 180) - Mathf.Sin(90 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))) - GM.instance.enemy_position.y;
GM.instance.e_bl_angle.eulerAngles = new Vector3(0, 0, Mathf.Atan2(vangle.y, vangle.x) * 90 / Mathf.PI);
Instantiate(bullet_enemy_7, b_position, GM.instance.e_bl_angle);
b_position = new Vector2(rb2d_e.position.x + size * (Mathf.Cos(234 * Mathf.PI / 180) + (Mathf.Cos(18 * Mathf.PI / 180) - Mathf.Cos(234 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))), rb2d_e.position.y + size * (Mathf.Sin(234 * Mathf.PI / 180) + (Mathf.Sin(18 * Mathf.PI / 180) - Mathf.Sin(234 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))));
vangle.x = rb2d_e.position.x + size * (Mathf.Cos(234 * Mathf.PI / 180) + (Mathf.Cos(18 * Mathf.PI / 180) - Mathf.Cos(234 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))) - GM.instance.enemy_position.x;
vangle.y = rb2d_e.position.y + size * (Mathf.Sin(234 * Mathf.PI / 180) + (Mathf.Sin(18 * Mathf.PI / 180) - Mathf.Sin(234 * Mathf.PI / 180)) * i / (10 + ((GM.instance.stage - GM.instance.stage % 15) / 15))) - GM.instance.enemy_position.y;
GM.instance.e_bl_angle.eulerAngles = new Vector3(0, 0, Mathf.Atan2(vangle.y, vangle.x) * 90 / Mathf.PI);
Instantiate(bullet_enemy_7, b_position, GM.instance.e_bl_angle);
}
}
試行錯誤の果てにこんなことになってしまいました。これはひどい(笑)。これでもスクリプトの一部ですが、この部分で弾を星型に生成しています。
まとめ
さて、ずいぶん長くなってしまいましたね。ここまで読んでくださった方ありがとうございます。長々とスクリプト等を書きましたが、unityの仕様をあまり把握できていなかったこともあり今回のゲーム作成はとにかく試行錯誤の繰り返しでした。色々具体的な数値が書いてありますが、ぶっちゃけた話、なぜその値にしたのかと聞かれても「なんかそれでうまくいったから」としか言いようが無い部分も多いです。結果的になんとか形にはなったので良かったですが、次回以降はもっとunityの仕様を把握してから作成に取り掛かりたいですね。
最後に、イベント当日までデバッグ&作品提出を手伝ってくださった先輩方、そしてこの作品をプレイしてくださった方ありがとうございました。
この作品をプレイする
以下のリンクからこの作品をプレイすることができます。