リヨスケのチラシ裏

学んだことを書き留めていく自分のための技術メモです。

【Unity】メイン画面上にマウスに追従する望遠カメラ画面を表示したい

狙撃銃のスコープのような望遠画面をメインカメラの画面の上に表示し、FPSのようにマウスで動かせるようにしたメモです。

 

↓のような感じに。

f:id:riyosuke_13:20180513114527g:plain

 

カメラを用意

メインカメラとは別に新規にカメラを用意し、transformをメインカメラと同じ位置に設定します。

狙撃銃のようなスコープにしたいので、インスペクターの「Field of View」を調整します。(今回は2に設定)

f:id:riyosuke_13:20180513115229p:plain

f:id:riyosuke_13:20180513115252p:plain

このようにぐっと拡大して表示されるようになります。

 

カメラをメインカメラの画面上に配置する

今の状態ではメインカメラを完全に隠してしまっていうるので、メインカメラ画面の上に小さく表示されるようにします。

レンダーテクスチャというものを作成し、スコープ用カメラの映像をリアルタイムで表示するテクスチャを用意。

それをGUIのイメージのテクスチャとして設定して画面上に表示するようにします。

こちらのサイトを参考にしました。

gametukurikata.com

 

手順は以下のとおり

  1. ProjectからCreate→「Render Texture」でレンダーテクスチャを作成
  2. 用意したスコープ用カメラの「Target Texture」に作成したレンダーテクスチャを設定
  3. Hierarchyに新規にCanvasを作成し、その子として「Raw Image」作成
  4. 「Raw Image(Script)」コンポーネントの「Texture」にレンダーテクスチャを設定

f:id:riyosuke_13:20180513120619p:plain

見辛いので赤枠で囲っていますが、メインカメラ画面の上にスコープ用カメラの画面が小さく表示されるようになりました。

 

マスクをかける

このままではスコープっぽくないので、マスクをかけて丸く表示されるようにします。

ヒエラルキービューのCanvasを右クリックし、Panelを作成します。

Panelのインスペクターで「Add Componet」→「UI」→「Mask」を選択。

「Source Image」から「knob」を選択して大きさを調整します。

f:id:riyosuke_13:20180513120738p:plain

このPanelの子として先ほどのスコープ用画面のRow Imageを配置してやると、以下のようにマスクがかけられた状態で表示されます。

f:id:riyosuke_13:20180513120805p:plain

 

スクリプトの作成

スコープ用の画面をメインカメラ上に表示するところまで実装できました。

次に、このスコープ用の画面をマウスの動きにぴったり合わせて動くようにします。

 

以下の方法をとりました。

  1. メインカメラからレイを投げる
  2. レイが衝突したコライダーの情報からpositionを取得する
  3. positionをワールド空間のものからスクリーン空間のものへ変換する
  4. 変換したpositionをスコープ用画面のRectTransformに渡す

ソースコードは以下

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

public class ScopeRendar : MonoBehaviour {

    RectTransform scopeRect;
    private float camRayLength = 80000f;

    void Start () {
        scopeRect = GetComponent<RectTransform>();
    }

    void Update () {
        // メインカメラからマウス位置までレイを取得
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        // 以下のRaycasで投げられたレイが衝突したコライダーの情報を詰め込むオブジェクトを用意
        RaycastHit floorHit;
        // レイを投げ、コライダーに衝突した場合はfloorHitにそのコライダーの情報を詰めて以降の処理を実施する
        if (Physics.Raycast(ray, out floorHit, camRayLength)) {
            // 第2引数であるレイが衝突したコライダーのpositionを、ワールド空間のものからメインカメラ画面のスクリーン空間のものへ変換する
            scopeRect.position = RectTransformUtility.WorldToScreenPoint(Camera.main, floorHit.point);
        }
    }
}

 まずScreenPointToRay関数で画面上のマウス位置に相当するワールド空間へのレイを取得します。

平面である画面の座標を立体であるワールド座標にうまく変換した上でレイを発してくれます。

 

次に、Raycast関数を使ってレイが衝突したコライダーの情報を取得します。

outキーワードによってあらかじめ用意したRaycastHit型のオブジェクトにコライダー情報を詰めてくれます。

ソースコード上の80000fはレイを投げる最大距離であり、今回の数値設定はテキトーです(汗

 

取得したコライダー情報から座標を抜き出し、RectTransformUtilityのWorldToScreenPoint関数でワールド座標からスクリーン座標へ変換し、スコープ用画面に渡します。

guiにはTransformではなく、RectTransformで位置を調整する必要があります。

 

作成したスクリプトをスコープ用画面にアタッチします。

この時に、Row Imageの方にアタッチするとマスクが動かずに取り残されてしまいます。

なのでマスクであるPanelにアタッチし、親子関係を利用して一緒に動くようにします。

 

これでマウスに合わせて動くスコープができ・・・ま・・・

f:id:riyosuke_13:20180513121611g:plain

できてない。スコープ画面の中身が動いてないです。。

スコープ用カメラの向きををマウス位置に合わせて動かすのを忘れていました(汗

 

スクリプトを作成。

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

public class ScopeContoroller : MonoBehaviour {

    private float camRayLength = 80000f;

    void Start () {

    }
    
    void Update () {
        Aim();
    }

    void Aim() {
        RaycastHit floorHit;
        if (Physics.Raycast(GetRayFromMainCamera(), out floorHit, camRayLength)) {
            transform.LookAt(floorHit.point);
        }
    }

    public Ray GetRayFromMainCamera() {
        return Camera.main.ScreenPointToRay(Input.mousePosition);
    }
}
 

 先と同様にメインカメラから投げたレイの衝突情報から座標を取得し、LookAt関数でサブカメラの向きを変更します。

今回はどちらもワールド座標なので変換は必要ありません。

 

このスクリプトをスコープ用カメラにアタッチすれば今度こそ冒頭のような動きになります。

 

 

 

マウス位置を取得してスコープ用画面を動かすのはいちいちレイなんて投げなくても直接Input.mousePositionを変換して渡してやればいけるかなと思ったのですが、なぜかうまくいきませんでした。

あとは、メインカメラからのレイを取得する部分などコードが重複しているところを共通化したいですね。

ネームスペースをちゃんと活用すればまとめられるのかな?

要勉強です。