3Dスキャンしたモデルで簡単なゲーム制作!

はじめに

こんにちは、オプティムアルバイトスタッフの岡村です。 先日、OPTiM × C3の合同イベントとしてUnityのハンズオンを行ったのでそのハンズオン用の記事を書きました。

C3は九州工業大学のコンピューターを使ってモノづくりをするサークルです。 OPTiMのオフィスが九州工業大学前にあるということもあり、OPTiMにはアルバイトとして勤務しているC3メンバーがいます。 そこで九州工業大学前アルバイトスタッフとC3との共同でハンズオンイベントを行いました。

今回行ったイベントのほかの記事はこちら↓

tech-blog.optim.co.jp

事前準備の記事はこちら

compositecomputer.club

ハンズオン準備

  • 前提条件として、Unity 2021.3.25f1がインストールされているものとします。
  • こちらにアップロードされているUnityPackageをダウンロードしてください。ダウンロードはCodeのDownload ZIPからできます。ダウンロード後、zipファイルを解凍しておいてください。
  • UnityHubのプロジェクトタブから新しいプロジェクトを選択。すべてのテンプレートの中の3D(コア)を選択して任意のプロジェクト名でプロジェクトを作成してください。
  • プロジェクト作成後、画面上部のAssetsからImportPackage->Custom Package ...を選択して先ほどダウンロードしたC3Event.unitypackageをインポートしてください。その時、importの選択画面が出てくると思いますがそのままで大丈夫です。
  • そのあと、シーンビュー下部のProjectウィンドウからScenesフォルダを開き、その中のC3_Eventをダブルクリックします。
  • 準備が完了するとこのような画面になっていると思います。
  • そのあと、画面上部のScene、Gameと書かれたタブの中でGameを選択して、FreeAspectになっている部分をFullHDにしましょう。FullHDにした後はSceneに戻してください。

的あてゲームの作成

今回作成してもらうのは簡単な的あてゲームです。点群データを利用して作成したステージ上で制限時間内に動く的にボールをぶつけるとスコアが上がるといったようなものです。

1. マウスのFPS操作を作る。

  • 今回、カメラの動きはカメラのオブジェクトのtransformの回転を変更することで作成したいと思います。
  • まずは、マウスを操作すると画面が動くシステムを作成します。ProjectウィンドウからScriptsフォルダを開いてください。
  • その中で右クリックを押し、Create->C#Scriptを左クリックして選択してください。

すると、画像のような#の文字が書かれたファイルが作成されるので名前をCameraMoveに変更してください。

  • ファイル作成後、ダブルクリックでVisualStudioが起動されるので、そのスクリプトに次のプログラムをコピー&ペーストしてください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraMove : MonoBehaviour
{
    [Header("カメラ感度")] public float Sensitivity = 2f;

    private float _mouseX;
    private float _mouseY;

    void Start()
    {
        //マウスの移動量の初期化
        _mouseX = 0;
        _mouseY = 0;
    }
    void Update()
    {
        //マウスの移動量の取得
        if(Cursor.visible == false) //もしカーソルが見えなかったら(ゲームが操作可能だったら)
        {
            _mouseX = Input.GetAxis("Mouse X") * Sensitivity; //マウスのX方向の移動量×カメラ感度
            _mouseY = Input.GetAxis("Mouse Y") * Sensitivity; //マウスのy方向の移動量×カメラ感度
        } 
        else
        {
            if (Input.GetKeyDown(KeyCode.Mouse0)) //そうでないとき、左クリックでマウス固定
            {
                Cursor.visible = false;
                Cursor.lockState = CursorLockMode.Locked;
            }
        }
        //Escキーでカーソル表示
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            Cursor.visible = true;
            Cursor.lockState = CursorLockMode.None;
        }
    }
    private void FixedUpdate()
    {
        //カメラの回転
        this.transform.eulerAngles += new Vector3(-_mouseY, _mouseX, 0); //カメラをマウスの移動量×カメラ感度の分だけ回転させる。
    }
}
  • プログラムを作成した後、ドラッグして左上のHierarchy内のMain Cameraにドロップしてください。すると右のInspectorの表示が次のようになります。

  • 画面中央上部の再生マークを押すとゲームが始まり、画面を左クリックするとマウスが固定されてマウスでFPSのようなカメラの回転ができるようになります。また、操作をやめたいときは、Escキーを押すとマウスの固定が解除されます。また操作したいときはもう一度画面を左クリックしてください。
  • このプログラムの構造としては、マウスの移動量を取得した後、それにあらかじめ指定したカメラの感度を掛け合わしたものを使ってカメラのゲームオブジェクトを回転させています。
  • しかし、そのままではゲームを起動した瞬間ずっとマウスの移動に合わせて回転し続けるためバグを生みやすいです。そこで、カメラが動くか動かないかのトリガーとしてマウスが固定されているかどうかで分けています。

2. ボールを発射する。

  • まずは発射するボールを作成しましょう。ヒエラルキーの何もないところで右クリックして、3DObject->Sphereをクリックします。名前はわかりやすくBallとしましょう。作成したものが見当たらないときは右上のInspector上のTransformのPositionを(X : 0 Y : 3 Z : 2)あたりに設定すると目の前に現れます。
  • 見るとわかりますが発射するにはボールが大きすぎるのでTransformのScaleをxyzすべて0.2にしましょう。
  • そのあと、ボールにタグをつけましょう。Inspector上部にTagの欄があり、Untaggedとなっていると思うので、それをBallに変更します。

  • ボールは物理エンジンを使って動かすので、その後Inspectorの下部にあるAdd ComponentからRididbodyを選択して物理エンジンをボールにもたせましょう。

ここで一度再生を押すとボールが重力に従って落下していくのがわかります。

さてボールをどのようにして発射するかといった点についてですが、いくつか方法はあるのですが今回はボールがゲーム内に登場した瞬間発射されるという風にしたいと思います。

  • まず、ボールが発射されるスクリプトを作成しましょう。先ほどと同様にして、Scriptのフォルダ内で右クリック->Create->C# ScriptからShootBallという名前で作成します。

  • その後、スクリプトをダブルクリックしてVisualStudioで以下のように作成します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShootBall : MonoBehaviour
{
    [Header("ボールのスピード")][SerializeField] private float _speed = 20; //ボールの発射速度
    private Transform _camera; //プレイヤーのカメラ
    private Rigidbody _rb; //ボールのRigidbody

    private void Awake()
    {
        _camera = GameObject.Find("Main Camera").transform; //カメラを取得
        _rb = GetComponent<Rigidbody>(); //ボールのRigidbodyを取得
        Vector3 _direction = _camera.forward; //カメラの正面を_directionに代入
        _rb.AddForce(_direction * _speed, ForceMode.Impulse); //_direcitonに向かってボールを発射する。
        Invoke("Destroy", 1.5f); //1.5秒後ゲームオブジェクトを削除
    }
    private void Destroy()
    {
        GameObject.Destroy(this.gameObject);
    }
}

このプログラムのミソは_rb.AddForce(_direction * _speed, ForceMode.Impulse);です。 これは先ほど追加したRigidbodyの中のAddForce()という関数を使用しています。これは、その名の通り「力を加える」という関数で、Unity内部の物理エンジンを使用して物体に瞬発的な力を加えています。これはさまざまなゲームで重要となってきますので覚えておくと良いでしょう。 また、覚えておくと良い関数としてInvoke関数があります。これはn秒後にある関数を実行するというもので、今回はボールが出現してから1.5秒後にDestroy関数を起こすと書かれています。よってボールは発射されてから1.5秒後に自動的に削除されます。

さて、ボールを発射するスクリプトが作成されたので、これをアタッチして動かしてみましょう。 アタッチの方法は作成したスクリプトをドラッグして、作成したBallにドロップしてみてください。

また、折角なのでボールの色を黄色にしようと思います。Assetsフォルダ内のMaterialフォルダを参照してみてください。そこにBallと書かれた黄色い丸のようなものがあるのがわかると思います。これはマテリアルと呼ばれ、3Dモデルにおける色の部分にあたります。これをドラッグして再びBallにドロップしてみましょう。 画像のようになっていれば大丈夫です。

これでゲームを実行した瞬間ボールが発射されるのがわかるかと思います。

3. ボールのプレハブ化

ここではボールをプレハブというものに変換させます。まず、プレハブについて説明しましょう。プレハブとはUnity上で作成したゲームオブジェクトを複製したり、別のシーンで使いまわしたりしたいと考えるとき、簡単に複製できるように保存しておくシステムのことです。例えば、今回発射するボールはマウス左クリックで無限に発射させたいですね。しかし、今のままではボールは1.5秒で消えてしまいますし、発射も一度限りです。これを何個もプレイヤーの手元で作成出来たらたくさんボールを放つことができゲームとして扱いやすくなります。

  • それではまず、ProjectウィンドウからAssets->Prefabsを開いてください。
  • そのあと、Hierarchy上で先ほど作成したBallをドラッグしてProjectウィンドウのPrefabsフォルダ内にドロップしてください。
  • すると、Hierarchy上では青色になって、PrefabsフォルダにBallというものが作成されていると思います。これでプレハブ化は成功です。
  • ただし、このままでは配置したままの位置が保存されているので作成したプレハブをクリックして右に表示されるInspectorの中からPositionをx,y,zすべて0にしましょう。

  • また、すでにHierarchy上にあるボールは不要なので右クリックからDeleteで削除してしまいましょう。

4. 左クリックでボールを発射する。

次はボールを左クリックで発射するようにしましょう。スクリプト自体は簡単で左クリックを感知してボールプレハブをゲーム上に複製するだけです。

  • 再びScriptフォルダに移動して、その中で右クリック->Create->C# scriptよりC#のスクリプトを作成します。その時、名前はPlayerManagerとしてください。
  • その後、スクリプトをダブルクリックしてVisualStudioを起動して、以下のコードを記述してください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerManager : MonoBehaviour
{
    [SerializeField] private GameObject _ballPrefab; //ボールのプレハブ


    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Mouse0) && Cursor.visible == false)
        {
            Instantiate(_ballPrefab, transform.position, Quaternion.identity); //ゲームオブジェクトの作成
        }
    }
}

簡単に解説すると、[SerializeField]を用いてボールのプレハブをインスペクター上から参照して、Update関数ないで常に左クリックが押されているかどうかと、カーソルが非表示になっていて操作可能になっているかどうかを確認しておいて、もし操作可能かつ、マウス左クリックが押されている場合は、Instantiate関数を用いて、Ballのプレハブを、transform.position(右手のボールの位置)で、特に回転させずに(Quaternion.identityで)作成します。すると、ボールは先ほどのShootBall.csスクリプトで勝手に飛んでいくという仕組みです。

Input.GetKeyDown(KeyCode.任意のキーコード)というコードはとても重要でキーボードやマウスなどあらゆる入力を検知できるので様々なゲームに応用が利きます。ぜひ覚えておいてください。

  • スクリプトが作成出来たらそれをドラッグして今度はHierarchy上のHandBallにアタッチしましょう。画面に映っていない人はドラッグする前にMain Cameraのトグルを開いておきましょう。
  • アタッチ出来たら、その中のPlayerManagerの中のBallPrefabが空白となっていると思うので、ProjectウィンドウからPrefabsを開いてその中のBallをドラッグして先ほどの部分にドロップしましょう。

ゲームを開始すると、左クリックでボールが発射されるようになっているかと思います。 目の前の緑色の的にボールをぶつけるとスコアが加算されているはずです。

ただし、このままだとボールがマップを貫通してしまいます。なぜこうなるかというと、3Dモデルに対応した当たり判定はそのままだと不安定だからです。ゲーム内で物理エンジンが処理を行われる前に、物体が貫通してしまい、当たり判定が正常に行われないことが多々あります。これを改善するためにRigidBodyの設定を見直しましょう。

  • Prefabsフォルダを開いて、Ballプレハブを選択してください。
  • RigidBodyのCollision Detectionを選んでDiscreteからContinuous Speculativeに変更してください。

Collision Detectionは衝突の検知方法を変更するオプションでこれを離散的から連続投機的に変更することで、一定のタイミングでしか検知しなかったものを現在の速度に基づいて検知させることで正常に衝突を検知させることができます。

これで基本となる部分は終了です。

5. 的の編集

ここからは余裕のある方向けですがぜひ挑戦してみてください。今回のゲームの的を少し改造してみましょう。

今回使用している的はTargetAとしてHierarchy上に描画されていると思います。これにはTargetとTargetMove1というスクリプトで制御しています。Targetには当たった時のスコアの加算が、TargetMove1には的の移動について記述しています。スコアを変更したいときはTargetAをクリックすると表示されるInspector上で加算されるスコアを変更できるようにしています。

好きな値にしてみてください。 では、的の移動はどうすればよいでしょうか?今回の記述は次のようになっています。

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

public class TargetMove1 : MonoBehaviour
{
    [SerializeField] private float _speed; //移動スピード
    [SerializeField] private float _range;  //移動幅

    private Vector3 _position; //初期位置

    void Start()
    {
        _position = transform.position; //初期位置の取得
    }

    // Update is called once per frame
    void Update()
    {
        transform.position = new Vector3(_range * Mathf.Sin(Time.time * _speed) + _position.x, _position.y, _position.z); //振幅運動
    }
}

解説すると、まず変数として的の速度を_speed、移動幅を_rangeとしています。 ゲームが開始されたとき現在の位置を取得して、そこからx座標の位置を_range * Mathf.Sin(Time.time * _speed)と記述して移動させています。このMathf.Sin()はC#において数学のSin関数を利用しています。経過時間をSinの内部に入れることで、時間の長さに応じたsinの値になります。つまり、GameObjectを簡単に周期的な動きをさせることができるのです。

例えば今回はnew Vector3の中のx軸部分にSin関数を入れていますが、これをy軸、z軸にもすることができますね。 また、次のようにすると、周期的に大きさが変化します。transform.localScaleはGameObjectの大きさを変化させられます。

transform.localScale = (Mathf.Sin(2 * Mathf.PI * Time.time) + 1) * 0.5f * _maxScale;

先ほどのコードに追記すると

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

public class TargetMove1 : MonoBehaviour
{
    [SerializeField] private float _speed; //移動スピード
    [SerializeField] private float _range;  //移動幅

    private Vector3 _position; //初期位置
    private Vector3 _maxScale = new Vector3(3, 3, 0.0967f); //最大の大きさ

    void Start()
    {
        _position = transform.position; //初期位置の取得
    }

    // Update is called once per frame
    void Update()
    {
        transform.position = new Vector3(_range * Mathf.Sin(Time.time * _speed) + _position.x, _position.y, _position.z); //振幅運動
        transform.localScale = (Mathf.Sin(2 * Mathf.PI * Time.time) + 1) * 0.5f * _maxScale;
    }
}

このようにして、いろいろ試してみてほしいです。こんな風に動かしたいと思ったら軽く検索をかけるだけでも出てくると思うのでいろいろ試して、ゲームとして面白くしてみましょう。プレハブを使って的の数を増やしてみたり、的の位置や大きさをInspectorから変えてみたりしてそれに応じて得点をつけたりするのもいいと思います。

終わりに

 最後になりましたが、今回の講座でUnityで的あてゲームの作り方がわかっていただければ幸いです。今回のシステムはFPSゲームなどにも通じる所があると思うので、3Dゲームの基本を学ぶのにはよかったのではないかと思います。また、点群データを用いることでリアルなマップを作成することができます。点群データは、フリーで公開されているものネット上に公開されているので興味のある方はぜひお試しください。


OPTiMでは運用改善やDevOpsに興味があるエンジニア、新卒からどんどんチャレンジしていきたいエンジニア学生を随時募集しております。少しでもご興味のある方はこちらも合わせてご覧ください。

www.optim.co.jp