はるきちの雑記

対戦よろしくおねがいします

【Unity】一人称視点カメラの上下方向の回転を制御したい

現在一人称視点のゲームを作ってるんですが、カメラの操作を実装しているときに少し悩んだところがあったので、忘れないように記事を残しときます。

まずカメラをマウスの移動に合わせて回転させる処理ですが、

        // マウスの移動方向に回転量を掛けた値を代入する
        xRotate = Input.GetAxis("Mouse X") * cameraRotateSpeed;
        yRotate = Input.GetAxis("Mouse Y") * cameraRotateSpeed;

        // カメラをマウスの移動量に応じて回転させる
        camera.transform.RotateAround(player.transform.position, Vector3.up, xRotate);
        camera.transform.RotateAround(player.transform.position, transform.right, -yRotate);

これでマウスの移動に合わせてカメラが回転します。

xRotateはマウスの横方向の移動量を取得、
yRotateはマウスの縦方向の移動量を取得しています。
cameraRotateSpeedはゲーム上で言うところの感度です。

変数を見れば想像つくと思いますが、xRotateはカメラの横方向でyRotateはカメラの縦方向の回転させるために使います。
RotateAround関数は、第1引数に回転に用いる中心、第2引数に回転軸、第3引数に回転する方向を指定します。


ここで、カメラの回転する処理部分で記述されているVector3.upは、ワールド座標のy軸(0, 1, 0)を軸として使っています。
ワールド座標とは、3D空間全体の中心を中心座標として座標を表します。
なので、オブジェクトの向きなどは考慮されずに向きを変えることができるので、オブジェクトがどんなに傾いていようが常に同じ方向へ回転させることができます。


transform.rightは、オブジェクト自身の座標のx軸(1, 0, 0)を軸として使っています。
オブジェクトの座標をとっているので、オブジェクトの向きが変わると、回転する方向も変化します。

これでカメラの回転制御は7割くらいは完成してるのですが、ここである処理を加えます。
それがカメラの上下の回転制御です。

現状、今のだとカメラが上下方向に対して360°回ってしまいます。
普通の人間ならこんな首の動かし方したら終わるので、上下方向に対する回転制御を実装します。

コードはこんな感じ

        // マウスの移動方向に回転量を掛けた値を代入する
        xRotate = Input.GetAxis("Mouse X") * cameraRotateSpeed;
        yRotate = Input.GetAxis("Mouse Y") * cameraRotateSpeed;

        // カメラの上下の回転量(x軸の回転量)を取得
        yRotateParam = transform.localEulerAngles.x;

        // カメラのインスペクターの回転量と同期させる
        // カメラのx軸の回転量が180を超えていた場合
        if (yRotateParam > 180)
        {
            // 0~180、0~-180で回転量を表現できるよう計算を行う
            yRotateParam = yRotateParam - 360;
        }

        // カメラの上方向の回転量が指定の下限より大きい場合
        // カメラの回転方向が0より大きい場合
        if (yRotateParam >= rotateLimitMin && yRotate > 0)
        {
            // カメラをマウスの移動量に応じて回転させる
            camera.transform.RotateAround(player.transform.position, transform.right, -yRotate);
        }

        // カメラの下方向の回転量が指定の上限より小さい場合
        // カメラの回転方向が0より小さい場合
        if (yRotateParam <= rotateLimitMax && yRotate < 0)
        {
            // カメラをマウスの移動量に応じて回転させる
            camera.transform.RotateAround(player.transform.position, transform.right, -yRotate);
        }

        // カメラをマウスの移動量に応じて回転させる
        camera.transform.RotateAround(player.transform.position, Vector3.up, xRotate);


少しだけif文とかを付け足しました。
コメントでほとんど説明してますが、追加したところを上から順に説明しようと思います。

        // カメラの上下の回転量(x軸の回転量)を取得
        yRotateParam = transform.localEulerAngles.x;

これはインスペクターのx軸の回転量を取得しています。
上下方向の回転制御をするために用います。

        // カメラのインスペクターの回転量と同期させる
        // カメラのx軸の回転量が180を超えていた場合
        if (yRotateParam > 180)
        {
            // 0~180、0~-180で回転量を表現できるよう計算を行う
            yRotateParam = yRotateParam - 360;
        }

これはlocalEulerAnglesだと回転量を0から360で表現するのに対し、
インスペクター上では0から180、181以上は-180から0で表現しているので、
180を超えた回転量は-180から0の表記に収めてインスペクター上の表現と同じにするようにしています。

        // カメラの上方向の回転量が指定の下限より大きい場合
        // カメラの回転方向が0より大きい場合
        if (yRotateParam >= rotateLimitMin && yRotate > 0)
        {
            // カメラをマウスの移動量に応じて回転させる
            camera.transform.RotateAround(player.transform.position, transform.right, -yRotate);
        }

カメラの上方向に対する回転制御です。
インスペクターの回転量が制限値より大きく、マウスが上方向に移動している場合、{}内の処理を実行します。
カメラを上方向にx軸を回転させると、xの回転量がマイナスになったので制限値の変数名もMinがついてます。

        // カメラの下方向の回転量が指定の上限より小さい場合
        // カメラの回転方向が0より小さい場合
        if (yRotateParam <= rotateLimitMax && yRotate < 0)
        {
            // カメラをマウスの移動量に応じて回転させる
            camera.transform.RotateAround(player.transform.position, transform.right, -yRotate);
        }

カメラの下方向に対する回転制御です。
インスペクターの回転量が制限値より小さく、マウスが下方向に移動している場合、{}内の処理を実行します。
カメラを下方向にx軸を回転させると、xの回転量がプラスになったので制限値の変数名もMaxがついてます。


上記の追記したスクリプトをカメラにアタッチすると、こんな感じの動作になると思います。

カメラが上下方向に対して一定の角度まで傾いていることが確認できると思います。
多分この先、壁があった時すり抜けないようにとかいろいろ考えなきゃいけないものがありそうだけど、いったんこれで完成とします。

滝登りの修正とアップデート


前の記事であげた滝登りゲーで面白そうなアドバイスをいただいたので、今回も修正とちょっとしたアップデートをしました。
いつもアドバイスくれて助かってます、ありがとうございます。
新しくなった滝登りゲーは下記リンクから遊んでみてください。

unityroom.com


以下ゲーム説明です。
 【操作方法】
 ・マウスカーソルを動かして鯉を移動させるだけです。

ゲーム内には足場として使える木箱が現在3種類登場しますが、性能は以下の通りです。
 ・茶色の木箱…普通の木箱。何回でも足場として使えます。
 ・赤色の木箱…踏むとめちゃ飛びます。頭上に気を付けて飛んでください。
 ・青色の木箱…一度踏むと消えます。慎重に踏んでください。

今回のゲームにクリア条件はないです。ただただ滝を登るだけのゲームです。
なので鯉が一定距離ゲーム画面より落下してしまうとゲームオーバーです。

修正点は、BGMを設定してもメイン画面に戻ると設定情報がリセットされる不具合の修正をしました。
アップデートは、カメラが強制スクロールするようにしました。
これで同じ木箱の上でチンタラ跳ねていたり、他の木箱に飛び移るのをミスったりするとすぐゲームオーバーになると思うので、結構難易度は上がったかなと思います。
ホントは木箱を移動させるようなギミックもつけようかなと思ったんですが、実装してみたらかなりムズくて面白くなかったんでやめました。

なんかバグとか面白そうなアイデアあったらどんどん教えてくださ~い

【Unity】鯉が滝を登るゲーム


またUnityでゲーム作りました。
小さな鯉を操作して滝登りさせてあげるゲームです。滝から流れてくる木箱を踏みながらいっぱい滝登りしちゃってください。
今回のタイトルはまじめに考えました。他にセンスあるタイトル思いついた方いたらパクリたいので教えてください。

unityroom.com

上記のサイトからじゃ読み込みが遅くてできない方はいつも面倒になっちゃうんですけど、下記のリンクからzipファイルをダウンロードしてください。

xgf.nu

zipファイルをダウンロードした場合、解凍して「Takinobori.exe」をクリックしてください。

以下ゲーム説明です。
 【操作方法】
 ・マウスカーソルを動かして鯉を移動させるだけです。

ゲーム内には足場として使える木箱が現在3種類登場しますが、性能は以下の通りです。
 ・茶色の木箱…普通の木箱。何回でも足場として使えます。
 ・赤色の木箱…踏むとめちゃ飛びます。頭上に気を付けて飛んでください。
 ・青色の木箱…一度踏むと消えます。慎重に踏んでください。

今回のゲームにクリア条件はないです。ただただ滝を登るだけのゲームです。
なので鯉が一定距離ゲーム画面より落下してしまうとゲームオーバーです。
また、今回はBGMを試しにつけてみました。
メインメニュー画面の右下から音量を調節できるので、適宜いじって調整してください。
難易度としては、滝を登れば登るほど難しくなるようには調整してみたので、結構一筋縄じゃいかないのかな~と思います。
作った僕でも700mくらいが限界だったので、みなさんは1000m以上いけるか挑戦してみてください。

このゲームを作った目的としては、2Dゲームの作り方をざっくり覚えようというのが主な目的です。
3D半裸男に比べると、今回のゲームはカメラ制御とか半裸男の移動処理やモーション設定みたいな、めんどくさい調整がなかったので比較的すらすらと実装が進みました。

ただ、自分でもこの滝登りゲームを遊んで確認した不具合がいくつかあったんですが、だいぶ形になったのでいったん本ブログにまとめようと思いました。
現状把握してる不具合は、

 ・音量を設定しても、メインメニューに戻ると設定情報がリセットされる
 ・WEB版でプレイすると、画面端らへんにある木箱が見切れるときがある
  (zipファイルをダウンロードしてexeファイルからプレイすると見切れる現象は起きませんでした)

ホントは上記2つの問題を解決してからアップしたかったんですけど、明日も早く起きなきゃいけない限界社畜マンなので修正する時間がありませんでした;;;;;

こういう機能があったら面白そうだな~とか、こうしてみたらいいんじゃね?みたいなアイデアや改善点などがあったらどしどし教えてください。
あとめちゃ急いで作ったので、不具合とかもすんごいあると思うので見つけたら教えてください~おねがいします。

【Unity】2Dゲームの罠

今2Dのゲーム作ってるんですけど、かなり初歩的な部分でめちゃ躓いてしまったので、戒めのためにもこの記事にまとめておきます。

それは2Dゲームを制作していたあるときのこと

操作可能なプレイヤーともなるオブジェクト(以降鯉とします)にColliderとRigidBodyを付けて、足場となるオブジェクト(以降木箱とします)にColliderを付ければ、鯉と木箱間で当たり判定が発生するだろうな~と思って実行してみたところ、こんなことが起きました。

鯉がすり抜けてる…おかしい

あまりにもおかしいので、鯉と木箱のインスペクターに何が追加されているかを確認してみた
まずは鯉の方から

しっかりRigidBodyとCapsuleColliderがついてます。

次に木箱のインスペクターです。

こっちにもちゃんとBoxColliderがあります。

ちなみに調べてみたところ、2DのColliderと3DのColliderをどっちも使うことは無理みたいです。
つまり、今回の例で行くと、鯉に2DのColliderを付けて、木箱に3DのColliderを付けても当たり判定は発生しないみたいです。
ただ、今回はどっちも3Dのを使ってるし、なんでだろうな~~と思いながらシーンタブをいじくりまわしていたら…

3D表記にできるんかいコイツ

しかもめちゃくちゃ鯉と木箱のZ座標ずれとるがな

結論としては、鯉と木箱のZ座標の位置が一致してなかったことが原因でした。
というか、2Dのゲームだからシーンを3Dにできないというのと、Z座標なんて使わないという先入観もあったのがまずかったかもしれない。
2DのゲームだからXとY座標さえあればいいんじゃね?って思ってたけど、確かに奥行きとか設定するのにZ軸もいるよね…

ということで、鯉と木箱のZ座標を見直します。

鯉と木箱の位置を調整しました、今度こそすり抜けるようなことは起きないはず

無事鯉が木箱にぶつかったり、ジャンプする動作ができるようになりました~やった~~

ということで、今回の原因はゲーム制作に無知すぎたがゆえに起きたことがわかりました。
これから2Dのゲームを作るときもZ座標のことを気にかけてあげようと思います。

【Unity】3D半裸男の機能追加

前回、この記事から3D半裸男を遊んでくれた方の意見をもとに、少しだけ機能の追加をしました。
いろいろとアドバイスをくれた方、本当にありがとうございます。

今回バージョンアップした3D半裸男は以下のリンクより遊べます。
unityroom.com

いつものように上のリンクでなかなかゲームが起動せず遊べない場合は、
面倒になっちゃうんですけど下記のリンクからzipファイルをダウンロードしていただけるとうれしいです。

xgf.nu


zipファイルをダウンロードした場合、解凍して「ActionGame.exe」をダブルクリックするとゲームが起動します。

【操作方法】
 ・WASDキー … キャラの移動
 ・左shiftキー … ダッシュ
 ・マウス右クリック … 押してる間狙いを定める(離すと攻撃します)
 ・マウス左クリック … 狙うのをやめる
 ・escキー … ポーズ画面(カメラ感度の変更ができます)
 ・マウスホイールクリック …近くにいる敵をロックオンします。
 ・Qキー … ロックオン中、近くに複数敵がいる場合、ロックオン対象を切り替えます。
 ・Fキー … ロックオンを解除します。

画面左下にある緑色のHPバーが0になるとゲームオーバーです。
また、ステージのどこかにいるちょーでかい敵を倒すとゲームクリアです。
道中に落ちてる医療キットに触れると体力が回復します。
あと、相変わらずこのゲームにはBGMやSEは一切ありません、申し訳ないです。

今回の主な変更点としては、カメラワークの修正と、ロックオン機能の追加です。
カメラワークの修正としては、以前カメラがつっかかって思うように動かせないような現象があったので、その修正になります。
ロックオン機能は、言葉の通り近くにいる敵をずっとカメラが注視するような機能を付けました。
基本的に道中の敵をロックオンすると、HPバーが表示されますが、ボスはロックオンしてもHPが表示されません。
これに関してはあえてHPを表示させないようにしてるだけで、バグではなく仕様なので気にしないでください。
ただ壁の向こうとかにいてもロックオンしちゃうんですけど、その辺もバグではなくて仕様なので勘弁してください;;


他にも狙う際に照準を付けるだとか、セーブ(チェックポイント)機能の追加とか、攻撃方法の追加、BGM・SEの追加とか、いろいろ面白そうな意見をいただいたので、また新しいゲームを作っている合間の時間あるときに追加していこうかなと思います。

引き続きこういう機能あったらおもしろそうだな~とか、改善点とか僕が意図してない不具合などあったら教えてくれるとめちゃうれしいです。

【Unity】敵をロックオンする機能を実装するまでの記録

前に3D半裸男を遊んでもらったときに、
「ロックオン機能が欲しい」
「敵のHPを表示させたら?」

みたいなアイデアをいただきました、ありがとうございます。ぜひ実装させていただきます。

実現したい機能としては、「敵をロックオンしてる間、カメラがその敵をずっと注視してHPバーを表示させる」みたいなものを実装させていこうと思います。
大まかな考え方の流れとしては、

  1. プレイヤーを中心に敵を探知する範囲を作成
  2. 敵がその範囲内に入っていたらロックオン対象リストに入れる
  3. 指定のキーを押すとリストの中からロックオンする敵を選ぶ
  4. カメラがロックオン対象の敵を注視する
  5. 注視している敵のHPを表示させる

みたいな感じでいこうかなーと思います。

1.プレイヤーを中心に敵を探知する範囲を作成
まずプレイヤーでもある半裸男にSphere Collierを追加します。

ここでは、追加したColliderのOnTriggerにチェックを入れてます。
そうすることで敵が範囲に触れたかどうかを判断したいと思います。

試しに範囲に触れたオブジェクト内に敵がいるかを判断するコードを書いてみました。

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.layer == 15)
        {
            Debug.Log("敵がいる");
        }
    }

上記のコードを半裸男にアタッチして、動作の確認をしてみました。

すこし見にくいんですけど、半裸男を中心に広がっている大きな円に白い四角のオブジェクトが触れると、画面下部に「敵がいる」と表示されています。
ここまでは想像通りに行ってるので、次は敵をロックオン対象に入れる処理を考えていこうと思います。

2.敵が範囲内に入っていたらロックオン対象リストに入れる
次に、ロックオン範囲内に敵が入っていたら、リストに追加する処理を実装しようと思います。
ぶっちゃけ自分は世に出回っているゲームのロックオン機能を隅々まで理解しているわけではないので、大まかな仕様としては、

 ・範囲外に出た敵はロックオン不可(ロックオン中の場合はロックオンが勝手に外れる)
 ・範囲内にいる間はロックオンの対象にする
 ・敵が複数いる場合、指定のキー操作でロックオン対象を切り替え可能

上記3点を基本として実装を進めていこうと思います。
敵が範囲内に入ったかの判断は少し前に確認できたので、次は範囲外に出たかを確認する処理を追加します。
とりあえずで書いたコードはこんな感じ

    private void OnTriggerExit(Collider other)
    {
        if(other.gameObject.layer == 15)
        {
            Debug.Log("敵が範囲外に出た");
        }
    }

それではまた実際に動かしてみます。

これまたちょっと見にくいんですけど、部屋の隅にいる四角いオブジェクトに一定範囲近づくとログが一行表示され、一定範囲離れるとまたログが一行追加で表示されていると思います。
なので、このままリストに追加・削除する処理を以下のように書きます

    private void OnTriggerEnter(Collider other)
    {
        // 範囲に触れたオブジェクトが敵の場合
        if (other.gameObject.layer == 15)
        {
            //Debug.Log("敵がいる");
            // ロックオン対象リストに追加
            targetList.Add(other.gameObject);
        }
    }

    private void OnTriggerExit(Collider other)
    {
        //範囲から出たオブジェクトが敵の場合
        if(other.gameObject.layer == 15)
        {
            //Debug.Log("敵が範囲外に出た");
            // ロックオン対象リストから削除
            targetList.Remove(other.gameObject);
        }
    }

これで範囲内・範囲外のロックオン対象リストの編集ができるようになりました。


3.指定のキーを押すとリストの中からロックオン対象を選ぶ
この調子でRキーを押すとロックオン開始、Qキーでターゲット切り替え処理を書いていきます。

   // ロックオン中フラグがfalseの状態でRキーを押した場合
        if (Input.GetKeyDown(KeyCode.R) && !IsLockOnNow && targetList.Count > 0)
        {        
            targetIndex = 0;
            // ロックオン中フラグをtrueにする
            IsLockOnNow = true;
            // リストのインデックスが0のオブジェクトを代入する
            targetObject = targetList[targetIndex];
            Debug.Log("ロックオン開始");
        }

        // ロックオン中フラグがtrueの状態
        // かつロックオン対象リストの数が2以上の状態でQキーを押した場合
        if(Input.GetKeyDown(KeyCode.Q) && IsLockOnNow && targetList.Count > 1)
        {
            // 現在ロックオンしてるオブジェクトのインデックスを取得
            int targetListNum = targetList.IndexOf(targetObject);
            // リストの最後のオブジェクトを取得する
            GameObject lastObject = targetList.Last();
            // 最後のオブジェクトのインデックスを取得
            int lastListNum = targetList.IndexOf(lastObject);

            // ロックオンしてるオブジェクトと最後のオブジェクトのインデックスが一致してる場合
            if(targetListNum == lastListNum)
            {
                targetIndex = 0;
                // インデックスが0のオブジェクトを代入する
                targetObject = targetList[targetIndex];
                Debug.Log(targetObject);
            }
            // ロックオンしてるオブジェクトと最後のオブジェクトのインデックスが一致してない場合
            else
            {
                // インデックス指定変数を+1する
                targetIndex = targetIndex + 1;
                // 次のインデックスにあるオブジェクトを代入する
                targetObject = targetList[targetIndex];
                Debug.Log(targetObject);
            }
        }

        // ロックオン中フラグがtrueの状態でFキーを押下した場合
        if(Input.GetKeyDown(KeyCode.F) && IsLockOnNow)
        {
            IsLockOnNow = false;
        }

急に論文並みの長文になってしまった。
上記のコードは敵が範囲外に出た場合を考慮してないので、次のような処理も追加します。

    private void OnTriggerExit(Collider other)
    {
        // 範囲から出たオブジェクトが敵の場合
        if(other.gameObject.layer == 15)
        {
            // ロックオン対象リストから削除
            targetList.Remove(other.gameObject);

            // ロックオン中の場合
            if (IsLockOnNow)
            {
                // ターゲット中の敵と範囲から出た敵の名前が同じ場合
                if(other.gameObject.name == targetObject.name)
                {
                    // ロックオン中フラグをfalseにする
                    IsLockOnNow = false;
                    Debug.Log("ロックオン終了");
                }
            }
        }
    }

先ほどの範囲外に出た際の処理に、ロックオン中の時の処理を追加しました。
IsLockOnNowとtargetObjectはカメラ制御で使う予定です。
いったんこれでロックオン対象リストは作成完了とします。

4.カメラがロックオン対象の敵を注視する
次にカメラ制御の実装に取り掛かろうと思います。
この記事にはある程度形になったものだけ載せますが、裏ではかなりうんうん唸りながら実装しました。


今回の機能追加にて、実現させたいカメラの動きとしては、
下の画像のように敵とプレイヤーの延長線上にカメラを置いて、敵もプレイヤーもカメラに映るようにします。

なので、図でいう①と②の位置がわかればカメラの位置もわかるのでは?ということです。

実際のカメラを制御するコードはこちら

        if (TargetLockOn.IsLockOnNow)
        {
            GameObject _targetObject = TargetLockOn.targetObject;
            // ロックオン対象からプレイヤーへのベクトルを算出
            Vector3 vectorTarget = _targetObject.transform.position - player.transform.position;
            // ロックオン対象へのベクトルを前方とするクォータニオンを取得
            Quaternion rotationTarget = Quaternion.LookRotation(vectorTarget);
            // ロックオン中のカメラの位置を指定
            Vector3 cameraPosition = rotationTarget * new Vector3(0, 2, -2);
            cameraObject.transform.position = player.transform.position + cameraPosition;

            // カメラが上下に回転しないようにする
            vectorTarget.y = 0f;
            // ロックオン対象に向くようなクォータニオンを取得
            Quaternion cameraRotation = Quaternion.LookRotation(vectorTarget);
            // カメラを回転させる
            cameraObject.transform.rotation = cameraRotation;
        }
        else
        {
            // カメラのy方向の高さを1.8に合わせる
            cameraObject.transform.position = new Vector3(cameraObject.transform.position.x, 1.8f, cameraObject.transform.position.z);
            // マウスの現在位置の座標を、移動前のマウスの座標で引いた値を代入
            float xRotate = xRotateParam * Input.GetAxis("Mouse X");
            // カメラをプレイヤーの位置座標を中心にして回転させる
            cameraObject.transform.RotateAround(player.transform.position, Vector3.up, xRotate);
        }

画像の①の部分はここで求めています。

            // ロックオン対象からプレイヤーへのベクトルを算出
            Vector3 vectorTarget = _targetObject.transform.position - player.transform.position;

②の部分はここ

            // ロックオン対象へのベクトルを前方とするクォータニオンを取得
            Quaternion rotationTarget = Quaternion.LookRotation(vectorTarget);
            // ロックオン中のカメラの位置を指定
            Vector3 cameraPosition = rotationTarget * new Vector3(0, 2, -2);

プレイヤーと敵のベクトルをもとにQuaternionを取得し、高さと後ろの距離をnew Vector3で指定することで、カメラの位置を決めています。
カメラの位置が決まったら、あとはロックオン対象の方にカメラが向くようすれば完成です。

ここまでの実装が完了したものがこれ

ロックオンが始まると、敵と半裸男の延長線上にカメラが常にいることが確認できますね。
しかも、ロックオン対象の切り替えもうまく行ってる様子です。
この調子で、最後はHPバーの表示をしましょう。


5.注視している敵のHPを表示させる
最後に敵キャラにHPバーをつけて、それをロックオン中は表示するようにします。
まずは敵の子要素にUI > スライダーを追加

Canvasを「EnemyHPUI」
Sliderを「EnemyHP」
命名
「Handle Slide Area」はいらないので非表示

EnemyHPUIのレンダーモードを「ワールド空間」に設定

これで上にある「Rect Transform」タブの中身がいじれるので、スライダーが敵の頭上に来るようEnemyHPUIの位置とスケールを調整する。
次にEnemyHPの子要素としてある「Background」、「Fill Area」、「Fill」の赤枠内を「0」にする。

そうすることでスライダーが左から右まで正常に表示されるようになる。
次に「Background」と「Fill」のソース画像を「None(なし)」に変更し、色をそれぞれ適当に決める

自分は「Background」の色を黒、「Fill」の色を赤に設定しました。
ここまでの設定が完了すれば、EnemyHPのインスペクター内にあるSliderの「Value」をいじればスライダーが下記の動画のように動作するはず

次にスクリプトからEnemyHPの制御をする。
まずはHPの変化させる関数。

    public void EnemyHPBarChange(float enemyHP)
    {
        enemyHPBar.value = enemyHP;
    }

これを敵のHPを計算してるクラスから呼び出し、そのときの引数をそのままHPバーに反映させてます。
ちなみに上のを呼び出す処理はこっち

    [SerializeField] private GameObject enemyObject;
    private EnemyHPUI eHPUI = new EnemyHPUI();

    void Start()
    {
        eHPUI = enemyObject.GetComponent<EnemyHPUI>();
    }

    private void OnCollisionEnter(Collision other)
    {
        // プレイヤーの出す弾に当たった場合
        if(other.gameObject.tag == "Bullet")
        {
            // 弾の威力だけ敵のHPを減らす
            enemyLife = enemyLife - bMove.bulletDamege;
            eHPUI.EnemyHPBarChange(enemyLife);
        }
    }

ちゃんとした原因はわからないけど、他のクラスの関数を呼び出すにはGetComponentしないとnullreferenceexceptionが出てしまったので、上記のようにコードを書いてます。

次にHPバーの表示の有無に関するコード

        // HPバーを常にカメラに向ける
        enemyUI.transform.LookAt(Camera.main.transform.position);
        if(enemyObject != null)
        {
            // ロックオン中でロックオンリストが1以上の場合
            if (TargetLockOn.IsLockOnNow && TargetLockOn.targetList.Count > 0)
            {
                // ロックオンしてる敵の名前が一致している場合
                if (TargetLockOn.targetObject.name == enemyObject.name)
                {
                    // HPバーを表示
                    enemyUI.SetActive(true);
                }
                // ロックオンしてる敵の名前が一致してない場合
                else if (TargetLockOn.targetObject.name != enemyObject.name)
                {
                    // HPバーを非表示
                    enemyUI.SetActive(false);
                }
            }
            else
            {
                // HPバーを非表示
                enemyUI.SetActive(false);
            }
        }

ひとことで言うと、ロックオン中ならHPバーを表示させる処理です。

あとHPバーとは関係ないんですけど、ロックオン中に敵が倒された場合のロックオン対象リストの削除に関するコードを載せておきます。

        List<GameObject> _targetList = targetList;
        // 順番にロックオンリストのオブジェクトを取得
        foreach (GameObject _gameObject in _targetList)
        {
            // リスト内のオブジェクトがnullではない場合
            if (_gameObject != null)
            {
                // リスト内のオブジェクトがactiveではない場合
                if (!_gameObject.activeInHierarchy)
                {
                    // Remove用変数にactiveではないオブジェクトを代入する
                    removeObject = _gameObject;
                    //_targetList.Remove(_gameObject);

                    // ロックオン中の場合
                    if (IsLockOnNow)
                    {
                        // ロックオン中のオブジェクトと名前が一致する場合
                        if (_gameObject.name == targetObject.name)
                        {
                            // ロックオンをやめる
                            IsLockOnNow = false;
                        }
                    }
                }
            }
            // リスト内のオブジェクトがnullの場合
            else
            {
                // Remove用変数にnullのオブジェクトを代入する
                removeObject = _gameObject;
                //_targetList.Remove(_gameObject);
            }
        }

        // removeObjectに入っているオブジェクトをリストから消す
        _targetList.Remove(removeObject);
        targetList = _targetList;

なんだこのif文の数は

ここまでごちゃごちゃしたコードになった原因としては、foreach文の中でリストの削除を行うと「InvalidOperationException」というエラーが起きたので、その対策になります。
InvalidOperationExceptionとは、「foreach文の中でリストの追加・削除をするな」とunityから怒られてしまっている状態です。
なので、一時的に削除対象になったオブジェクトを指定の変数の中に入れて、foreachの処理が終わったらRemoveで消すようにしました。

でもこれって削除対象が2つ以上発生するとremoveObject内の値が上書きされる気がするんだけど、大丈夫なんですかね??
もっと上手な方法がありそうだけど、テストプレイしてみた感じ特に不具合とかなかったんでこれでいきます。

長くなってしまったが、ここまでの実装が完了した様子がこれです

完成です。長かった~~~

まとめ
クソ疲れました。
今回作ったロックオン機能もある程度形にはなったと思うので、またほかのゲームとかにも使いまわせるのではと思います。
ただ今回作ったのも結構粗削りな部分もあるので、もっとスマートに実装できるのかな~と思います。
もしこのロックオン機能をアレンジするなら
 ・ロックオン開始時、一番近い敵をロックオンする
 ・現在ロックオンしてる敵がいなくなったら、次の敵を自動的にロックオンする
 ・ロックオンしてる敵が壁に隠れたら、ロックオンをやめる
 (もしくは敵のシルエットを表示させる)
など、いろいろ手の施しようがありそう

とりあえずこれでロックオン機能の実装を完了させたいと思います。

【Unity】カメラワークについて考える

前回僕がアップしたうんちみたいなゲームを、無理言って周りの人にやらせました。
そしたら想像してたよりもちゃんとしたアドバイスなどをもらいました。本当に感謝してます。
正直ゲームの出来もあんまよくないから、結構ふざけた意見しかないかなと思ってました、ごめんなさい;;


アドバイスの中でも、特に多かったものとしては

「カメラワークがクソ」

が9割を占めてました。


何言ってんだ~~?って思いながら実際に自分がネットにあげた3D半裸男をやってみると、確かにおかしい…
下の動画が実際に操作している様子です。

なんかパントマイムやってるように見えるんですが、実際はカメラがつっかえるような感覚があり、動画ではどんなにマウスを動かしても一定以上カメラが動かないような現象が見られました。

原因を探ってみたら、以下の2つが主な理由でした。
 
 ・マウスカーソルの現在位置をもとにカメラの操作を行っていた
 ・マウスカーソルを画面外まで動かせるようにしていた

なので今回のことがまた起きないよう、カメラワークの制御に関するコードの修正箇所を簡単にまとめておこうかなと思います。
まず、マウスカーソルが画面外に出ないようにする処理ですが、

    void Start()
    {
        // マウスカーソルを動かせないようにする
        Cursor.lockState = CursorLockMode.Locked;
        // マウスカーソルを非表示にする
        Cursor.visible = false;
    }

このように実装してみました。
これでスタートボタンをクリックしたらマウスカーソルが非表示になり、画面外に移動してしまうことがなくなるはずです。
また、ポーズ画面関連でのマウスカーソルの処理としては、

        // 時間が止まっている場合
        if (Mathf.Approximately(Time.timeScale, 0f))
        {
            // マウスカーソルを動かせるようにする
            Cursor.lockState = CursorLockMode.None;
            // マウスカーソルを表示にする
            Cursor.visible = true;
        }
        // 時間が止まっていない場合
        else
        {
            // マウスカーソルを動かせないようにする
            Cursor.lockState = CursorLockMode.Locked;
            // マウスカーソルを非表示にする
            Cursor.visible = false;
        }

こんな感じに実装してみました。
コードの動きとしてはコメントに書いてある通りなのですが、ポーズ画面中はマウスカーソルを動かせるようにし、ポーズ画面を解除したときはマウスカーソルが動かせないようにしました。

ちなみに、Cursorでマウスカーソルの制御を色々と変更することができるそうです。
上記のを含めてCursorで実現可能な動作としては

        // マウスカーソル表示
        Cursor.visible = true;
        // マウスカーソル非表示
        Cursor.visible = false;

        // マウスカーソルを自由に動かせる
        Cursor.lockState = CursorLockMode.None;
        // マウスカーソルを画面内で動かせる
        Cursor.lockState = CursorLockMode.Confined;
        // マウスカーソルを画面中央にロックする
        Cursor.lockState = CursorLockMode.Locked;

とかがあるみたいです。結構使い道がありそう


さっき実装したコードをもとに実行してみたら、マウスが画面中央から移動しなくはなったんですが、今度はカメラがまっっったく動かないという現象が起きました。
これの原因としては、冒頭でも書いたようにマウスカーソルの現在位置をもとにカメラの回転量を決めていたからです。
コードでいうとこんなかんじ

        // マウスの現在位置の座標を、移動前のマウスの座標で引いた値を代入
        Vector3 nowMousePosition = Input.mousePosition - lastMousePosition;

        // 回転速度とマウスの位置座標の差分を掛けた値を代入
        newAngle = Vector3.zero;
        newAngle.x = xRotateParam * nowMousePosition.x;

        // カメラをプレイヤーの位置座標を中心にして回転させる
        cameraObject.transform.RotateAround(player.transform.position, Vector3.up, newAngle.x);

        // 現在のマウスの位置座標を、最後のマウスの位置座標として代入する
        lastMousePosition = Input.mousePosition;

以前のマウスカーソルと現在のマウスカーソルの位置の差分を取得して、それをカメラの回転量として使用しています。
ただ、マウスカーソルが動かなくなってしまった以上、こんなソースコードはクソの役にも立ちません。
なので、以下のように修正しました。

        // マウスのX方向の移動量と回転速度を掛けた値を代入
        float xRotate = xRotateParam * Input.GetAxis("Mouse X");

        // カメラをプレイヤーの位置座標を中心にして回転させる
        cameraObject.transform.RotateAround(player.transform.position, Vector3.up, xRotate);

ずいぶんスリムなコードになりました。
修正後のコードの動きとしては、マウスをどれだけ動かしたかで回転量が決まります。
なのでマウスカーソルの位置は見てないので、さっきまでの現象はもう発生しないはず。


実際に動かしてみました。

おお~~~いいかんじになりました。
それにEscキーを押すたびにカーソルが中心に固定されてますね。
なんとか実装したい処理が実現できたので、これでカメラワークの修正を終わりにしようと思います。