Rokiのチラ裏

学生による学習のログ

Unity 導入から簡単なスターファイターを作るまでのチュートリアル

学校やその周辺で、Unity が〜…とよく聞くのだが、なんだかんだ触った事がなかったので、先日初めて Unity を触ってみた。そこそこお遊びとしても楽しめたので、わざわざまとめておく事もないかなと思っていたのだが、まあ無駄な事はないだろうと思ったので記事に起こす事にした。一応このエントリを順に読み進めていけば以下のようなものが出来上がる。

対象読者としては、恐らくプログラムを一切書いた事がなくともそこそこできるのではないかと予想している。

インストー

Unity Technologies Japan から導入し、アカウントを作成しておく。アカウントはなくても Unity そのものは動作するが、アセットなどをアセットストアから取り込む事ができないので作っておいた方が楽だ。

プロジェクトの作成

インストールが終わったら Unity を起動する。New から新しいプロジェクトを作る。Project name と Organization は何でも良い。今回は一応 3D ゲームを作るので 3D を選択する。Location もどこでも良いわけだが、一応散らばらないように各々の任意のディレクトリを選択しておく。Create project でプロジェクトを生成する。

f:id:Rok1:20170723152505p:plain

諸々の初期設定

まずここは好みの問題なのだが、簡単のためにまずペインのレイアウトを変更しておく。デフォルトでは以下のようなレイアウトになっている。 f:id:Rok1:20170723153104p:plain これを以下のようにする。 f:id:Rok1:20170723153244p:plain 画像右上の Default となっているボタンをクリックし 2 by 3 に変更する。すると Hierarchy タブと Project タブと Inspector タブが並び、これはこれで使いにくいので Hierarchy の下に Project タブをドラッグして上記の画像のようなレイアウトにした。次に、Project タブ内を見ると、本プロジェクトのディレクトリ階層が表示されているので(まだ Assets しかないはずだが)その Assets の中に自分がこれから記述するコードを入れるディレクトリを作っておく。ここで階層を分けておくのは、後々アセットを外部から導入した時にそれらをごちゃ混ぜにならないようにするためだ。ディレクトリの作成は、Assets を右クリックし Create から Folder を選択する。ディレクトリ名は便宜上 MyDir とした。さらに Mydir の内部に src と prefab というディレクトリを作成しておく。これらの作業を終えると以下のようになっているはずだ。

f:id:Rok1:20170723154520p:plain:w240

次に本プロジェクトで使う 3D モデルをアセットストアから導入する。3D モデルは Maya などで自作する事もできるが、動かしたいだけならアセットストアから導入してしまった方が楽だ。アセットストアは Window から Asset Store で開く事ができる。今回はアセットストアから背景モデルと戦闘機二機を入手する。モデルは何でも良いのだが、便宜上背景モデルには 3 Skyboxes 2 、自機には Free SciFi Fighter、敵機には Space fighter を使う。それぞれ、アセットストア上部の Search からそのまま検索すれば出てくるはずだ。Download をクリックして完了すると Import するかのウィンドウが出てくるので Import をクリックする。全ての作業を終えると以下のようになっているはずだ。

f:id:Rok1:20170723155828p:plain:w240

ゲームシーンへの追加

次に、入手したアセットをゲームシーンへ追加する。追加は、モデルデータを Hierarchy タブにドラックアンドドロップする事で可能だ。まずは、自機と背景モデルと読み込む。Free SciFi Fighter の場合、モデルデータは /Assets/Meshes にあるのでこれをそのまま Hierarchy タブにドラックアンドドロップする。

f:id:Rok1:20170723171648p:plain 次に背景モデルを読み込む。背景モデルは Hierarchy の Main Camera から設定する。Main Camera を選択したら Inspector の Add Component で Skybox と検索し選択する。Inspector に Skybox が追加されているはずだ。さらに、Skybox の歯車のマークの下部にある円とその中心に点が描かれているボタンをクリックする。すると、Select Material ダイアログが表示されるので、任意の背景を選択する。選択に応じて、Game タブの背景も変わるはずだ。こちらでは、便宜上 BlueGreenNebular を選択してみた。この段階では、SciFi Fighter が目の前に来ていてとても見にくいので、カメラの位置を変えてやる。カメラの Inspector タブの Transform から任意にカメラの位置を変更する事ができる。取り敢えず、Position のY軸を 10、Z軸を -35 とした。これまでの作業を終えると以下のようになっているはずだ。

f:id:Rok1:20170723172637p:plain ここいらで一度保存しておこう。/Assets/MyDir/ 配下に先ほどと同じように今度は scene というディレクトリを作っておく。Windows なら Ctrl + s で、Mac なら ⌘ + s でシーンの保存ができる。これを行うと保存先を指定できるので今作成した /Assets/MyDir/scene ディレクトリ内部に該当シーンを保存する。シーン名は、便宜上 game_scene とした。保存が完了すると Hierarchy タブ内の該当シーンが、保存したシーン名になっているはずだ。

シューティングゲーム本体の作成

シューティングゲームそのものを先に作るか、メニュー画面などを先に作るかはその方針によって様々になるだろうが、取り敢えずまずはシューティング要素そのものを作る事とした。で、まずは自機を制御するコードを書く。Unity では言語としては C#, javascript, Boo(Python) という選択指があるが C# の利用を勧めるユーザーが多いようなので C# を利用する。Unity を利用するにあたってどの言語を利用するかは最終的にはその状況や過去の経験に依存する事と思うのでその辺りはよく考えた方が良いだろう。

ファイルの作成とエディタの設定

ソースコードの作成もディレクトリ作成の操作と似たような感じで右クリックから Create 、C# Script というように選択する。これは、/Assets/MyDir/src/ 内に作成する。ファイル名は starfighter.cs とした。

f:id:Rok1:20170723175214p:plain:w240

早速コードを書きたいところだがその前に、Unity は任意のエディターでコードを編集する事ができる。デフォルトでは monodevelop を使うようになっている。これで良ければそのままで問題ないのだが、筆者は普段 vim を使っておりどうせならば vim で編集したいところなので、そのように設定した。なのでそのようにしたくない場合、この部分は飛ばして問題ない。
さて Mac の場合、普通にターミナルエミュレータなどで vim を使っているのであれば大抵それは普通の vim であり macvim ではないと思うのだが、筆者は macvim を持っていなかったのでそれを導入した。特別日本語を使うとも思わないが、もしかしたら今後そういう事もあるかもしれないと思ったので取り敢えず以下のものを入れた。導入方法はリンク先にある通りだ。


導入したら、Preferences の External Toolsから、macvim を設定する。しかし単純に macvim の実行ファイルを指定するだけだと Unity から呼び出す度に新しいウィンドウが生成されてしまうのでこれはよくない。そこで、macvim を以下のように設定する。

f:id:Rok1:20170723180230p:plain:w300

さらに当然 vim に補完させたいので、 GitHub - kitao/unity_dict: Generates the keyword list of Unity for Vim を利用して vim に辞書を登録しておく。

自機を動かす

先ほど作成した starfighter.cs というファイルを Project タブの /Assets/MyDir/src/ から開くと以下のようなコードが既に構成されているはずだ。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    // Use this for initialization
    void Start () {
        
    }
    
    // Update is called once per frame
    void Update () {
        
    }
}

Start 関数はこのオブジェクトが構築された時、一番初めに一度だけ呼ばれる関数であり、いわばコンストラクタのように動く。Update 関数は毎フレームごとに呼ばれるメインループの関数だ。見てわかる通り至って単純であり、ゲーム内のオブジェクト間の処理や諸々の設定は全て GUI や自動設定によって勝手に Unity が済ましてくれるため殆どコードを書く必要がないのだ。
取り敢えず、まずはキーボードの入力に従って自機が動くようにしよう。入力は矢印キーから行うことにしよう。

// starfighter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();

    // Use this for initialization
    void Start () {
        
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    // Update is called once per frame
    void Update () {
        get_key();
    }
}


Input.GetAxis("Vertical")によって上下キーの入力に応じて -1~1 の値を取得、Input.GetAxis("Horizontal")によって左右キーの入力に応じて -1~1 の値を取得する事が出来る。if 文を else if とすると斜め移動を禁止できるのでそこらへんは好みで良い。transform.Translate 関数によって、自身の方向と距離を定める事ができる。保存したら、Hierarchy 内の SciFi_Fighter_AK5 に starfighter ファイルをドラックアンドドロップする。SciFi_Fighter_AK5 を Hierarchy から選択し Inspector タブを確認する。Starfighter(Script) が追加されていれば今書いたコードがそのゲームオブジェクトと紐づいたことを示す。これで、もう自機が上下左右のキーボード入力によって移動するようになったはずだ。上部の ▶️ ボタンをクリックすると Game タブでシミュレートが行える。コード中にエラー箇所があれば下部にその旨を示すエラーメッセージが行番号付きで示されているはずなのでそれを確認して対応する。

さて、自機の移動ができるようになったのは良いが、このままだと自機が永遠に画面外へ移動できてしまう。移動範囲を画面内に納めるよう制御を加えよう。まずは x 軸を設定する。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();
    
    public const float max_x = 35.0f;

    // Use this for initialization
    void Start () {
        
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    void check_position(){
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(transform.position.x,-max_x,max_x);
        transform.position = pos;
    }

    // Update is called once per frame
    void Update () {
        get_key();
        check_position();
    }
}

max_x が x 軸の最大の値だ。publicにしているのは、Inspector タブからこの値を任意に変更できるようにするためだ。SciFi_Fighter_AK5 を Hierarchy タブから選択して Starfighter(Script) の Max_x を弄ると自機の移動可能な領域を変えられるので任意に設定すると良いだろう。次に、Y 軸に対する設定だ。Y 軸に対しても同じように移動領域に制限を設けるのも良いが、今回は領域は設けず、代わりにカメラを自機について行かせるようにしよう。
/Assets/MyDir/src 配下に camera_control.cs を作成し以下のように記述する。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class camera_control : MonoBehaviour {

    Vector3 diff;
    float diff_x;

    // Use this for initialization
    void Start () {
        diff = transform.localPosition;
        diff_x = diff.x;
    }
    
    // Update is called once per frame
    void Update () {
        if(GameObject.Find("SciFi_Fighter_AK5")){
            Vector3 pre = GameObject.Find("SciFi_Fighter_AK5").transform.localPosition;
            transform.localPosition = new Vector3(diff_x,pre.y + diff.y,pre.z + diff.z);
        }
    }
}

シーン中の ”SciFi_Fighter_AK5″ というオブジョクトの位置を取得し、それに対してそのオブジェクトの最初の位置を足した値を現在のカメラの位置として設定している。これを、Hierarchy の Main Camera にドラックアンドドロップをして、コードと紐づける。
以上で、自機が画面から移動しすぎて消えてしまったり前に行きすぎて小さくなってしまったりといった事はなくなったはずだ。▶️ ボタンをクリックして確認できる。

弾を出す

次に弾を出していく。弾の 3D モデルはまたもやアセットストアから利用してしまおう。取り敢えず、Simple Particle Pack というものを使うことにした。アセットストアからダウンロードしインポートしたら /Assets/SimpleParticle/Resources/Directional/ から任意の *.prefab を選択して Hierarchy にドラックアンドドロップする。便宜上、取り敢えず Torch(Blue) を入れた。このままだと SciFi_Fighter_AK5 の上に今 Hierarchy に移した Torch(Blue) が重なってしまい様子がよく見えないと思うので、一旦 SciFi_Fighter_AK5 を非表示にしよう。SciFi_Fighter_AK5 を選択して Inspector タブの左側にあるチェックを外すと非表示にする事ができる。さて、ここまでの作業が完了すると以下のようになっているはずだ。

f:id:Rok1:20170723190342p:plain

さて、この弾に対して制御を与えていく。/Assets/MyDir/src 配下に bullet というファイル名で C# ファイルを作成し、以下の内容を記述する。

// bullet.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class bullet : MonoBehaviour {

    float speed = 3;

    // Use this for initialization
    void Start () {
        Destroy(this.gameObject,5);
    }
    
    // Update is called once per frame
    void Update () {
        transform.Translate(0,0,speed);
    }
}

Destroy 関数によって対象オブジェクトを破棄する事ができる。これは、5 と指定しているので Start 関数が実行された 5秒後に自身を破棄するようにしている。また、弾は打つと前方に向かっていく動きをするはずだから、常に前方に向かうように設定している。
このソースファイルを先ほどと同じように、今度は Torch(Blue) にドラッグアンドドロップをし紐づける。

次にこの Torch(Blue) をプレハブとして作成しておく。プレハブとは…

適切な値のコンポーネントを追加した個々のゲームオブジェクトでシーンを構築するのは簡便ですが、シーン内に繰り返し使用されるNPCや小道具、背景パーツなどのオブジェクトが有る場合には、問題点もあります。単純にオブジェクトをコピーして複製すると、それらのプロパティ全てが独立したものになってしまいます。一般的には、ある特定オブジェクトのインスタンス全てのプロパティを揃えた い事が多いので、シーン内でオブジェクトを編集したら、その編集をコピー全てに適用したい、と考えるでしょう。 幸いなことに、Unity は、GameObject オブジェクトのコンポーネントとプロパティーすべてを格納する Prefab アセットタイプを持っています。プレハブはテンプレートとして使われ、シーン内に新しいオブジェクトインスタンスを作成することができます。プレハブアセットに加えられたすべての編集内容はすぐに生成されたすべてのインスタンスに反映されますが、各インスタンス別にコンポーネン トを オーバーライド したり、設定したりできます。 https://docs.unity3d.com/ja/current/Manual/Prefabs.html 引用

プレハブ化は簡単で、先ほど Hierarchy に追加した Torch(Blue) を /Assets/MyDir/prefab 配下にドラッグアンドドロップする事で Torch(Blue) のプレハブ化が完了する。尚、今 Hierarchy 上にある Torch(Blue) は使わないので削除しておく。わかりやすいよう、今作られた Torch(Blue).prefab を my_bullet.prefab に改名しておく。ここまでの作業が完了すると以下のようになっているはずだ。f:id:Rok1:20170723193152p:plain

次に、今度はスペースキーを押すと、自機がこの弾を射出するようにしよう。/Assets/MyDir/src/starfighter.cs を編集する。

// starfighter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();
    
    public GameObject bullet;
    public float interval = 0;
    public float max_x = 35.0f;

    // Use this for initialization
    void Start () {
        
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    void check_position(){
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(transform.position.x,-max_x,max_x);
        transform.position = pos;
    }

    void shoot(){
        interval += Time.deltaTime;
        if(Input.GetKey("space")){
            if(interval >= 0.1f){
                interval = 0.0f;
                Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            }
        }
    }


    // Update is called once per frame
    void Update () {
        get_key();
        check_position();
        shoot();
    }
}

追加したのは、shoot関数とその内部で使うGameObjectintervalだ。GameObjectはプレハブを読み込む事のできる型でこれを public とする事で GUI から任意のプレハブを設定できるようにしている。intervalは、スペースを連続的にまたは継続的に押し続けた場合に、弾をどれだけ射出するかを決定する、その名の通り弾の射出のインターバルを定める変数だが、これも public とする事で Inspector タブから弾の射出度を任意に打ち込めるので調節が楽だ。
編集を終えたら、Hierarcy から SciFi_Fighter_AK5 を選択し、Inspector の Starfighter(Script) を確認する。そこに Bullet と Interval という項目が追加されているはずなので、Bullet に /Assets/MyDir/prefab/my_bullet.prefab をドラックアンドドロップする。Interval は 0 となっていると思うが、その値は先ほども述べた通り弾の射出頻度を設定するので、任意に変えて調節すると良いかもしれない。ここまでの作業が完了すると、以下のようになっているはずだ。

f:id:Rok1:20170723202900p:plain

さて、これでもう弾を射出できるようになっているので、▶️ボタンをクリックして試してみよう。

敵機を出す

まずは敵機を Hierarchy に読み込む必要がある。/Assets/SpaceFighter/Art Assets/FighterMedeum_FBX.fbx を Hierarchy タブへドラッグアンドドロップする。取り敢えず、自機に重なって見えないので適当に Inspector からPosition, Rotation, Scaleを弄って適切な大きさに設定する。

f:id:Rok1:20170723204048p:plain

画像にもある通り、敵機は自機と対面してくるはずなので、Rotation の Y軸は 180 になるはずだ。ではこの敵機に対して処理を書いていく。/Assets/MyDir/src 配下に enemy.cs を作成し記述する。

// enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class enemy : MonoBehaviour {

    public GameObject bullet;
    float z_speed = 0.7f;
    public float interval = 0;

    // Use this for initialization
    void Start () {
        
    }

    void shoot(){
        Quaternion quat = Quaternion.Euler(0,180,0);
        interval += Time.deltaTime;
        if(interval >= 0.3f){
            interval = 0.0f;
            Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),quat);
        }
    }
    
    // Update is called once per frame
    void Update () {
        transform.Translate(0,0,z_speed);
        shoot();
    }
}

敵機は何もしなくとも自動でこちらに迫ってきてほしいので、そのように設定している。これを Hierarchy の FighterMideum_FBX にドラックアンドドロップしてコードを紐づける。そして、敵の弾は自機と違うものにしたいので、/Assets/SimpleParticlePack/Resources/Directional から任意のプレハブを Hierarchy にドラックアンドドロップする。今回は便宜上、Torch(Green) を選んだ。次に Torch(Green) を Hierarchy から選択し、Transform の Rotation の Y軸を 180度回転させておく。これは、弾が敵側からこちら側に向かってくるはずだからだ。その後、/Assets/MyDir/src/bullet をこの Torch(Green) にドラッグアンドドロップしてコードを紐づける。さらに、この Torch(Green) を Hierarchy から /Assets/MyDir/prefab にドラッグアンドドロップしてそれを enemy_bullet.prefab へと改名し、Hierarchy の enemy_bullet を削除する。ここまでの作業が完了すると以下のようになっているはずだ。

f:id:Rok1:20170723205558p:plain

そして、FighterMedium_FBX を Hierarchy から選択し、Enemy(Script) の Bullet に /Assets/MyDir/prefab/enemy_bullet.prefab を ドラッグアンドドロップして紐づける。これで敵が進行しながら弾を発車するようになったはずだ。次に、敵の出現位置を設定していく。敵の出現位置は、自機の前方のうちランダムな x 軸上(画面内)に一定間隔で出現すれば良いだろう。この処理は、本当は敵機のコード上で管理した方が後々に機能を追加する事を考えると適切なのだが、取り敢えず簡単に自機の座標を使うために、starfighter.cs にこの処理を記述する。

// starfighter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();
    
    public GameObject bullet;
    public GameObject enemy;
    public float interval = 0;
    public float max_x = 35.0f;
    public float enemy_interval = 4.0f;
    float enemy_time = 0;
    public float enemy_distance = 200;

    // Use this for initialization
    void Start () {
        
    }

    void get_enemy(){
        Quaternion quat = Quaternion.Euler(0,180,0);
        enemy_time += Time.deltaTime;
        if(enemy_time >= enemy_interval){
            enemy_time = 0.0f;
            Instantiate(enemy,new Vector3(Random.Range(-max_x,max_x),transform.position.y,transform.position.z + enemy_distance),quat);
        }
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    void check_position(){
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(transform.position.x,-max_x,max_x);
        transform.position = pos;
    }

    void shoot(){
        interval += Time.deltaTime;
        if(Input.GetKey("space")){
            if(interval >= 0.1f){
                interval = 0.0f;
                Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            }
        }
    }


    // Update is called once per frame
    void Update () {
        get_key();
        check_position();
        shoot();
        get_enemy();
    }
}

追加したのは、get_enemy 関数だ。実機 + enemy_distance の距離でenemy_timeに設定した秒数で出現させる。画面内の x 軸上に、ランダムに出現させるために、Random.Range関数を用いている*1。次に FighterMedium_FBX を /Assets/MyDir/prefab へとドラッグアンドドロップし、プレハブ化する。Hierarchy 上の FighterMedium_FBX はもう使わないので削除する。ここまでの作業が完了すると以下のようになっているはずだ。 f:id:Rok1:20170723215727p:plain

この段階で、実機の前方に敵機がランダムに発生するはずだ。▶️ で確認できる。

衝突判定をつける

まずは、こちらの撃った弾が敵機に当たるようにする。/Assets/MyDir/prefab/my_bullet.prefab を Hierarchy タブにドラッグアンドドロップする。Hierarchy に移した my_bullet を選択して、Inspector の Add Component で Box Collider を選択する。これが衝突判定を設定するコンポーネントとなる。まず Is Trigger にチェックを入れる。次に Hierarchy から SciFi_Fighter_AK5 を選択して一旦非表示にしてから Box collider の Center や Size を弄って任意に衝突範囲を設定する。

f:id:Rok1:20170723221356p:plain

設定が完了したら、Inspector タブ上部の右側にある Apply ボタンを押して適用し、Hierarchy タブから my_bullet を削除する。続けて、/Assets/MyDir/prefab/FighterMedium_FBX.prefab を Hierarchy タブにドラッグアンドドロップし、Inspector タブの Add Component から Box Collider と Rigidbody を追加する。Box Collider は同じく Is Trigger のチェックをしてから衝突判定の範囲を定める。Rigidbody は Unity の物理エンジンの管理下に置くものであるが、今回重力は用いないので Use Gravity のチェックを外しておく。

f:id:Rok1:20170723222603p:plain

設定が完了したら、同じく Apply をクリックし、Hierarchy から FighterMedium_FBX を削除する。次に、弾が当たった場合に機体を爆発させたいので爆発のエフェクトを作成する。これは、/Assets/SimpleParticlePack/Resources/Explosions 内にあるので任意のエフェクトを選ぼう。便宜上、Explosion01c を選んだ。これを、Hierarchy にドラッグアンドドロップし、/Assets/MyDir/prefab にドラッグアンドドロップしておく。これで、爆発エフェクトの完成だ。Hierarchy 上のものはもう使わないので Explosion01c を削除する。さて、先ほどの設定で弾と敵があたった時の衝突判定が取得できるようになったので、/Assets/MyDir/src/enemy.cs を編集して衝突があった場合の処理を書こう。

// enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class enemy : MonoBehaviour {

    public GameObject bullet;
    public GameObject explosion;
    float z_speed = 0.7f;
    public float interval = 0;

    // Use this for initialization
    void Start () {
        
    }

    void shoot(){
        Quaternion quat = Quaternion.Euler(0,180,0);
        interval += Time.deltaTime;
        if(interval >= 0.3f){
            interval = 0.0f;
            Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),quat);
        }
    }
    
    // Update is called once per frame
    void Update () {
        transform.Translate(0,0,z_speed);
        shoot();
    }

    void OnTriggerEnter(Collider coll){
        if(coll.gameObject.tag == "Player_Bullet"){
            Instantiate(explosion,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            Destroy(this.gameObject);
        }
    }
}

OnTriggerEnterに衝突判定があった場合の処理を記述する。coll.gameObject.tagとはこれから弾に設定するユニークなタグであり、これをプレイヤーの弾であった場合に関する分岐に利用する。explosionオブジェクトを新たに生成しているが、これは爆発のエフェクトを GameObject に差し込む事を前提としている。その後、Destroy関数によって自身を破棄する。
あとは、タグの設定と、爆発オブジェクトを設定するだけだ。/Assets/MyDir/prefab/my_bullet.prefab を Hierarchy にドラッグアンドドロップして、Inspector から Tag を設定する。Add Tag から “Player_Bullet” というタグを新規に作成し、my_bullet に適用し Apply をクリックする。

f:id:Rok1:20170723225350p:plain:w250

そして Hierarchy から my_bullet を削除する。次に、/Assets/MyDir/prefab/FighterMedium_FBX.prefab を Hierarchy にドラッグアンドドロップし、 Inspector から Enemy(Script) の Explosion に /Assets/MyDir/prefab/Explosion01c.prefab をドラッグアンドドロップする。これで、こちらの射出した弾が敵に当たると、爆発して敵が消滅するようになったはずだ。

自機に弾が当たるようにする

後は自機に弾が当たればゲーム部分はひとまず完成だ。まずは /Assets/MyDir/src/starfighter.cs を編集して衝突判定のコードを先に書いてしまう。

// starfighter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();
    
    public GameObject bullet;
    public GameObject enemy;
    public GameObject explosion;
    public float interval = 0;
    public float max_x = 35.0f;
    public float enemy_interval = 4.0f;
    float enemy_time = 0;
    public float enemy_distance = 200;

    // Use this for initialization
    void Start () {
        
    }

    void get_enemy(){
        Quaternion quat = Quaternion.Euler(0,180,0);
        enemy_time += Time.deltaTime;
        if(enemy_time >= enemy_interval){
            enemy_time = 0.0f;
            Instantiate(enemy,new Vector3(Random.Range(-max_x,max_x),transform.position.y,transform.position.z + enemy_distance),quat);
        }
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    void check_position(){
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(transform.position.x,-max_x,max_x);
        transform.position = pos;
    }

    void shoot(){
        interval += Time.deltaTime;
        if(Input.GetKey("space")){
            if(interval >= 0.1f){
                interval = 0.0f;
                Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            }
        }
    }


    // Update is called once per frame
    void Update () {
        get_key();
        check_position();
        shoot();
        get_enemy();
    }

    void OnTriggerEnter(Collider coll){
        if(coll.gameObject.tag == "Enemy_Bullet"){
            Instantiate(explosion,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            Destroy(this.gameObject);
        }
    }
}

続いて Hierarchy の SciFi_Fighter_AK5 を選択して Inspector から Explosion に /Assets/MyDir/prefab/Explosion01c.prefab を設定する。続けて /Assets/MyDir/prefab/enemy_bullet.prefab を Hierarchy にドラッグアンドドロップして、先ほどと同じように、これに対してタグをつける。タグ名は、"enemy_bullet" だ。

f:id:Rok1:20170723231249p:plain:w250

Apply をクリックして Hierarchy から削除する。そして、SciFi_Fighter_AK5 の Inspector で Rigidbody と Mesh Collider を設定して下図のようにする。

f:id:Rok1:20170723232228p:plain:w250

最後に、/Assets/MyDir/prefab/enemy_bullet を Hierarchy にドラッグアンドドロップして Box Collider をコンポーネントに追加して Apply 、Hierarchy から削除する。これで、自機と敵機両方とも、射出した弾に当たれば爆発して消滅するようになったはずだ。

メニュー画面の作成

ゲーム本体部分は完成したので、次にメニュー画面を作成する。Hierarchy タブ上で右クリックし、UI から Button をクリックする。続いて Scene タブの 2D ボタンをクリックし Hierarchy から今作成した Button をダブルクリックすると、作成した Button が確認できる。Button から Text を選択し Inspector からボタンの内容を変更できる。

f:id:Rok1:20170723233606p:plain

ここから、ボタンを押した時の処理を加えていく。/Assets/MyDir/src/ に game_control.cs を作成し以下のように記述する。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class game_control : MonoBehaviour {

    public GameObject Start_btn;
    public GameObject player;
    public GameObject Reset_btn;
    bool game_status = true;
    
    // Use this for initialization
    void Start () {
        
    }
    
    // Update is called once per frame
    void Update () {
        if(!game_status){
            Reset_btn.SetActive(true);
        }
    }

    public void StartButton(){
        Instantiate(player);
        Start_btn.SetActive(false);
    }

    public void ResetButton(){
        Application.LoadLevel("game_scene");
    }
}

スタートボタンが押された時に自機を生成するようにする事で、ゲームの開始を行うようにしてある。また、game_statusをパブリックに持たせて、この状態に応じてリセットボタンを出すようにする。つまり、このフラグにゲームオーバーの状態を外部から知らせる必要がある。これを Hierarchy の Main Camera にドラッグアンドドロップをして紐づける。次に、Hierarchy から Button を選択、Inspector 下部の On Click()を下図のように設定する。

f:id:Rok1:20170723235133p:plain:w250

これで、このボタンが押された時に、StartButton 関数が呼ばれるようになる。次に、Hierarchy にある SciFi_Fighter_AK5 を /Assets/MyDir/prefab にドラッグアンドドロップしてプレハブ化し、Hierarchy から削除する。尚、今までは、 Hierarchy タブの中にあった自機をカメラで追う設定だったが、ボタンを押してから生成される自機に対して追尾するようにする必要があるため、/Assets/MyDir/src/camera_control.cs にて追尾するオブジェクトの名前を変更してやる必要がある。

// camera_control.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class camera_control : MonoBehaviour {

    Vector3 diff;
    float diff_x;

    // Use this for initialization
    void Start () {
        diff = transform.localPosition;
        diff_x = diff.x;
    }
    
    // Update is called once per frame
    void Update () {
        if(GameObject.Find("SciFi_Fighter_AK5(Clone)")){
            Vector3 pre = GameObject.Find("SciFi_Fighter_AK5(Clone)").transform.localPosition;
            transform.localPosition = new Vector3(diff_x,pre.y + diff.y,pre.z + diff.z);
        }
    }
}

次に、リセットボタンを作成する。スタートボタンと同じく作成する。

f:id:Rok1:20170724000403p:plain:w250

さらに、/Assets/MyDir/src/starfighter.cs 内でゲームオーバーの判定を行うために、game_control の game_status を反転させる。

// starfighter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();
    
    public GameObject bullet;
    public GameObject enemy;
    public GameObject explosion;
    public float interval = 0;
    public float max_x = 35.0f;
    public float enemy_interval = 4.0f;
    float enemy_time = 0;
    public float enemy_distance = 200;

    // Use this for initialization
    void Start () {
        
    }

    void get_enemy(){
        Quaternion quat = Quaternion.Euler(0,180,0);
        enemy_time += Time.deltaTime;
        if(enemy_time >= enemy_interval){
            enemy_time = 0.0f;
            Instantiate(enemy,new Vector3(Random.Range(-max_x,max_x),transform.position.y,transform.position.z + enemy_distance),quat);
        }
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    void check_position(){
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(transform.position.x,-max_x,max_x);
        transform.position = pos;
    }

    void shoot(){
        interval += Time.deltaTime;
        if(Input.GetKey("space")){
            if(interval >= 0.1f){
                interval = 0.0f;
                Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            }
        }
    }


    // Update is called once per frame
    void Update () {
        get_key();
        check_position();
        shoot();
        get_enemy();
    }

    void OnTriggerEnter(Collider coll){
        if(coll.gameObject.tag == "Enemy_Bullet"){
            Instantiate(explosion,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            Destroy(this.gameObject);
            GameObject.Find("Main Camera").GetComponent<game_control>().game_status = false; // 反転
        }
    }
}

最後に、Main Camera の Inspector から、Game_control(Script) に、各ゲームオブジェクトを設定する。Start_btn には Hierarchy のStartButton を Player には /Assets/MyDir/prefab/SciFi_Fighter_AK5.prefab を Reset_btn には Hierarchy の ResetButton を設定し、ResetButton を非表示にする。

f:id:Rok1:20170724003604p:plain:w250

これで、スタートボタンから始まり、ゲームオーバーするとリセットボタンが表示されるようになった。これで一通り完成である。

ゲームのビルド

最後にビルドであるが、File から Build Settings で様々なプラットフォーム用に出力設定が行える。取り敢えず今回は PC,Max and Linux Stand Arone に設定する。Build and Run として以下のようにプレイできる。

f:id:Rok1:20170724004840g:plain

ちょっとスピードが全体的に早すぎる気もするが、まぁそこらへんはパラメータを変えれば程よい感じになるだろう。

総括

という感じで、Unity は、このような簡単なゲームであればあまりコードを書かずに、殆どを GUI の操作で済ませる事ができる。しかし、逆に多くをラップされている分、内部で何が起こっているのかはしっかりとドキュメントなど読んで把握しなければならないが、そこそこ把握できれば恐らくなんでも作れるだろう。このチュートリアルが何かの役に経てば光栄である。

個人的には、自作でゲームエンジンを作ってみたくなった。

*1:乱数生成アルゴリズムについての記述が公式ドキュメントにはないが、一応プラットフォーム固有のランダムジェネレータを用いているようだ。気に入らなければ同リンクから fast PRNG を利用できる。