3DCG覚書とかとか

参考書のすみっこ

~普段メモとかしても忘れちゃうのでブログにしました~

目指せUnityマスター【STG企画編】

Q.何この記事?
A.新卒Webエンジニアが、Unityマスターになるまでの過程を描いていく記事。
1カ月突破(゚∀゚)

今回の目玉は、ゲーム企画・物体の回転(オイラー角/クォータニオンかな。

今週やったこと

前までやってたCreate with Codeが終わったので、「ゲームの企画」と「実装」を進めてた。

ゲーム企画

前にゲームを作った時は参考書ゲームちょっと改変しただけだったので、今度は自分でゲームを企画してみたいと思う。

テーマ

「冒険」「発見」「成長」
自分の好きなゲームを考えた時、大まかにこの流れがあるゲームが好きだと思ったので、この先ずっとテーマはこれで行くと思う。

冒険とは

冒険と聞くと探索要素かな?と思うかもしれないし、実際自分がゲームやっててもつい隅々まで探索してしまったりする。
けどここでは「物語がある」ことかなと思う。プレイしてるときにある程度プレイヤーがキャラに入り込めると楽しいなと感じたので。

発見とは

これは冒険要素とある程度結びついていて、オープンワールドなら景色が綺麗な場所を見つけたりとか、メトロイドとかのアクションゲームなら強化パーツ見つけたりとか、そういう時テンション上がるよな~っていう感じ。

それと「調合」とか「合成」とかで、新しいアイテムが作成できるのも好き。
それに似て、素材を集めて新しい装備が作れるとかも好き。
だから、「新しいモノの発見」てことかな。

あと「こんなところまでギミックあるんか」みたいな隠し要素もこれにあたるかな。

成長とは

これは段々強くなっていくこと全般かな。

RPG:レベルアップや装備でステータス上昇
アクション:新しい武器見つけたり、ステータス上昇パーツ見つける
STG:乙らないでアイテム取得し続けると、弾幕が強化される

こんな感じだけど、例に挙げた3ジャンル以外のゲームでもこんな感じの事があれば好き。

ゲーム性

まず作ろうと思ってるのは、↑のpdfにまとめたゲーム企画をSTGにしたもの。
最初からRPG作ろうと思うとちょっと重たく感じたので、まずSTGにして手軽に遊べる感じにしてunityroomとかに上げたいと思ってる。

今回企画したゲームは、最初に
「ステージがゴロゴロ転がる感じ」
を思いついて、その後こじつけに
「筒状でステージの方から向かってくれば、立体感あって楽しいんじゃね」
とか
「3Dのアクション系で、自分のキャラ操作して探索するのって、軽く遊びたい時はちょっと面倒だと思っちゃうよな」
とか考えてた。

ぶっちゃけ縦型STGと特に大差はないと思うけど、その分立体感を生かしてキングダムハーツ的な派手なエフェクトにこだわりたいと思ってる。

設計

資料を載せるのは面倒くさいので決めたことを一覧にしておく。

基本設計

・大まかなゲームの流れ
・ゲーム要素の洗い出し

詳細設計

・ゲーム要素のタスク
・ゲームロジック
・ビジュアルデザイン
・音響デザイン
・オブジェクトの種類(ステージ、プレイヤー、敵...等)
・各オブジェクトの属性と操作

今日までに実装した機能

プレイヤーの移動に関する機能を実装した。

プレイヤー移動

ステージが円筒型なので、その中心に重力があるような感じにした。

実装手順としては、
1.重力をステージの中心にする
2.横方向の移動キーを押すと、横向きにtranslateする
3.縦方向の移動キーを押すと、ステージを中心にrotationする
4.マウスカーソルを横に動かすと、キャラクターの向きが左右する
(5.プレイヤーにアニメーションをつける)

という感じで、中でも4番目にてこずって2日格闘してた(ベクトルの回転ってムズイ)。

▼見た目

f:id:kuraudo0309:20200801225229g:plain
プレイヤー移動

1.重力をステージ中心の向きにする

これは簡単で、自分の位置から重力に設定するオブジェクトの位置の方向ベクトルを求めて、それに重力加速度をかけてあげればいい。

/* 重力をステージ中心の向きにする */

//重力の基となるゲームオブジェクト
    GameObject gravityObject;
    float gravityMagnitude = 9.8f;

    void Start()
    {
        gravityObject = GameObject.Find("Ground");
        StartCoroutine(UpdateGravity());
    }

    Vector3 CalculateGravityDirection()
    {
        Vector3 gravityPosition = new Vector3(transform.position.x, gravityObject.transform.position.y, gravityObject.transform.position.z);
        Vector3 gravityDirection = (gravityPosition - transform.position).normalized;
        return gravityDirection;
    }

    IEnumerator UpdateGravity()
    {
        while (true)
        {
            Physics.gravity = CalculateGravityDirection() * gravityMagnitude;
            //Debug.Log(Physics.gravity);
            yield return new WaitForSeconds(0.1f);
        }
    }

2.横方向の移動キーを押すと、横向きにtranslateする

タイトル通り。

/* 横方向移動 */
    [SerializeField] float speed = 5.0f;

    private void FixedUpdate()
    {
        MoveKeyInput();
    }

    //移動キーが押されたら、それに対応して動く
    void MoveKeyInput()
    {
        if (IsInputHorizontalMoveKey())
        {
            float horizontalInput = Input.GetAxis("Horizontal");
            Vector3 moveDirection = new Vector3(horizontalInput, 0, 0).normalized;

            playerBaseY.gameObject.transform.Translate(moveDirection * this.speed * Time.fixedDeltaTime, Space.World);
        }
    }

    //横移動キーが押されている?
    bool IsInputHorizontalMoveKey()
    {
        return Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.D) ||
            Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.RightArrow);
    }

3.縦方向の移動キーを押すと、ステージを中心にrotationする

プレイヤーの親にemptyを設定しておいて、
・PlayerBaseX
 ・PlayerBaseZ
  ・Player(本体)

こんな感じの階層にする。それぞれの役割は、
PlayerBaseX:ステージを中心に、その周りをまわる(rotation.xを変える)
PlayerBaseZ:プレイヤーを中心に、z軸の初期角度を変える
Player:プレイヤーの左右の向きを変える

それから縦方向の移動はPlayerBaseXを使って、

/* 縦方向移動 */

    [SerializeField] float speed = 5.0f;
    float rotateSpeed;
    float radius;

    void Start()
    {
        playerBaseX = GameObject.Find("PlayerBaseX");
        playerBaseZ = playerBaseX.transform.Find("PlayerBaseZ").gameObject;
        radius = GameObject.Find("Ground").transform.localScale.x / 2;
        //2πr : 360° = speed : rotateSpeed
        rotateSpeed = (180 * speed) / (Mathf.PI * radius);
    }

    //移動キーが押されたら、それに対応して動く
    void MoveKeyInput()
    {
        if (IsInputVerticalMoveKey())
        {
            float verticalInput = Input.GetAxis("Vertical");
            Vector3 rotateDirection = new Vector3(verticalInput, 0, 0).normalized;

            playerBaseX.transform.Rotate(rotateDirection * this.rotateSpeed * Time.fixedDeltaTime);
        }
    }

    //縦移動キーが押されている?
    bool IsInputVerticalMoveKey()
    {
        return Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.S) ||
            Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.DownArrow);
    }

こんな感じに表してみた。

移動量の計算は、横移動のspeedに合わせるためにステージの半径をrとして、
2πr : 360° = speed : rotateSpeed
として計算させた。こうすればspeedの移動量とrotateSpeedの角移動量(度数法)が等しくなって、speedあたりに回転させる角度rotateSpeedが求まるはず。

4.マウスカーソルを横に動かすと、キャラクターの向きが左右する

画面の中心からカーソルがどれくらい離れてるかによって、プレイヤーに左右を向かせるようにした。 (最初はマウスカーソルから無限遠にRayを飛ばして、その交点に向かせようと思ったけど、方法を変えた。)

まず画面中央からマウスまでのベクトルを取得するために、GetCenterToMouse()を実装

/* 画面中央からマウスまでのベクトル取得 */

    //画面中央の座標
    Vector2 centerPosition;

    void Start()
    {
        centerPosition = GetCenterPosition();
    }

    //画面中央からマウスまでのベクトルを取得
    Vector2 GetCenterToMouse()
    {
        Vector2 mousePosition = Input.mousePosition;
        Vector2 centerToMouse = mousePosition - this.centerPosition;
        return centerToMouse;
    }

    //画面中央の座標を取得
    Vector2 GetCenterPosition()
    {
        float centerX = Screen.width / 2;
        float centerY = Screen.height / 2;
        return new Vector2(centerX, centerY);
    }

次にプレイヤーの左右の回転角を計算するために、GetLocalRotationY()を実装

/* プレイヤーの左右の回転角を計算 */

    //プレイヤーの左右の回転角を取得
    float GetLocalRotationY()
    {
        Vector2 centerToMouse = GetCenterToMouse();

        //真ん中を(0,0)として、画面端の最大値
        float maxMousePositionX = GetCenterPosition().x;
        float maxLocalLotationY = 60;
        float moveAmount = maxLocalLotationY / maxMousePositionX;

        //最終的なプレイヤーローカルRotaionのY回転量
        float localRotationY = moveAmount * centerToMouse.x;

        return localRotationY;
    }

ここでは画面中心を(0,0)と見た時に、左右の画面端までカーソルを動かしたときにプレイヤー.yが60°回転することにした。
moveAmount = maxLocalLotationY/ 60° / / maxMousePositionX/ 中心から画面恥の距離 /
これでx方向の1座標動かしたあたりのmoveAmount(回転量)を計算してる。

最後に、これらの動きを反映させるために
プレイヤーローカル座標のy軸
を回転させる。

注意しなければならないのが、
VectorやQuaternionをtransform.rotationに代入する方法は、(不可能ではないが)やめたほうが無難
だということだ。
説明前にまずこの階層構造を思い出してほしい。
・PlayerBaseX
 ・PlayerBaseZ
  ・Player(本体)

最初にplayer.transform.upを軸に回転させたものをplayer.transform.rotationに代入しようとしたのだが、これではどうあがいても前フレームのtransform.upを軸に反映されてしまうため、この値が変わらない。
player.transform.rotation = Quaternion.AngleAxis(moveAmount, transform.up);

次に親であるPlayerBaseZ.transform.upを軸に回転させたものを反映しようとしたのだが、これはあくまでtransform.upを軸に回転しているのであって、playerのx軸、つまり地面に水平に向くようには回転しない。
要するに、「プレイヤーが直立している状態で、回転軸だけが変わっていく」ということだ。
player.transform.rotation = Quaternion.AngleAxis(moveAmount, PlayerBaseX.transform.up);

なので確実にローカル座標のy軸を回転させるために、transform.Rotate()を使うことにした。
処理の流れとしては、
1.前フレームの回転量で逆向きに回転させる
2.現フレームの回転量を取得
3.現フレームの回転量で回転させる
4.現フレームの回転量を保存

こんな感じになっている。初めに前フレームの回転量で戻してるのは、Rotate()はローカルの角度に対してVector3を足すような処理になっているからだ。
なので初めに回転量が0の状態を作り出すために、前フレームの回転量を保存しておいて逆向きに回転させている。
処理が画面に反映されるのは1フレームごとの処理を終えた後なので、ガタガタする心配もない。

スクリプトにするとこんな感じになった。

/* プレイヤーの向きをアップデートする */

    //プレイヤーの向きをアップデートする
    void UpdatePlayerRotation()
    {
        transform.Rotate(-prevRotateY);

        float localRotationY = GetLocalRotationY();
        
        transform.Rotate(new Vector3(0,localRotationY,0));

        prevRotateY = new Vector3(0, localRotationY, 0);
    }

最終的な処理はこんなに単純なのに、ここでめちゃくちゃ詰まってしまった...。
まぁでもQuaternionの学習になったのでよしとする。ゲームオブジェクトの親子関係を利用するときは、Translate()やRotate()で処理を実現させるようにしよう。

感想

ゲーム開発って最初の企画でも発想力は必要だけど、実装するときも思い付きが大事だし、ずっと躓いててトイレとか風呂とかにいるときに急に思いつくから、これがまた楽しいんだよね。
もちろん躓いてるときはもやもやして息苦しくなるし、ずっと頭抱えてるけど、だんだん出来上がってる感じする。

まだゲームのビジュアルとかストーリーが決まってないから、最終的な完成めどは立ってないけど、まぁ地道に行こうと思う。
あとunityroomの「Unity1週間ゲームジャム」に参加してみようと思ってるから、そこではクソゲー作って玉砕してこようと思う。運が良いことに会社は休みなので時間はたっぷりある。

皆さん今週もお疲れ様です( ´_ゝ`)