Unity で C# スクリプト：敵を撃破する3Dゲームを作る

# Unity で C# スクリプト：敵を撃破する3Dゲームを作る

こんにちは、@studio_meowtoon です。今回は Windows 11 の Unity アプリ開発環境で、C# 言語で原始的な3Dゲームを作成する方法を紹介します。
![csharp_in_unity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/0a784c1a-f580-0d22-cafc-93c450f28461.png)

## 目的

Unity エディターで C# 言語を用いて原始的な3Dゲームを作成します。

:::note warn
Unity を使った簡単な3Dゲーム開発でも、通常は3Dモデルが必要です。この記事では Unity エディターのみを使って基本的な3Dゲームの骨組みを作り、理解を深めます。
:::

## ダウンロード

:::note info
この記事の内容の Unity プロジェクトは、GitHub からダウンロードできます。
:::

https://github.com/3d-gamedev/battle-tank/releases/tag/v0.1.0

## 開発環境

+ Windows 11 Home 22H2
+ Unity 2022.3.10f1 LTS
+ Visual Studio Community 2019 Version 16.11.19

## Unity プロジェクトの作成

Unity Hub を起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/703641b0-eafd-ed73-d00c-d6be48b8e108.png)

[新しいプロジェクト] ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/21f26a23-cf39-7530-084a-3790af71e939.png)

[3D コア] ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e1a4c431-fdd6-4924-fe3c-d2094b26877a.png)

プロジェクト名を入力して、[プロジェクトを作成] ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/9fd27444-bea5-bb66-57f4-28012694d3de.png)

空の Unity プロジェクトが立ち上がります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/97fc24c2-b458-2b1a-ce0a-11aaacef6e65.png)

:::note info
ここまでの作業で空の Unity プロジェクトを作成することができました。
:::

## 3Dゲームの実装

### 地面を作る

左の [ヒエラルキー] タブ内で右クリメニューを表示して、[3D オブジェクト] > [キューブ] と選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/7b9a3ec9-a67a-ea1d-a024-8220774d4485.png)

ここでは、[Cube] を、[Ground] とリネームしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/6a9e1c7c-5ec2-1efe-7a6c-2f9f954f372f.png)

右の [インスペクター] タブ内の [Transform] のスケールで X: 100、Z: 100 と入力します。また、位置で Y: -0.5 と入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/018a164c-682f-9ce7-1c1a-765eb8e717cb.png)

:::note info
Unity の単位 1 は **1m** になります。ここまでの作業で、100m四方の地面を作成することができました。
:::

### 地面に色を塗る

#### マテリアルの作成

下部の [プロジェクト] タブから [Assets] > [Materials] フォルダを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/7189b6ce-aad8-e37b-a8c5-3a09a6907b9b.png)

[Materials] フォルダで右クリメニューから、[作成] > [マテリアル] と選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/1ecf7bc4-a64e-c0ed-5548-4d6bbf9272eb.png)

ここでは、[ground_green] という名前を付けました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/61b8535b-7aa9-1360-3e2c-474305b9f0b8.png)

右の [インスペクター] タブ内の [Main Maps] 項目の [アルベド] をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/266a590b-8187-e5b8-8e8a-f5d37be018b0.png)

[色] 選択ダイアログで [HSV] に切り替えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/fb2a75c7-d646-0787-cde5-36728041d13a.png)

ここでは、H: 120、S: 50、V: 60 と設定しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/03aadb79-d666-677c-8cf4-2bd2dac16314.png)

このような緑色のマテリアルが作成されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/1d9ef45a-9ef1-3747-f5a9-3a2df6bbfa3d.png)

#### マテリアルを適用

[ground_green] マテリアルをつかみ、[シーン] タブ内の [Ground] オブジェクトにドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/758bc066-de17-476d-47fb-42fde2e5206a.png)

[Ground] オブジェクトに [ground_green] マテリアルが適用されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/320e84fc-7b73-fc49-64c9-3dcc9c201ef5.png)

:::note info
ここまでの作業で、緑色の地面を作成することができました。
:::

### 自機を作る

左の [ヒエラルキー] タブ内で右クリメニューを表示して、[3D オブジェクト] > [キューブ] と選択し、[Player] オブジェクトを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/f88e4c96-4dfa-1675-d348-b32c14e7cba8.png)

右の [インスペクター] タブ内の [Transform] の位置で Y: 0.5 と入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/43d47902-9bb1-b62f-78ee-8b29b458e8a0.png)

:::note info
ここまでの作業で、Player オブジェクトを作成することができました。
:::

### 自機を操作する

#### Rigidbody コンポーネントを追加

:::note warn
Rigidbody を使うと、ゲームオブジェクト を物理特性によって制御する事ができるようになります。
:::

[Player] オブジェクトを選択します。右の [インスペクター] タブ内の [コンポーネントを追加] ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/942f6bf2-ce78-b929-c1c3-223faa5a447b.png)

検索から [Rigidbody] を選択して、[Rigidbody] ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/207b1b31-541d-e85b-e548-a81c25d94836.png)

[Player] の [インスペクター] タブ内に [Rigidbody] コンポーネントが追加されました。回転を固定を全てチェックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/491e25fe-1649-562c-61ef-b9bf60a6a317.png)

#### カメラの設定

メインカメラを自機の子オブジェクトします。

[ヒエラルキー] の [Main Camera] をドラッグして、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/f787fb91-a6b9-1381-3a31-84e333a4979f.png)

[Player] オブジェクトの子オブジェクトに設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/c39644eb-721e-3dff-5276-639d5f4d9557.png)

カメラが映す角度を調整します。

右の [インスペクター] タブ内の [Transform] の位置で Y: 1.5、Z: -4 と入力します。また、回転で X: 15 と入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/c259f60f-a44e-8ec2-fb98-35b1849d52d4.png)

[ゲーム] タブで確認すると、カメラが自機を後方から捉える視点となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/c825186d-ad0f-1484-c06e-8fdc19275780.png)

#### Input System のインポート

操作したキーを入力値として扱います。

[ウィンドウ] > [パッケージマネージャー] を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/0bb0c1e3-5798-1d62-ffb0-482c9c40dd82.png)

[パッケージ:Unity レジストリ] から [Input System] を検索します。[Input System] を選択して [インポート] ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/1c20aba4-f0c1-7be2-90d5-b5e52e3ef848.png)

[ヒエラルキー] タブ内で右クリメニューから、[UI] > [イベントシステム] を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/f903d95c-863a-5ec5-b529-08f1c5d87cbd.png)

[EventSystem] オブジェクトが追加されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/5011c318-7561-a66a-d5ef-410a772b9abd.png)

#### UniRx ライブラリのインポート

イベント処理に特化した UniRx ライブラリを導入します。

[ウィンドウ] > [アセットストア] を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/4cdd5f65-fdc1-a98f-dac5-2e59ef72271c.png)

[Search online] ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/ae8f2f3b-5491-a098-2388-618e158a2641.png)

UniRx をマイアセットに追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/d331107e-268f-8baa-31de-217c1802f386.png)

[ウィンドウ] > [パッケージマネージャー] を選択します。

[パッケージ:マイアセット] から、[UniRx] を選択して [インポート] ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/8710de70-6cde-0dec-938e-61b6b062495d.png)

[Examples] フォルダのチェックを外して、[インポート] ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/88b4e5c5-02c7-3fbe-50ac-54ad116a7658.png)

[プロジェクト] タブ内の [Assets] > [Plugins] フォルダに [UniRX] がインストールされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/245c86be-e0ac-6240-909e-c4458b0b0495.png)

#### スクリプトの実装

Player オブジェクトを操作する C# スクリプトを作成します。

[Assets] > [Scripts] フォルダを作成します。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/ab902ac9-de50-c174-ea96-45e063bc9f78.png)

[Scripts] フォルダで右クリメニューから、[作成] > [C# スクリプト] と選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/53da5fe4-b08a-2ae5-3f2f-b3e09c5add43.png)

ここでは、[Player] という名前のスクリプトを作成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/6cd967c0-5520-8f0e-a175-8827382f24be.png)

スクリプトを実装します。

```csharp:Assets\Scripts\Player.cs
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using static UnityEngine.GameObject;
using UniRx;
using UniRx.Triggers;

namespace GameDev {
    /// <summary>
    /// player script.
    /// </summary>
    public class Player : MonoBehaviour {

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // Fields [noun, adjectives] 

        ButtonControl _a_button, _b_button, _up_button, _down_button, _left_button, _right_button;

        // Start is called before the first frame update.
        void Start() {
            /// <summary>
            /// キー入力を取得します
            /// </summary>
            this.UpdateAsObservable()
                .Subscribe(onNext: _ => {
                    mapGamepad();
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// Rigidbody コンポーネントを取得します
            /// </summary>
            Rigidbody rb = transform.GetComponent<Rigidbody>();

            /// <summary>
            /// 速度を取得します
            /// </summary>
            float speed = 0f;
            this.FixedUpdateAsObservable()
                .Subscribe(onNext: _ => {
                    speed = rb.velocity.magnitude;
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 前進します
            /// </summary>
            bool forward = false; // 前進フラグ
            float SPEED_LIMIT = 5.0f; // スピード制限フラグ
            this.UpdateAsObservable()
                .Where(predicate: _ => 
                    _b_button.isPressed && // 上ボタンが押されて かつ
                    speed < SPEED_LIMIT) // スピード制限以下の時
                .Subscribe(onNext: _ => {
                    forward = true; // 前進フラグをONにする
                }).AddTo(gameObjectComponent: this);

            float FORWARD_FORCE = 7.5f; // 前進力
            this.FixedUpdateAsObservable()
                .Where(predicate: _ => 
                    forward) // 前進フラグがONの場合
                .Subscribe(onNext: _ => {
                    const float ADJUST_VALUE = 10.0f; // 調整値
                    rb.AddFor​​ce( // Rigidbody コンポーネントに力を加えます
                        force: transform.forward * FORWARD_FORCE * ADJUST_VALUE, // 力のベクトル
                        mode: ForceMode.Acceleration // 力をかけるモード: アクセラレーション
                    );
                    forward = false; // 前進フラグをOFFにする
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 回転します
            /// </summary>
            float ROTATIONAL_SPEED = 7.5f; // 回転スピード
            this.UpdateAsObservable()
                .Subscribe(onNext: _ => {
                    const float ADJUST_VALUE = 10.0f; // 調整値
                    int axis = _right_button.isPressed ? // 右ボタンが押されているか？
                        1 : // Yes: axis は 1
                        _left_button.isPressed ? // No: 左ボタンが押されているか？
                            -1 : // Yes: axis は -1
                            0; // No: axis は 0 (※左右キーは押されていない)
                    transform.Rotate( // オブジェクトを回転します
                        xAngle: 0, 
                        yAngle: axis * (ROTATIONAL_SPEED * Time.deltaTime) * ADJUST_VALUE, // Y軸(ヨー)で回転する
                        zAngle: 0
                    );
                }).AddTo(gameObjectComponent: this);
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // private Methods [verb]

        /// <summary>
        /// キー入力を取得します
        /// </summary>
        void mapGamepad() {
            // PCのキーボード入力を取得
            if (Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor) {
                _up_button = Keyboard.current.upArrowKey; // キーボードの上カーソルキー
                _down_button = Keyboard.current.downArrowKey; // キーボードの下カーソルキー
                _left_button = Keyboard.current.leftArrowKey; // キーボードの左カーソルキー
                _right_button = Keyboard.current.rightArrowKey; // キーボードの右カーソルキー
                _b_button = Keyboard.current.zKey; // キーボードのZキー
                _a_button = Keyboard.current.xKey; // キーボードのXキー
                return;
            }
        }
    }
}
```

オブジェクトにスクリプトを追加します。

[Player] スクリプトをつかみ、[シーン] タブの [Player] オブジェクトにドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e36ac588-a68d-e999-cb49-4d3ae3f09039.png)

[Player] の [インスペクター] タブ内に [Player] スクリプトが追加されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/1d48c97e-1deb-cd66-cc2d-7890b7f5ce4a.png)

#### ゲームを起動する

[ゲーム] タブの [▶] ボタンを押すと、このゲームを遊ぶことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/8e19e10f-4afb-8516-54ef-fac6c5948716.png)

:::note info
ここまでの作業で、自機である Player オブジェクトをPCのキーボード入力から操作することができました。
:::

### 障害物を作る

自機を操作している実感を得るために、地面に障害物を作ってみます。

#### マテリアルを作成

[object_black] マテリアルを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/8ff96e9e-ae74-e6c6-cf06-12baa79ad581.png)
色を作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/b4c0f8a3-59d5-607d-d01c-dc2fc8fe247f.png)
このようなマテリアルを作成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e1d39bcd-1510-be71-a16c-00b572013a5c.png)

#### オブジェクトを作成

[3D オブジェクト] > [キューブ] から、[Object] オブジェクトを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/53a49ca8-9057-42e3-6420-e598f028a5db.png)

右の [インスペクター] タブ内の [Transform] の位置で X:2、Y: 1、Z: 2 と入力します。また、スケールで X:2、Y: 2、Z: 2 と入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/a0ca72b5-c91d-59c3-6a80-2e8e363a9b51.png)

[object_black] マテリアルをつかみ、[シーン] タブの [Object] オブジェクトにドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/9c727350-3ab3-b5b8-8f14-62faa2bc69bf.png)

[Object] オブジェクトにマテリアルが適用されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/bc51fef2-69c8-ecf0-748d-4d5f7136829f.png)

#### プレハブに適用

:::note warn
Unity の Prefab (プレハブ) 機能で、プレハブと呼ばれる**オブジェクトのテンプレート**を作成することができます。
:::

[Assets] > [Prefabs] フォルダを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/7fc4c49f-59cd-8ecf-6da4-64d8d3c97f44.png)

[ヒエラルキー] タブの [Obuject] オブジェクトを、[Prefabs] フォルダにドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/dd46139e-6ed7-54cd-1a59-4fe3b7778b29.png)

右の [インスペクター] タブ内の [Transform] の位置で X:0、Y: 0、Z: 0 と入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e3601806-f474-5746-9be0-132b8440af44.png)

:::note info
ここまでの作業で障害物オブジェクトのプレハブを作成することができました。
:::

#### オブジェクトを配置

[Prefabs] フォルダの [Obuject] オブジェクトを [シーン] タブの地面オブジェクトにドラッグ＆ドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/c07d42a3-a058-78e5-cfe7-720a37ef8eef.png)

[シーン] にプレハブから作成したオブジェクトを配置することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/62effad7-3838-9ce1-5ded-d86ff901cb76.png)

[Obuject_n] オブジェクトをリネームしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/303150d4-1bcd-fe6d-81ef-ef9e0e374fb9.png)

:::note info
ここまでの作業で、地面に複数の障害物オブジェクトを配置することができました。
:::

### 弾を発射する

#### マテリアルを作成

[bullet_yellow] という名前のマテリアルを作成します。 (HSL: 60, 80, 100)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/44f21439-b548-930b-4da7-faa28f476a6d.png)

#### オブジェクトを作成

[ヒエラルキー] タブ内で右クリメニューから、[3D オブジェクト] > [スフィア] を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/270962aa-462b-14d2-96df-e70f1bbff476.png)

ここでは、[Bullet] という名前のオブジェクトを作成しました。(スケール X: 0.75、Y: 0.75、Z: 0.75)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/18fe5f94-b119-1987-9d26-08a5c6694fbe.png)

[bullet_yellow] マテリアルを [Bullet] オブジェクトにドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/06f76af9-4fed-6bc0-9140-5aef40d0e7d2.png)

検索から [Rigidbody] を選択して、[Rigidbody] ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/207b1b31-541d-e85b-e548-a81c25d94836.png)

[Bullet] オブジェクトに [Rigidbody] が追加されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/36e88bf4-31bb-3db5-aff4-cb6a432710b6.png)

#### プレハブに適用

[ヒエラルキー] タブの [Bullet] オブジェクトを、[Prefabs] フォルダにドロップして、プレハブを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/65531252-4f4c-4195-7bd6-26f39065dac8.png)

また、[ヒエラルキー] タブの [Bullet] オブジェクトを削除します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/efbc6fc9-9de4-c25f-8c03-ea57ceb24127.png)

#### スクリプトの実装

以下の内容を [Player] スクリプトに追加しました。

```csharp:Assets\Scripts\Player.cs
// ※省略

namespace GameDev {
    /// <summary>
    /// player script.
    /// </summary>
    public class Player : MonoBehaviour {

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // References [bool => is+adjective, has+past participle, can+verb prototype, triad verb]

        [SerializeField] GameObject _bullet; // 弾のプレハブを設定します

        // ※省略

        // Start is called before the first frame update
        void Start() {
            // ※省略

            /// <summary>
            /// 弾を撃ちます
            /// </summary>
            this.UpdateAsObservable()
                .Where(predicate: _ =>  
                    _a_button.wasPressedThisFrame)
                .Subscribe(onNext: _ => {
                    shoot(); // 弾を撃つ
                }).AddTo(gameObjectComponent: this);
        }
        
        // ※省略

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // private Methods [verb]

        // ※省略

        /// <summary>
        /// 弾を撃ちます
        /// </summary>
        void shoot() {
            float BULLET_SPEED = 7500.0f; // 弾の速度

            // 弾の複製
            GameObject bullet = Instantiate(original: _bullet);

            // 弾の位置
            Vector3 position = transform.position + (transform.forward * 1.0f); // ノズル前方
            bullet.transform.position = position;

            // 弾へ加える力
            Vector3 force = (transform.forward + Vector3.up * 0.01f) * BULLET_SPEED; // ごくわずかに上向き

            // 弾を発射
            bullet.GetComponent<Rigidbody>().AddForce(force: force, mode: ForceMode.Force);
        }
    }
}
```

[ヒエラルキー] タブ内で [Player] オブジェクトを選択して、[Player] スクリプトの Bullet 参照に [Bullet] プレハブをドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e8a42a50-f84d-bfe9-b2ae-887cec8638ae.png)

:::note info
ここまでの作業で、Player オブジェクトから弾を発射することができました。
:::

### 弾で障害物を破壊する

#### スクリプトの実装

破壊されるオブジェクトを操作するスクリプトを作成します。

ここでは、[Assets] > [Scripts] > [Destroyable] スクリプトを作成をしました。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/0cb090e3-1deb-50ad-ea4f-f1f85e3e54d6.png)

```csharp:Assets\Scripts\Destroyable.cs
using System.Linq;
using UnityEngine;
using UniRx;
using UniRx.Triggers;

namespace GameDev {
    /// <summary>
    /// destroyable script.
    /// </summary>
    public class Destroyable : MonoBehaviour {

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // References [bool => is+adjective, has+past participle, can+verb prototype, triad verb]

        [SerializeField] int ENDURANCE = 3; // 耐久値

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // Fields [noun, adjectives]

        bool _auto_destroy = false; // 自動消去フラグ

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // Properties [noun, adjectives]

        public bool AutoDestroy { set => _auto_destroy = value; }

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // update Methods

        // Start is called before the first frame update.
        void Start() {
            /// <summary>
            /// 弾としてヒットしたとき
            /// </summary>
            this.OnCollisionEnterAsObservable()
                .Where(predicate: x =>
                    gameObject.name.Contains(value: "Bullet") && // 自分の名前に "Bullet" が含まれる かつ
                    !x.gameObject.name.Contains(value: "Ground")) // 名前に "Ground" が含まれないオブジェクトに衝突したとき
                .Subscribe(onNext: x => {
                    Rigidbody rb = x.gameObject.GetComponent<Rigidbody>();
                    if (rb != null) {
                        rb.velocity = Vector3.zero; // ヒットした相手を一時停止
                    }
                    Destroy(obj: gameObject); // 自分を削除
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 弾がヒットしたとき
            /// </summary>
            this.OnCollisionEnterAsObservable()
                .Where(predicate: x => 
                    x.gameObject.name.Contains(value: "Bullet")) // 名前に "Bullet" が含まれるオブジェクトが衝突したとき
                .Subscribe(onNext: x => {
                    ENDURANCE--; // 耐久度を減らす
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 耐久度がゼロになったとき
            /// </summary>
            this.UpdateAsObservable()
                .Where(predicate: _ => 
                    ENDURANCE == 0 && // 耐久度がゼロ かつ
                    !name.Contains(value: "_Piece")) // 名前に "_Piece" が含まれない場合には
                .Subscribe(onNext: _ => {
                    explodePiece(); // 破片を生成する
                    ENDURANCE = -1; // 耐久度を無効化
                    Destroy(obj: gameObject); // 自分を削除
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 自動消去フラグがONになったとき
            /// </summary>
            this.UpdateAsObservable()
                .Where(predicate: _ => 
                    _auto_destroy == true && // 自動消去フラグがON かつ
                    name.Contains(value: "_Piece")) // 名前に "_Piece" が含まれる破片である場合には
                .Subscribe(onNext: _ => {
                    // 破片を消去する
                    Observable
                        .TimerFrame(dueTimeFrameCount: Mathf.FloorToInt(f: Random.Range(minInclusive: 2500, maxInclusive: 5000)))
                        .Subscribe(onNext: _ => {
                            Destroy(obj: gameObject); // 自分(破片)を削除
                        }).AddTo(gameObjectComponent: this);
                }).AddTo(gameObjectComponent: this);
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // private Methods [verb]

        /// <summary>
        /// 破片を生成します
        /// </summary>
        void explodePiece(int number = 12, float scale = 0.5f, int force = 5) {
            // 複数の破片を作成
            for (int i = 0; i < number; i++) {
                // 破片オブジェクトを作成
                GameObject piece = Instantiate(original: gameObject); // 自分を複製して破片とする
                piece.name += $"_Piece_{i + 1}"; // 破片には名前に "_Piece_n" を付加する
                piece.transform.localScale = new Vector3(x: scale, y: scale, z: scale); // 縮小する
                if (piece.GetComponent<Rigidbody>() == null) {
                    piece.AddComponent<Rigidbody>(); // Rigidbody コンポーネントを持たなければ追加する
                }
                piece.GetComponent<Rigidbody>().isKinematic = false;
                piece.GetComponent<Rigidbody>().mass = 1.0f;
                // ランダム値のベクトルを取得
                Vector3 random_force = new Vector3(
                    x: Random.Range(minInclusive: force / 2, maxInclusive: force * 2), 
                    y: Random.Range(minInclusive: force / 2, maxInclusive: force * 2), 
                    z: Random.Range(minInclusive: force / 2, maxInclusive: force * 2)
                );
                // 破片に力と回転を加える
                piece.GetComponent<Rigidbody>().AddForce(force: random_force, mode: ForceMode.Impulse);
                piece.GetComponent<Rigidbody>().AddTorque(torque: random_force, mode: ForceMode.Impulse);
                // 破片の子オブジェクトを削除
                piece.GetComponentsInChildren<Transform>().ToList().ForEach(x => {
                    if (piece.name != x.name) { // 破片も破片の子リストにいるので除外
                        x.parent = null;
                        Destroy(obj: x.gameObject); // 破片の子オブジェクトは最初に削除
                    }
                });
                // 破片の自動消去フラグをON
                piece.GetComponent<Destroyable>().AutoDestroy = true;
            }
        }
    }
}
```

:::note info
ここまでの作業で、Player オブジェクトから発射した弾で、障害物オブジェクトを破壊することができました。
:::

### 敵を作る

#### マテリアルを作成

[Assets] > [Materials] > [enemy_blue] マテリアルを作成します。(HSV: 210, 90, 90)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/48be09e1-863c-4726-86ad-e63ea9c586ef.png)

#### オブジェクトを作成

[ヒエラルキー] 右クリメニュー > [3D オブジェクトを] > [キューブ] を選択して [Enemy] オブジェクトを作成します。また、[enemy_blue] マテリアルを [Enemy] オブジェクトにドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/9007e681-4d43-3f7d-fd5b-b5a102f29345.png)

#### プレハブに適用

[ヒエラルキー] タブ内の [Enemy] オブジェクトを、[Prefabs] フォルダにドロップして、プレハブを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/9c08210a-10be-09c0-e995-8f85ae79ce3c.png)

Rigidbody コンポーネントを追加します。回転を固定を全てチェックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/a352871c-aee2-7e78-cfbc-b2b643e28682.png)

敵が自機を索敵するために使用する、スフィアコライダー コンポーネントを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/79f01919-591d-e727-9a31-e628a8da990f.png)

[トリガーにする] にチェックを入れます。半径を 15 に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/41b491f2-79c7-ddb0-bf37-561455aca363.png)

#### スクリプトを実装

敵オブジェクトを操作するスクリプトを作成します。

ここでは、[Assets] > [Scripts] > [Enemy] スクリプトを作成をしました。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/a64b7928-01e7-5f18-c1cd-61714ff2c967.png)

```csharp:Assets\Scripts\Enemy.cs
using UnityEngine;
using static UnityEngine.GameObject;
using UniRx;
using UniRx.Triggers;

namespace GameDev {
    /// <summary>
    /// enemy script.
    /// </summary>
    public class Enemy : MonoBehaviour {

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // References [bool => is+adjective, has+past participle, can+verb prototype, triad verb]

        [SerializeField] GameObject _bullet; // 弾のプレハブを設定します

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // Fields [noun, adjectives] 

        float SEARCH_ANGLE = 60.0f; // 索敵アングル(※実際の範囲は2倍になります)

        GameObject _player_object;

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // Properties [noun, adjectives] 

        public bool NotPiece { get => !name.Contains(value: "_Piece"); } // 破片ではないフラグ

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // update Methods

        // Awake is called when the script instance is being loaded.
        void Awake() {
            _player_object = Find(name: "Player"); // Player オブジェクトの参照を取得
        }

        // Start is called before the first frame update.
        void Start() {
            /// <summary>
            /// Rigidbody コンポーネントを取得します
            /// </summary>
            Rigidbody rb = transform.GetComponent<Rigidbody>();

            /// <summary>
            /// 速度を取得します
            /// </summary>
            float speed = 0f;
            this.FixedUpdateAsObservable()
                .Subscribe(onNext: _ => {
                    speed = rb.velocity.magnitude;
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 索敵中に回転します
            /// </summary>
            bool searching = true; // 索敵中フラグ
            bool rotate = false; // 回転中フラグ
            bool stop = false; // 停止フラグ
            this.UpdateAsObservable()
                .Where(predicate: _ => 
                    searching && !rotate && // 索敵中フラグONかつ回転中ではない場合
                    NotPiece)
                .Subscribe(onNext: _ => {
                    // ランダムで移動位置指定
                    int random1 = Mathf.FloorToInt(f: Random.Range(minInclusive: 0.0f, maxInclusive: 8.0f));
                    int random2 = Mathf.FloorToInt(f: Random.Range(minInclusive: 0.1f, maxInclusive: 4.0f));
                    int random3 = Mathf.FloorToInt(f: Random.Range(minInclusive: -3.0f, maxInclusive: 3.0f));
                    int random4 = Mathf.FloorToInt(f: Random.Range(minInclusive: -3.0f, maxInclusive: 3.0f));
                    rotate = true; // 回転中フラグON
                    // random1 秒後に
                    Observable.Timer(System.TimeSpan.FromSeconds(value: random1))
                        .Subscribe(onNext: _ => {
                            stop = true; // 一時停止して
                            // さらに random2 秒後に
                            Observable.Timer(System.TimeSpan.FromSeconds(value: random2))
                                .Subscribe(onNext: _ => {
                                    int random5 = Mathf.FloorToInt(f: Random.Range(minInclusive: 1.0f, maxInclusive: 6.0f));
                                    if (random5 % 2 == 1) { // 偶数なら
                                        // ランダムな方向を向く
                                        transform.LookAt(worldPosition: new Vector3(
                                            x: transform.position.x + random3,
                                            y: transform.position.y,
                                            z: transform.position.z + random4
                                        ));
                                        stop = false; rotate = false;
                                    } else { // 奇数ならそのままの方向を維持
                                        stop = false; rotate = false;
                                    }
                            }).AddTo(gameObjectComponent: this);
                        }).AddTo(gameObjectComponent: this);
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 一時停止します
            /// </summary>
            this.FixedUpdateAsObservable()
                .Where(predicate: _ => 
                    stop && // 停止フラグONの場合
                    NotPiece)
                .Subscribe(onNext: _ => {
                    rb.velocity = Vector3.zero;
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 索敵中に移動します
            /// </summary>
            float SPEED_LIMIT = 3.0f; // スピード制限フラグ
            float FORWARD_FORCE = 16.0f; // 前進力
            this.FixedUpdateAsObservable()
                .Where(predicate: _ => 
                    searching && !stop && // 索敵中フラグONかつ停止中でない場合
                    NotPiece)
                .Subscribe(onNext: _ => {
                    if (speed < SPEED_LIMIT) { // 速度リミットまで
                        rb.AddFor​​ce(force: transform.forward * FORWARD_FORCE, mode: ForceMode.Acceleration); // 前に力を加える
                    }
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 追跡中に回転します
            /// </summary>
            bool chasing = false; // 追跡中フラグ
            this.UpdateAsObservable()
                .Where(predicate: _ => 
                    chasing && // 追跡中フラグONの場合
                    NotPiece)
                .Subscribe(onNext: _ => {
                    // Player オブジェクトの方向に回転する
                    transform.LookAt(worldPosition: new Vector3(
                        x: _player_object.transform.position.x,
                        y: transform.position.y,
                        z: _player_object.transform.position.z
                    ));
                    shoot(); // 弾を撃つ
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 追跡中に異動します
            /// </summary>
            this.FixedUpdateAsObservable()
                .Where(predicate: _ => 
                    chasing && // 追跡中フラグONの場合
                    NotPiece)
                .Subscribe(onNext: _ => {
                    if (speed < SPEED_LIMIT) { // 速度リミットまで
                        rb.AddFor​​ce(force: transform.forward * FORWARD_FORCE, mode: ForceMode.Acceleration); // 前に力を加える
                    }
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 索敵用コライダーに接触したとき
            /// </summary>
            this.OnTriggerEnterAsObservable()
                .Where(predicate: x => 
                    x.name.Equals("Player"))
                .Subscribe(onNext: _ => {
                    chasing = discovered(target: _player_object); // 追跡フラグ判定
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 索敵用コライダーとの接触を維持しているとき
            /// </summary>
            this.OnTriggerStayAsObservable()
                .Where(predicate: x => 
                    x.name.Equals("Player"))
                .Subscribe(onNext: _ => {
                    chasing = discovered(target: _player_object); // 追跡フラグ判定
                }).AddTo(gameObjectComponent: this);

            /// <summary>
            /// 索敵用コライダーから離脱したとき
            /// </summary>
            this.OnTriggerExitAsObservable()
                .Where(predicate: x => 
                    x.name.Equals("Player"))
                .Subscribe(onNext: _ => {
                    chasing = false; // 追跡フラグOFF
                }).AddTo(gameObjectComponent: this);
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////
        // private Methods [verb]

        /// <summary>
        /// 追跡フラグを判定します (※視界の角度内に収まっているか)
        /// </summary>
        bool discovered(GameObject target) {
            // 対象までのベクトルを取得
            Vector3 position_delta = target.transform.position - transform.position;
            // 前方ベクトルと対象までの角度を取得
            float target_angle = Vector3.Angle(from: transform.forward, to: position_delta);
            // 視界内に収まっている場合
            if (target_angle < SEARCH_ANGLE) { 
                return true; // 追跡フラグON
            }
            return false; // 追跡フラグOFF
        }

        /// <summary>
        /// 弾を撃ちます
        /// </summary>
        void shoot() {
            float BULLET_SPEED = 7500.0f; // 弾の速度

            // ランダム値を生成して3の時だけ弾を発射
            int random = Mathf.FloorToInt(f: Random.Range(minInclusive: 0.0f, maxInclusive: 1024.0f));
            if (random == 3.0f) {
                // 弾の複製
                GameObject bullet = Instantiate(original: _bullet);

                // 弾の位置
                Vector3 position = transform.position + (transform.forward * 1.0f); // ノズル前方
                bullet.transform.position = position;

                // 弾へ加える力
                Vector3 force = (transform.forward + Vector3.up * 0.01f) * BULLET_SPEED; // ごくわずかに上向き

                // 弾を発射
                bullet.GetComponent<Rigidbody>().AddForce(force: force, mode: ForceMode.Force);
            }
        }
    }
}
```

[Enemy] プレハブにスクリプトを設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/bc2cf411-0d09-d547-b927-746966bd8ba4.png)

### 敵を配置する

[Enemy] プレハブを [シーン] に複数追加しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/4ae6ce10-7bdb-4fdf-9bdb-06f4cb8ae7c8.png)

:::note info
ここまでの作業で、敵オブジェクトをシーンに配置することができました。
:::

### 自機の耐久度を設定

[Player] オブジェクトに [Destroyable] スクリプトを追加して、ENDURANCE (耐久度) を5に設定しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/544bbe47-250f-9b18-e7ff-980c7f46cb3b.png)

:::note info
ここまでの作業で、障害物や敵オブジェクトを撃破するゲームを作成することができました。
:::

## このゲームをプレイした動画

![Movie_002.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/5bb018a5-b2f8-147c-f344-41b5ea843eb4.gif)

:::note info
まだとても初歩的な段階ではありますが、敵を撃破する3Dゲームの実装に成功しました。
:::

## まとめ

Unity エディターで C# 言語を用いて原始的な3Dゲームを作成することができました。

:::note warn
今回の実装にはいくつかの問題が見受けられます。将来的な改善点については、別の機会に実装記事で紹介していく予定です。
:::

:::note alert
この記事の実装例は一つのアプローチに過ぎず、必ずしも正しい方法とは限りません。他にも多様な方法がありますので、さまざまな情報を照らし合わせて検討してみてください。
:::

どうでしたか？ Window 11 の Unity で3Dゲームを開発する環境を手軽に構築することができます、ぜひお試しください。今後も Unity の開発トピックなどを紹介していきますので、ぜひお楽しみにしてください。